1 /**
2 *   Application contains four build configurations: production, unittests, integration test 1,
3 *   integration test 2 and test client.
4 *
5 *   Unittests configuration produce dummy executable the only purpose is to run module unittests.
6 *
7 *   Production configuration is main and default configuration. There the configuration files and
8 *   argument parameters are parsed, daemon or terminal mode is selected and actual rpc server starts.
9 *
10 *   Integration test 1 performs simple tests on real PostgreSQL instance. The configuration expects
11 *   '--conn' parameter with valid connection string to test database. The main thing that is tested
12 *   is connection pool operational correctness.
13 *
14 *   Integration test 2 performs major tests on real PostgreSQL instance. The configuration expects
15 *   '--conn' parameter with valid connection string to test database. There are many tests for
16 *   binary converting from libpq format. This test is the most important one as it should expose
17 *   libp binary format changing while updating to new versions. 
18 *
19 *   Test client is most general integration tests set. First client expects that there is an instance
20 *   of production configuration is running already. The '--host' argument specifies URL the server is
21 *   binded to (usually 'http://localhost:8080). The '--serverpid' argument should hold the server 
22 *   process PID to enable automatic server reloading while changing json-rpc table. '--conn' and
23 *   '--tableName' specifies connection string to PostgreSQL and json-rpc table respectively that are 
24 *   used in server being tested.
25 *
26 *   Test client performs automatic rules addition to json-rpc table, testing request sending to
27 *   the production server and checking returned results. Finally test client cleans up used
28 *   testing rules in json-rpc table.
29 *
30 *   Copyright: © 2014 DSoftOut
31 *   License: Subject to the terms of the MIT license, as written in the included LICENSE file.
32 *   Authors: NCrashed <ncrashed@gmail.com>
33 *            Zaramzan <shamyan.roman@gmail.com>
34 */
35 module app;
36 
37 version(unittest)
38 {
39     void main() {}
40 }
41 else version(RpcClient)
42 {
43     import std.getopt;
44     import std.process;
45     import std.conv;
46     import std.stdio;
47     import client.client;
48     import client.test.testcase;
49     import client.test.simple;
50     import client.test.nullcase;
51     import client.test.numeric;
52     import client.test.unicode;
53     import client.test.multicommand;
54     import client.test.longquery;
55     import client.test.notice;
56     import client.test.singlequery;
57     import client.test.onerow;
58     import client.test.array;
59     import client.test.timestamp;
60     import client.test.auth_info;
61     
62     immutable helpStr =
63     "JSON-RPC client for testing purposes of main rpc-server.\n"
64     "   pgator-client [arguments]\n\n"
65     "   arguments = --host=<string> - rpc-server url\n"
66     "               --conn=<string> - postgres server conn string\n"
67     "               --tableName=<string> - json_rpc table\n"
68     "               --serverpid=<uint> - rpc server pid\n";
69 
70     uint getPid()
71     {
72 	uint pid;
73 	File("/var/run/pgator/pgator.pid", "r").readf("%d", &pid);
74 
75 	return pid;
76     }
77 
78     // Getting pid via pgrep
79     uint getPidConsole()
80     {
81         return parse!uint(executeShell("pgrep pgator").output);
82     }
83     
84     int main(string[] args)
85     {
86         string host = "http://127.0.0.1:8080";
87         string connString;
88         string tableName = "json_rpc";
89         uint pid;
90         bool help = false;
91         
92         getopt(args,
93             "host", &host,
94             "help|h", &help,
95             "conn", &connString,
96             "tableName", &tableName,
97             "serverpid", &pid
98         );
99         
100         if(help || connString == "")
101         {
102             writeln(helpStr);
103             return 1;
104         }
105         
106         if(pid == 0)
107         {
108             writeln("Trying to read pid file at '/var/run/pgator/pgator.pid'");
109             try pid = getPid();
110             catch(Exception e)
111             {
112                 writeln("Trying to read pid with pgrep");
113                 try pid = getPidConsole();
114                 catch(Exception e)
115                 {
116                     writeln("Cannot find pgator process!");
117                     return 1;
118                 }
119             }
120         }
121         
122         auto client = new RpcClient!(
123         	SimpleTestCase, 
124         	NullTestCase,
125         	TimestampCase,
126         	NumericTestCase,
127         	UnicodeTestCase,
128         	MulticommandCase,
129         	NoticeTestCase,
130         	SingleQueryTestCase,
131         	OneRowTestCase,
132         	ArrayTestCase,
133         	LongQueryTestCase,
134 		AuthInfoTestCase,
135         	)(host, connString, tableName, pid);
136         scope(exit) client.finalize;
137         
138         client.runTests();
139         
140         return 0;
141     }
142 }
143 else
144 {
145     import std.stdio;
146     import std.typecons;
147     import std.concurrency;
148     import std.process;
149     import std.string;
150 	import core.time;
151 	import server.server;
152 	import server.options;
153 	import server.config;
154 	
155 	import daemon;
156 	import terminal;
157 	import dlogg.strict;
158 	import util;
159 	
160 	immutable struct LoadedConfig
161     {
162         AppConfig config;
163         Options options;
164     }
165     
166 	LoadedConfig loadConfig(immutable Options options)
167 	{
168         if(options.configName != "")
169         {
170             return LoadedConfig(immutable AppConfig(options.configName), options);
171         }
172         else
173         {
174             auto res = tryConfigPaths(options.configPaths); 
175             return LoadedConfig(res.config, options.updateConfigPath(res.path));
176         }
177 	}
178 	
179 	/**
180 	*  Converts group and user names to corresponding gid and uid. If the $(B groupName) or
181 	*  $(B userName) are already a ints, simply converts them and returns.
182 	*
183 	*  Retrieving of user id is performed by 'id -u %s' and group id by 'getent group %s | cut -d: -f3'.
184 	*/
185 	Tuple!(int, int) resolveRootLowing(shared ILogger logger, string groupName, string userName)
186 	{
187 	    int tryConvert(string what)(string s, string command)
188 	    {
189 	        enum warningMsg = "Failed to retrieve " ~ what ~ " id for root lowing: ";
190 	        int handleFail(lazy string msg)
191 	        {
192 	            logger.logWarning(text(warningMsg, msg));
193                 return -1;
194 	        }
195 	        
196 	        try return s.to!int;
197 	        catch(ConvException e)
198 	        {
199 	            try
200 	            {
201 	                auto res = executeShell(command.format(s));
202 	                if(res.status != 0) return handleFail(res.output);
203 	                
204 	                try return res.output.parse!int;
205 	                catch(ConvException e) return handleFail(e.msg);
206                 } 
207 	            catch(ProcessException e) return handleFail(e.msg);
208 	            catch(StdioException e) return handleFail(e.msg);
209 	        }
210 	    }
211 	    
212 	    return tuple(tryConvert!"group"(groupName, "getent group %s | cut -d: -f3")
213 	               , tryConvert!"user"(userName, "id -u %s"));
214 	}
215 	
216 	int main(string[] args)
217 	{	
218 		auto options = new immutable Options(args);
219 		
220 		if (options.help)
221 		{
222 			writeln(options.helpMsg);
223 			return 0;
224 		}
225 		if (options.showVersion)
226 		{
227 		    writeln(options.versionMsg);
228 		    return 0;
229 		}
230 		
231 		if (options.genConfigPath != "")
232 		{
233 			genConfig(options.genConfigPath);
234 			return 0;
235 		}
236 		
237 		try
238 		{
239 		    auto loadedConfig = loadConfig(options);
240             auto logger = new shared StrictLogger(loadedConfig.config.logname, StrictLogger.Mode.Append);
241             auto app = new shared Application(logger, loadedConfig.options, loadedConfig.config);
242             
243             enum mainFunc = (string[] args)
244             {
245                 int res;
246                 do
247                 {
248                     res = app.run;
249                 } while(receiveTimeout(dur!"msecs"(1000), 
250                         // bug, should be fixed in 2.067
251                         //  (shared(Application) newApp) {app = newApp;}
252                         (Variant v) 
253                         {
254                             auto newAppPtr = v.peek!(shared(Application)); assert(newAppPtr);
255                             app = *newAppPtr;
256                         }));
257                 
258                 logger.logDebug("Exiting main");
259                 return res;
260             };
261             
262             enum termFunc = ()
263             {
264                 auto newApp = app.restart;
265                 send(thisTid, newApp);
266             };
267 
268             int groupid, userid;
269             tie!(groupid, userid) = resolveRootLowing(logger, loadedConfig.config.groupid, loadedConfig.config.userid);
270             
271             if(options.daemon) 
272                 return runDaemon(logger, mainFunc, args, termFunc
273                     , (){app.finalize;}, () {app.logger.reload;}
274                     , options.pidFile, options.lockFile
275                     , groupid, userid);
276             else 
277                 return runTerminal(logger, mainFunc, args, termFunc
278                     , (){app.finalize;}, () {app.logger.reload;}
279                     , groupid, userid);
280 	    }
281 	    catch(InvalidConfig e)
282         {
283             writeln("Configuration file at '", e.confPath, "' is invalid! ", e.msg);
284             return 1;
285         }
286         catch(NoConfigLoaded e)
287         {
288             writeln(e.msg);
289             return 1;
290         }
291         catch(Exception e)
292         {
293             writeln("Failed to load configuration file at '", options.configName, "'! Details: ", e.msg);
294             return 1;
295         }
296 	}
297 }