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, ®isterLogger); 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