diff --git a/package-lock.json b/package-lock.json index a2dcd74..dd7f9cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -144,6 +144,23 @@ "@pushrocks/smartpromise": "^3.0.2" } }, + "@pushrocks/smartenv": { + "version": "4.0.7", + "resolved": "https://verdaccio.lossless.one/@pushrocks%2fsmartenv/-/smartenv-4.0.7.tgz", + "integrity": "sha512-ThSJMEcUAF1VIa4vAvn1zGnziEuDY4h8T+EQ5JCiKy2qnUM/F6zqzFogyo495X6rg71gC/7RmktQVQaN06gjWA==", + "requires": { + "@pushrocks/smartparam": "^1.0.4", + "@pushrocks/smartpromise": "^3.0.2", + "@types/node": "^12.7.2" + }, + "dependencies": { + "@types/node": { + "version": "12.7.3", + "resolved": "https://verdaccio.lossless.one/@types%2fnode/-/node-12.7.3.tgz", + "integrity": "sha512-3SiLAIBkDWDg6vFo0+5YJyHPWU9uwu40Qe+v+0MH8wRKYBimHvvAOyk3EzMrD/TrIlLYfXrqDqrg913PynrMJQ==" + } + } + }, "@pushrocks/smartevent": { "version": "2.0.3", "resolved": "https://verdaccio.lossless.one/@pushrocks%2fsmartevent/-/smartevent-2.0.3.tgz", @@ -251,7 +268,6 @@ "version": "1.0.4", "resolved": "https://verdaccio.lossless.one/@pushrocks%2fsmartparam/-/smartparam-1.0.4.tgz", "integrity": "sha512-UAqhnTQGBaJRptTK3qTd47Yt2ZTrAWByteow4auSNZD+k0xrpY9mTPPgKh0IwsURe0cZhj7zYNpGiekhKkL4rA==", - "dev": true, "requires": { "@pushrocks/smartpromise": "^2.0.5", "is-promise": "^2.1.0", @@ -261,8 +277,7 @@ "@pushrocks/smartpromise": { "version": "2.0.5", "resolved": "https://verdaccio.lossless.one/@pushrocks%2fsmartpromise/-/smartpromise-2.0.5.tgz", - "integrity": "sha512-9j/chLtIiNkR0MDw7Mpxg9slxAVvAQwUZuiaPYX5KpHdKxQaHLI1VZ8IN0vPhwlfgNO4i4vGXV0wB8BvSDj03g==", - "dev": true + "integrity": "sha512-9j/chLtIiNkR0MDw7Mpxg9slxAVvAQwUZuiaPYX5KpHdKxQaHLI1VZ8IN0vPhwlfgNO4i4vGXV0wB8BvSDj03g==" } } }, @@ -318,6 +333,32 @@ "which": "^1.3.1" } }, + "@pushrocks/smartsystem": { + "version": "2.0.8", + "resolved": "https://verdaccio.lossless.one/@pushrocks%2fsmartsystem/-/smartsystem-2.0.8.tgz", + "integrity": "sha512-lBruo1Ikai35oiDMy3Lx1NhddTMynCCd8MTx4dBU22a0HnNYZRKxTITuPAfIDD3VB94ZpEGbAvzblnZpuFFkMQ==", + "requires": { + "@pushrocks/lik": "^3.0.10", + "@pushrocks/smartenv": "^4.0.7", + "@pushrocks/smartpromise": "^3.0.2" + }, + "dependencies": { + "@pushrocks/lik": { + "version": "3.0.11", + "resolved": "https://verdaccio.lossless.one/@pushrocks%2flik/-/lik-3.0.11.tgz", + "integrity": "sha512-SDKRPj9+xBTqozlDPcA7O6BcccM1Tw/sXPVP+OnhNxCubDZ/L2kGNpPpqm43NJUoNxSSo5wdBw4N7MAFYCGdVg==", + "requires": { + "@pushrocks/smartdelay": "^2.0.3", + "@pushrocks/smartpromise": "^3.0.2", + "@pushrocks/smartrx": "^2.0.3", + "@pushrocks/smarttime": "^3.0.12", + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.4", + "symbol-tree": "^3.2.4" + } + } + } + }, "@pushrocks/smarttime": { "version": "3.0.12", "resolved": "https://verdaccio.lossless.one/@pushrocks%2fsmarttime/-/smarttime-3.0.12.tgz", @@ -898,8 +939,7 @@ "is-promise": { "version": "2.1.0", "resolved": "https://verdaccio.lossless.one/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, "is-stream": { "version": "1.1.0", diff --git a/package.json b/package.json index 9ea638c..9c4a863 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@pushrocks/smartlog": "^2.0.19", "@pushrocks/smartlog-destination-local": "^8.0.2", "@pushrocks/smartshell": "^2.0.25", + "@pushrocks/smartsystem": "^2.0.8", "@types/fs-extra": "^8.0.0", "fs-extra": "^8.1.0" }, diff --git a/ts/smartdaemon.classes.smartdaemon.ts b/ts/smartdaemon.classes.smartdaemon.ts new file mode 100644 index 0000000..00b01a2 --- /dev/null +++ b/ts/smartdaemon.classes.smartdaemon.ts @@ -0,0 +1,60 @@ +import * as plugins from './smartdaemon.plugins'; +import { SmartDaemonTemplateManager } from './smartdaemon.classes.templatemanager'; + +const smartDaemonNamespace = 'smartdaemon_'; + +interface SmartDaemonConstructorOptions { + name: string; + scriptPath: string; + workingDir: string; +} + +export class SmartDaemon { + private templateManager: SmartDaemonTemplateManager; + private smartshellInstance: plugins.smartshell.Smartshell; + private smartsystem: plugins.smartsystem.Smartsystem; + private shouldExecute: boolean = false; + + constructor() { + this.templateManager = new SmartDaemonTemplateManager; + this.smartshellInstance = new plugins.smartshell.Smartshell({ + executor: 'bash' + }); + this.smartsystem = new plugins.smartsystem.Smartsystem(); + } + + public async checkElegibility () { + if (await this.smartsystem.env.isLinuxAsync()) { + this.shouldExecute = true; + } else { + console.log('Smartdaemon can only be used on Linuc systems! Refusing to set up a service.'); + this.shouldExecute = false; + } + return this.shouldExecute; + } + + /** + * enables the service + */ + public async enable() { + + }; + + /** + * disables the service + */ + public async disable() { + + }; + + /** + * pauses the service + */ + public pause() {}; + + + private async execute(commandArg: string) { + + this.smartshellInstance.exec(commandArg); + } +} \ No newline at end of file diff --git a/ts/smartdaemon.classes.templatemanager.ts b/ts/smartdaemon.classes.templatemanager.ts new file mode 100644 index 0000000..f185645 --- /dev/null +++ b/ts/smartdaemon.classes.templatemanager.ts @@ -0,0 +1,34 @@ +import * as plugins from './smartdaemon.plugins'; + +export class SmartDaemonTemplateManager { + public generateServiceTemplate = (optionsArg: { + serviceName: string; + description: string; + serviceVersion: string; + pathNodeJs: string; + pathWorkkingDir; + pathJsFileToRun; + }) => { + return ` +# servicVersion: ${optionsArg.serviceVersion} +[Unit] +Description=${optionsArg.description} +Requires=network.target +After=network.target + +[Service] +Type=simple +ExecStart=${optionsArg.pathNodeJs} ${optionsArg.pathJsFileToRun} +WorkingDirectory=${optionsArg.pathWorkkingDir} +Restart=on-failure +LimitNOFILE=infinity +LimitCORE=infinity +StandardInput=null +StandardOutput=syslog +StandardError=syslog +Restart=always +[Install] +WantedBy=multi-user.target +`; + }; +} diff --git a/ts/smartdaemon.paths.ts b/ts/smartdaemon.paths.ts new file mode 100644 index 0000000..22c0f16 --- /dev/null +++ b/ts/smartdaemon.paths.ts @@ -0,0 +1,4 @@ +import * as plugins from './smartdaemon.plugins'; + +export const packageDir = plugins.path.join(__dirname, '../'); +export const systemdDir = plugins.path.join('/lib/systemd/system/'); diff --git a/ts/smartdaemon.plugins.ts b/ts/smartdaemon.plugins.ts index 1ec89ec..72f2ba5 100644 --- a/ts/smartdaemon.plugins.ts +++ b/ts/smartdaemon.plugins.ts @@ -7,14 +7,16 @@ export { }; // @pushrocks scope -import * as smartshell from '@pushrocks/smartshell'; import * as smartlog from '@pushrocks/smartlog'; import * as smartlogDestinationLocal from '@pushrocks/smartlog-destination-local'; +import * as smartshell from '@pushrocks/smartshell'; +import * as smartsystem from '@pushrocks/smartsystem'; export { - smartshell, smartlog, - smartlogDestinationLocal + smartlogDestinationLocal, + smartshell, + smartsystem }; // third party diff --git a/ts/smartdaemon.settings.ts b/ts/smartdaemon.settings.ts deleted file mode 100644 index b83b13a..0000000 --- a/ts/smartdaemon.settings.ts +++ /dev/null @@ -1,174 +0,0 @@ -export const settingsReference = { - name: { - cli: { - short: 'n', - long: 'service', - value: '[name]', - description: 'Service name' - }, - mandatory: true - }, - description: { - cli: { - short: 'd', - long: 'description', - value: '[description]', - description: 'Service description' - }, - default: '{name} service' - }, - author: { - cli: { - short: 'k', - long: 'author', - value: '[author]', - description: 'Service author' - } - }, - user: { - cli: { - short: 'u', - long: 'user', - value: '[user]', - description: 'User to run service as' - } - }, - group: { - cli: { - short: 'g', - long: 'group', - value: '[group]', - description: 'Group to run service as' - } - }, - app: { - cli: { - short: 'A', - long: 'app', - value: '[app]', - description: 'Application main file' - }, - default: 'main.js' - - }, - cwd: { - cli: { - short: 'c', - long: 'cwd', - value: '[path]', - description: 'Application cwd' - }, - mandatory: true, - fs: true - }, - 'app.args': { - cli: { - short: 'p', - long: 'app.args', - value: '[args]', - description: 'Application arguments' - } - }, - env: { - cli: { - short: 'e', - long: 'env', - value: '[NAME=VALUE]', - description: 'Environment variables to set in systemd job', - function: (v, vars) => { - const _v = v.split('=') - vars[_v[0]] = _v[1]; return vars - }, - store: [] - } - }, - pid: { - cli: { - short: 'P', - long: 'pid', - value: '[file]', - description: 'Service pid file' - }, - default: '/var/run/{name}.pid' - }, - log: { - cli: { - short: 'L', - long: 'log', - value: '[log]', - description: 'Service log file' - }, - default: '/var/log/{name}/log' - }, - error: { - cli: { - short: 'E', - long: 'error', - value: '[error]', - description: 'Service error file' - }, - default: '/var/log/{name}/error' - }, - engine: { - cli: { - short: 'X', - long: 'engine', - value: '[node|forever|pm2]', - description: 'Service engine (node|forever|pm2)' - }, - default: 'node', - mandatory: true - }, - 'engine.bin': { - cli: { - short: 'b', - long: 'engine.bin', - value: '[bin]', - description: 'Service engine bin' - }, - default: '/usr/bin/{engine}', - fs: true - }, - 'engine.args': { - cli: { - short: 't', - long: 'engine.args', - value: '[eargs]', - description: 'Service engine args' - } - }, - logrotate: { - cli: { - short: 'l', - long: 'logrotate', - description: 'Add logrotate config' - } - // default false - }, - 'logrotate.rotate': { - cli: { - short: 'R', - long: 'logrotate.rotate', - value: '[rotate]', - description: 'Logrotate rotations' - }, - default: 10 - - }, - 'logrotate.frequency': { - cli: { - short: 'F', - long: 'logrotate.frequency', - value: '[frequency]', - description: 'Logrotate frequency' - }, - default: 'daily' - }, - noroot: { - cli: { - short: 'w', - long: 'noroot', - description: 'Skip check for root permission' - } - } -}; diff --git a/ts/smartdaemon.setup.ts b/ts/smartdaemon.setup.ts deleted file mode 100644 index 6e003c3..0000000 --- a/ts/smartdaemon.setup.ts +++ /dev/null @@ -1,259 +0,0 @@ -import * as plugins from './smartdaemon.plugins'; - -import { logger } from './smartdamon.logging'; -import { settingsReference } from './smartdaemon.settings'; -import { templateReference } from './smartdaemon.templates'; - -const smartshellInstance = new plugins.smartshell.Smartshell({ - executor: 'bash' -}); - -/** - * print success message - * @method setup.success - * @param {string} message - */ -export const setupSuccess = (message: string) => { - logger.log('success', `service-systemd: ${message}`); -}; - -/** - * print error message - * @method setup.fail - * @param {string} message - */ -export const setupFail = (message) => { - logger.log('error', `service-systemd ${message}`); -}; - -/** - * install the service - * can also be used to update the service - * @method setup.add - * @param {object} settings - * @return {string} - */ -export const setupAdd = async (settings) => { - setupCheckSettings(settings); - const contents = setupParse(settings); - await setupCheckPaths(settings); - await setupAddLog(settings); - await setupAddScripts(settings, contents); - await setupAddLogrotate(settings, contents); - return `service ${settings.name} installed`; -}; - -/** - * remove the service - * @method setup.remove - * @param {string} service service name - * @return {string} - */ -export const setupRemove = async (service) => { - if (!service) { - throw new Error('Missing argument: service name'); - } - - const cmd = `systemctl disable ${service}.service`; - logger.log('info', `service-systemd > ${cmd}`); - await smartshellInstance.exec(cmd); - - let file = plugins.path.join('/etc/systemd/system', `${service}.service`); - logger.log('info', `service-systemd remove ${file}`); - await plugins.fs.unlink(file); - - file = plugins.path.join('/usr/local/bin', `systemd-${service}-start`); - logger.log('info', `service-systemd remove ${file}`); - await plugins.fs.unlink(file); - - file = plugins.path.join('/etc/logrotate.d', service); - logger.log('info', `service-systemd remove ${file}`); - await plugins.fs.unlink(file); - return `service ${service} uninstalled`; -}; - -/** - * check mandatories params and paths - * @method setup.checkSettings - * @param {object} settings - */ -export const setupCheckSettings = (settings) => { - if (!settings.name) { - settings.name = settings.service; - } - - delete settings.service; - - const paths = []; - for (const optionArg of Object.keys(settingsReference)) { - const option = settingsReference[optionArg]; - if (option.mandatory && !settings[optionArg]) { - throw new Error(`Missing ${optionArg} in settings file or arguments`); - } - if (option.fs) { - settings[optionArg] = plugins.path.resolve(settings[optionArg]); - } - } -}; - -/** - * check mandatories params and paths - * @method setup.checkPaths - * @param {object} settings - */ -export const setupCheckPaths = async (settings) => { - const exists = []; - for (const optionArg of Object.keys(settingsReference)) { - const option = settingsReference[optionArg]; - if (option.fs) { - exists.push((async () => { - const exists2 = await plugins.fs.pathExists(settings[optionArg]); - if (!exists2) { - throw Error(`path ${settings[optionArg]} (${optionArg}) does not exists`); - } - })()); - } - } - - await Promise.all(exists); -}; - -/** - * merge settings with templates - * create scripts contents - * @method setup.parse - * @param {object} settings - */ -export const setupParse = (settings) => { - - if (settings.env) { - settings.envs = ''; - for (const key of Object.keys(settings.env)) { - settings.envs += 'Environment=' + key + '=' + settings.env[key] + '\n'; - } - } - settings.user = settings.user - ? `User=${settings.user}` - : ''; - settings.group = settings.group - ? `Group=${settings.group}` - : ''; - - settings.date = (new Date()).toString(); - - for (const key in settings) { - if (typeof settings[key] === 'string') { - settings[key] = plugins.string.template(settings[key], settings, true); - } - } - - const _service = plugins.string.template(templateReference.engines[settings.engine].service, settings, true); - const _start = plugins.string.template(templateReference.engines[settings.engine].start, settings, true); - const _stop = plugins.string.template(templateReference.engines[settings.engine].stop, settings, true); - const _logrotate = settings.logrotate ? plugins.string.template(templateReference.logrotate, settings, true) : ''; - - return { - service: _service, - start: _start, - stop: _stop, - logrotate: _logrotate - }; -}; - -/** - * write scripts and run commands to install the service - * @method setup.addScripts - * @param {object} settings - * @param {object} contents scripts contents - * @param {string} contents.start - * @param {string} contents.stop - * @param {string} contents.service - */ -export const setupAddScripts = async (settings, contents) => { - const service = plugins.path.join('/etc/systemd/system', `${settings.name}.service`); - const tasks = []; - - if (contents.start) { - tasks.push(() => { - const start = plugins.path.join('/usr/local/bin', `systemd-${settings.name}-start`); - logger.log('info', `service-systemd: write file ${start}`); - return plugins.fs.writeFile(start, contents.start, 'utf8'); - }); - } - - if (contents.stop) { - tasks.push(() => { - const stop = plugins.path.join('/usr/local/bin', `systemd-${settings.name}-stop`); - logger.log('info', `service-systemd: write file ${stop}`); - return plugins.fs.writeFile(stop, contents.stop, { - encoding: 'utf8' - }); - }); - } - - if (contents.start || contents.stop) { - tasks.push(() => { - const cmd = `chmod a+x /usr/local/bin/systemd-${settings.name}*`; - logger.log('info', `service-systemd > ${cmd}`); - return smartshellInstance.exec(cmd); - }); - } - - tasks.push(() => { - logger.log('info', `service-systemd write file ${service}`); - return plugins.fs.writeFile(service, contents.service, 'utf8'); - }); - - tasks.push(() => { - const cmd = `systemctl enable ${service};systemctl daemon-reload`; - logger.log('info', `service-systemd > ${cmd}`); - return smartshellInstance.exec(cmd); - }); - - for (const task of tasks) { - await task(); - } -}; - -/** - * ensure dirs for log files - * @method setup.addLog - * @param {object} settings - */ -export const setupAddLog = async (settings) => { - const tasks = []; - let dirLog; - let dirError; - - if (settings.log) { - dirLog = plugins.path.dirname(settings.log); - logger.log('info', `service-systemd: ensure dir ${dirLog}`); - tasks.push(plugins.fs.ensureDir(dirLog)); - } - if (settings.error) { - dirError = plugins.path.dirname(settings.error); - if (dirError !== dirLog) { - logger.log('info', `service-systemd ensure dir ${dirError}`); - tasks.push(plugins.fs.ensureDir(dirError)); - } - } - await Promise.all(tasks); -}; - -/** - * write logrotate conf script - * @method setup.addLogrotate - * @param {object} settings - * @param {object} contents scripts contents - * @param {string} contents.start - * @param {string} contents.stop - * @param {string} contents.service - */ -export const setupAddLogrotate = async (settings, contents) => { - if (!settings.logrotate) { - return; - } - const file = plugins.path.join('/etc/logrotate.d/', settings.name); - logger.log('info', `service-systemd: write logrotate file ${file}`); - await plugins.fs.writeFile(file, contents.logrotate, 'utf8'); -}; diff --git a/ts/smartdaemon.templates.ts b/ts/smartdaemon.templates.ts deleted file mode 100644 index 26fb1a3..0000000 --- a/ts/smartdaemon.templates.ts +++ /dev/null @@ -1,75 +0,0 @@ -export const templateReference = { - engines: { - node: { - service: - '# Generated by service-systemd on {{date}}\n' + - '[Unit]\n' + - 'Description={description}\n' + - 'Requires=network.target\n' + - 'After=network.target\n' + - '\n' + - '[Service]\n' + - 'Type=simple\n' + - 'ExecStart={engine.bin} {engine.args} {app} {app.args}\n' + - 'WorkingDirectory={cwd}\n' + - 'Restart=on-failure\n' + - '{envs}\n' + - 'LimitNOFILE=infinity\n' + - 'LimitCORE=infinity\n' + - 'StandardInput=null\n' + - 'StandardOutput=syslog\n' + - 'StandardError=syslog\n' + - 'Restart=always\n' + - 'SyslogIdentifier={name}\n' + - 'PIDFile={pid}\n' + - '{user}\n' + - '{group}\n' + - '[Install]\n' + - 'WantedBy=multi-user.target\n' - }, - forever: { - service: - '# Generated by service-systemd on {date}\n' + - '[Unit]\n' + - 'Description={description}\n' + - 'Requires=network.target\n' + - 'After=network.target\n' + - '\n' + - '[Service]\n' + - 'Type=forking\n' + - 'WorkingDirectory={cwd}\n' + - '{envs}\n' + - 'ExecStart=/usr/local/bin/systemd-{name}-start\n' + - 'ExecStop=/usr/local/bin/systemd-{name}-stop\n' + - 'PIDFile={pid}\n' + - '{user}\n' + - '{group}\n' + - '[Install]\n' + - 'WantedBy=multi-user.target\n', - start: - '#!/bin/bash\n' + - '{engine.bin} start ' + - '--pidFile {pid} ' + - '--uid {name} ' + - '--sourceDir {cwd} ' + - '-l {log} ' + - '-e {error} ' + - '--append --minUptime 5000 --spinSleepTime 2000 ' + - '{engine.args} ' + - '{app} {app.args}\n' + - 'exit 0', - stop: '#!/bin/bash\n' + '{engine.bin} stop {name}\n' + 'exit 0' - } - }, - logrotate: - '{log}\n{error} {\n' + - ' {logrotate.frequency}\n' + - ' rotate {logrotate.rotate}\n' + - ' create\n' + - ' missingok\n' + - ' notifempty\n' + - ' compress\n' + - ' sharedscripts\n' + - // ' postrotate\n{restart}\n' + - ' endscript\n}' -};