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