1 /**
2  * The module implements application skeleton
3  *
4  * Copyright: (c) 2015-2017, Milofon Project.
5  * License: Subject to the terms of the BSD license, as written in the included LICENSE.txt file.
6  * Authors: Maksim Galanin
7  */
8 module dango.system.application;
9 
10 public
11 {
12     import uniconf.core : UniConf;
13 
14     import dango.system.exception;
15     import dango.system.logging;
16     import dango.system.plugin : SemVer;
17 }
18 
19 private
20 {
21     import std.format : fmt = format;
22     import std.functional : toDelegate;
23 
24     import vibe.core.log : logDiagnostic, registerLogger;
25     import vibe.core.path : NativePath;
26     import vibe.core.file : existsFile, readFileUTF8, writeFile;
27     import vibe.core.core : lowerPrivileges, runEventLoop;
28 
29     import commandr : Option, Command;
30     import uniconf.core : loadConfig;
31 
32     import dango.inject : DependencyContainer, existingInstance, Inject,
33             registerDependencyContext;
34     import dango.system.logging.core : configureLogging;
35     import dango.system.plugin;
36 }
37 
38 
39 /**
40  * Интерфейс приложения
41  */
42 interface Application
43 {
44     /**
45      * Свойство возвращает наименование приложения
46      */
47     string name() const pure @safe nothrow;
48 
49     /**
50      * Свойство возвращает версию приложения
51      */
52     SemVer release() const pure @safe nothrow;
53 
54     /**
55      * Функция загружает свойства из файла при помощи локального загрузчика
56      * Params:
57      *
58      * filePath = Путь до файла
59      *
60      * Returns: Объект свойств
61      */
62     UniConf loadConfigFile(string filePath) @safe;
63 
64     /**
65      * Возвращает глобальный объект настроек приложения
66      */
67     const(UniConf) getConfig() const pure nothrow @safe;
68 
69     /**
70      * Возвращает глобальный контейнер зависимостей
71      */
72     DependencyContainer getContainer() @safe nothrow;
73 }
74 
75 
76 /**
77  * Делегат инициализации зависимостей
78  */
79 alias DependencyBootstrap = void delegate(DependencyContainer cont, UniConf config) @safe;
80 alias DependencyBootstrapFn = void function(DependencyContainer cont, UniConf config) @safe;
81 
82 /**
83  * Делегат инициализации плагинов
84  */
85 alias PluginBootstrap = void delegate(PluginManager manager) @safe;
86 alias PluginBootstrapFn = void function(PluginManager manager) @safe;
87 
88 /**
89  * Делегат инициализации приложения
90  */
91 alias ApplicationBootstrap = void delegate(DependencyContainer cont, UniConf config) @safe;
92 alias ApplicationBootstrapFn = void function(DependencyContainer cont, UniConf config) @safe;
93 
94 
95 /**
96  * Реализация приложения
97  */
98 class DangoApplication : Application, PluginContainer!ConsolePlugin
99 {
100     private @safe
101     {
102         string _applicationName;
103         string _applicationSummary;
104         SemVer _applicationVersion;
105 
106         string[] _defaultConfigs;
107         UniConf _applicationConfig;
108         ConsolePlugin[] _plugins;
109 
110         DependencyContainer _container;
111         PluginManager _pluginManager;
112         DependencyBootstrap[] _dependencyBootstraps;
113         ApplicationBootstrap[] _applicationBootstraps;
114         PluginBootstrap[] _pluginBootstraps;
115     }
116 
117     /**
118      * Main application constructor
119      */
120     this(string name, string _version, string summary) @safe
121     {
122         this(name, SemVer(_version), summary);
123     }
124 
125     /**
126      * Main application constructor
127      */
128     this(string name, SemVer _version, string summary) @safe
129     {
130         this._applicationVersion = _version;
131         this._applicationSummary = summary;
132         this._applicationName = name;
133         this._container = new DependencyContainer();
134         this._pluginManager = new PluginManager(_container);
135     }
136 
137     /**
138      * Свойство возвращает наименование приложения
139      */
140     string name() const pure nothrow @safe
141     {
142         return _applicationName;
143     }
144 
145     /**
146      * Свойство возвращает версию приложения
147      */
148     SemVer release() const pure nothrow @safe
149     {
150         return _applicationVersion;
151     }
152 
153     /**
154      * Функция загружает свойства из файла при помощи локального загрузчика
155      * Params:
156      *
157      * filePath = Путь до файла
158      *
159      * Returns: Объект свойств
160      */
161     UniConf loadConfigFile(string filePath) @safe
162     {
163         if (existsFile(filePath))
164             return loadConfig(filePath);
165         else
166             throw new DangoApplicationException(
167                     fmt!"Config file '%s' not found"(filePath));
168     }
169 
170     /**
171      * Возвращает глобальный объект настроек приложения
172      */
173     const(UniConf) getConfig() const pure nothrow @safe
174     {
175         return _applicationConfig;
176     }
177 
178     /**
179      * Возвращает глобальный контейнер зависимостей
180      */
181     DependencyContainer getContainer() @safe nothrow
182     {
183         return _container;
184     }
185 
186     /**
187      * Добавить путь до файла конфигурации
188      * Params:
189      *
190      * filePath = Путь до файла
191      */
192     void addDefaultConfigFile(string filePath) @safe nothrow
193     {
194         _defaultConfigs ~= filePath;
195     }
196 
197     /**
198      * Регистрация плагина
199      * Params:
200      * plugin = Плагин для регистрации
201      */
202     void collectPlugin(ConsolePlugin plugin) @safe nothrow
203     {
204         _plugins ~= plugin;
205     }
206 
207     /**
208      * Добавить инициализатор зависимостей
209      */
210     void addDependencyBootstrap(DependencyBootstrap bst) @safe nothrow
211     {
212         _dependencyBootstraps ~= bst;
213     }
214 
215     /**
216      * Добавить инициализатор зависимостей
217      */
218     void addDependencyBootstrap(DependencyBootstrapFn bst) nothrow
219     {
220         _dependencyBootstraps ~= toDelegate(bst);
221     }
222 
223     /**
224      * Добавить инициализатор плагинов
225      */
226     void addPluginBootstrap(PluginBootstrap bst) @safe nothrow
227     {
228         _pluginBootstraps ~= bst;
229     }
230 
231     /**
232      * Добавить инициализатор плагинов
233      */
234     void addPluginBootstrap(PluginBootstrapFn bst) nothrow
235     {
236         _pluginBootstraps ~= toDelegate(bst);
237     }
238 
239     /**
240      * Добавить инициализатор приложения
241      */
242     void addApplicationBootstrap(ApplicationBootstrap bst) @safe nothrow
243     {
244         _applicationBootstraps ~= bst;
245     }
246 
247     /**
248      * Добавить инициализатор приложения
249      */
250     void addApplicationBootstrap(ApplicationBootstrapFn bst) nothrow
251     {
252         _applicationBootstraps ~= toDelegate(bst);
253     }
254 
255     /**
256      * Запуск приложения
257      *
258      * Params:
259      * args = Входящие параметры
260      *
261      * Returns: Код завершения работы приложения
262      */
263     int run()(string[] args) @trusted
264     {
265         import commandr : parse;
266 
267         initializationApplicationConfig(args);
268         initializeDependencies(_container, _applicationConfig);
269 
270         configureLogging(_container, _applicationConfig, &registerLogger);
271 
272         logInfo("Start application %s (%s)", _applicationName, _applicationVersion);
273 
274         _pluginManager.registerPluginContainer(this);
275         foreach (bootstrap; _pluginBootstraps)
276             bootstrap(_pluginManager);
277         _pluginManager.initializePlugins();
278 
279         auto prog = new Program(_applicationName)
280                 .version_(_applicationVersion.toString)
281                 .summary(_applicationSummary);
282 
283         this.registerCommand(prog);
284         foreach (ConsolePlugin plug; _plugins)
285             plug.registerCommand(prog);
286 
287         auto progArgs = prog.parse(args);
288 
289         if (this.runCommand(progArgs))
290             return 0;
291 
292         foreach (bootstrap; _applicationBootstraps)
293             bootstrap(_container, _applicationConfig);
294 
295         foreach (ConsolePlugin plug; _plugins)
296         {
297             if (auto ret = plug.runCommand(progArgs))
298                 return ret;
299         }
300 
301         return 0;
302     }
303 
304 
305 private:
306 
307 
308     /**
309      * Регистрация обработчика команды dango
310      */
311     void registerCommand(Program prog) @trusted
312     {
313         auto dangoCommand = new Command("dango", "Dango utilities");
314         dangoCommand.add(new Option(null, "saver", "Save application version to file").required());
315         prog.add(dangoCommand);
316         prog.defaultCommand(dangoCommand.name);
317     }
318 
319     /**
320      * Получение параметров командной строки
321      */
322     int runCommand(ProgramArgs progArgs) @trusted
323     {
324         int ret = 0;
325         progArgs.on("dango", (cmdArgs) {
326                 auto versionFile = cmdArgs.option("saver");
327                 if (versionFile !is null && versionFile.length)
328                     writeFile(NativePath(versionFile), cast(ubyte[])release.toString);
329                 ret = 1;
330             });
331         return ret;
332     }
333 
334 
335     void initializeDependencies(DependencyContainer container, UniConf config) @safe
336     {
337         container.register!(Application, typeof(this)).existingInstance(this);
338         container.registerDependencyContext!LoggingContext();
339         foreach (bootstrap; _dependencyBootstraps)
340             bootstrap(container, config);
341     }
342 
343 
344     void initializationApplicationConfig()(ref string[] args) @trusted
345     {
346         import std.getopt : getopt, arraySep, gconfig = config;
347 
348         initializationConfigSystem();
349 
350         arraySep = ",";
351         string[] configFiles;
352         auto helpInformation = getopt(args,
353                 gconfig.passThrough,
354                 "c|config", &configFiles);
355 
356         if (helpInformation.helpWanted)
357             args ~= "-h";
358 
359         if (!configFiles.length)
360             configFiles = _defaultConfigs;
361 
362         foreach (string cFile; configFiles)
363         {
364             auto config = loadConfigFile(cFile);
365             _applicationConfig = _applicationConfig ~ config;
366         }
367     }
368 
369 
370     void initializationConfigSystem()() @trusted
371     {
372         import uniconf.core : registerConfigLoader, setConfigReader;
373 
374         setConfigReader((string path) {
375                 return readFileUTF8(path);
376             });
377 
378         registerConfigLoader([".json"], (string data) {
379                 import uniconf.json : parseJson;
380                 return parseJson!UniConf(data);
381             });
382 
383         version (Have_uniconf_yaml)
384             registerConfigLoader([".yaml", ".yml"], (string data) {
385                     import uniconf.yaml : parseYaml;
386                     return parseYaml!UniConf(data);
387                 });
388 
389         version (Have_uniconf_sdlang)
390             registerConfigLoader([".sdl"], (string data) {
391                     import uniconf.sdlang : parseSDLang;
392                     return parseSDLang!UniConf(data);
393                 });
394     }
395 }
396 
397 
398 /**
399  * Реализация плагина для запуска приложения в фоне
400  */
401 class DaemonApplicationPlugin : PluginContainer!DaemonPlugin, ConsolePlugin
402 {
403     private @safe
404     {
405         DaemonPlugin[] _plugins;
406     }
407 
408     /**
409      * Свойство возвращает наименование плагина
410      */
411     string name() pure @safe nothrow
412     {
413         return "Daemon";
414     }
415 
416     /**
417      * Свойство возвращает описание плагина
418      */
419     string summary() pure nothrow @safe
420     {
421         return "Daemon application";
422     }
423 
424     /**
425      * Свойство возвращает версию плагина
426      */
427     SemVer release() pure nothrow @safe
428     {
429         return SemVer(0, 0, 1);
430     }
431 
432     /**
433      * Регистрация плагина
434      * Params:
435      * plugin = Плагин для регистрации
436      */
437     void collectPlugin(DaemonPlugin plug) @safe nothrow
438     {
439         _plugins ~= plug;
440     }
441 
442     /**
443      * Register command
444      */
445     void registerCommand(Program prog) @safe
446     {
447         auto cmd = prog.getCommandOrCreate("start", summary, release.toString);
448         cmd.add(new Option("uid", "user", "Sets the user name for privilege lowering."));
449         cmd.add(new Option("gid", "group", "Sets the group name for privilege lowering."));
450     }
451 
452     /**
453      * Run commnad
454      */
455     int runCommand(ProgramArgs args) @trusted
456     {
457         import vibe.core.core : Timer, setTimer;
458         import core.time : seconds;
459 
460         auto cmd = args.command();
461         int ret = 0;
462         if (cmd is null || cmd.name != "start")
463             return ret;
464 
465         foreach (DaemonPlugin dp; _plugins)
466         {
467             ret = dp.startDaemon();
468             if (ret)
469                 return ret;
470         }
471 
472         string uid = cmd.option("user");
473         string gid = cmd.option("group");
474         lowerPrivileges(uid, gid);
475 
476         void emptyTimer() {}
477         auto timer = setTimer(1.seconds, &emptyTimer, true);
478 
479         logDiagnostic("Running event loop...");
480         ret = runEventLoop();
481         logDiagnostic("Event loop exited with status %d.", ret);
482 
483         foreach (DaemonPlugin dp; _plugins)
484         {
485             ret = dp.stopDaemon(ret);
486             if (ret)
487                 return ret;
488         }
489 
490         return ret;
491     }
492 }
493 
494 
495 /**
496  * Возвращает команду или создает новую
497  */
498 Command getCommandOrCreate(Command prog, string name, string summary, string ver) @safe
499 {
500     if (auto cmd = name in prog.commands)
501         return *cmd;
502     else
503     {
504         auto cmd = new Command(name, summary, ver);
505         prog.add(cmd);
506         return cmd;
507     }
508 }
509