UNPKG

13.8 kBJavaScriptView Raw
1const path = require('path');
2const fs = require('fs');
3const os = require('os');
4const spawn = require('child_process').spawn;
5const chalk = require('chalk');
6
7const readline = require('readline')
8const which = require('../../tools/which.js')
9const sexec = require('../../tools/sexec.js')
10const copydirSync = require('../../tools/copydirSync.js')
11const deleteFolderRecursive = require('../../tools/deleteFolderRecursive.js')
12
13var Configuration = require('../../Configuration.js');
14var cst = require('../../../constants.js');
15var Common = require('../../Common');
16var Utility = require('../../Utility.js');
17
18module.exports = {
19 install,
20 uninstall,
21 start,
22 publish,
23 generateSample,
24 localStart,
25 getModuleConf
26}
27
28/**
29 * PM2 Module System.
30 * Features:
31 * - Installed modules are listed separately from user applications
32 * - Always ON, a module is always up along PM2, to stop it, you need to uninstall it
33 * - Install a runnable module from NPM/Github/HTTP (require a package.json only)
34 * - Some modules add internal PM2 depencencies (like typescript, profiling...)
35 * - Internally it uses NPM install (https://docs.npmjs.com/cli/install)
36 * - Auto discover script to launch (first it checks the apps field, then bin and finally main attr)
37 * - Generate sample module via pm2 module:generate <module_name>
38 */
39
40function localStart(PM2, opts, cb) {
41 var proc_path = '',
42 cmd = '',
43 conf = {};
44
45 Common.printOut(cst.PREFIX_MSG_MOD + 'Installing local module in DEVELOPMENT MODE with WATCH auto restart');
46 proc_path = process.cwd();
47
48 cmd = path.join(proc_path, cst.DEFAULT_MODULE_JSON);
49
50 Common.extend(opts, {
51 cmd : cmd,
52 development_mode : true,
53 proc_path : proc_path
54 });
55
56 return StartModule(PM2, opts, function(err, dt) {
57 if (err) return cb(err);
58 Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched');
59 return cb(null, dt);
60 });
61}
62
63function generateSample(app_name, cb) {
64 var rl = readline.createInterface({
65 input: process.stdin,
66 output: process.stdout
67 });
68
69 function samplize(module_name) {
70 var cmd1 = 'git clone https://github.com/pm2-hive/sample-module.git ' + module_name + '; cd ' + module_name + '; rm -rf .git';
71 var cmd2 = 'cd ' + module_name + ' ; sed -i "s:sample-module:'+ module_name +':g" package.json';
72 var cmd3 = 'cd ' + module_name + ' ; npm install';
73
74 Common.printOut(cst.PREFIX_MSG_MOD + 'Getting sample app');
75
76 sexec(cmd1, function(err) {
77 if (err) Common.printError(cst.PREFIX_MSG_MOD_ERR + err.message);
78 sexec(cmd2, function(err) {
79 console.log('');
80 sexec(cmd3, function(err) {
81 console.log('');
82 Common.printOut(cst.PREFIX_MSG_MOD + 'Module sample created in folder: ', path.join(process.cwd(), module_name));
83 console.log('');
84 Common.printOut('Start module in development mode:');
85 Common.printOut('$ cd ' + module_name + '/');
86 Common.printOut('$ pm2 install . ');
87 console.log('');
88
89 Common.printOut('Module Log: ');
90 Common.printOut('$ pm2 logs ' + module_name);
91 console.log('');
92 Common.printOut('Uninstall module: ');
93 Common.printOut('$ pm2 uninstall ' + module_name);
94 console.log('');
95 Common.printOut('Force restart: ');
96 Common.printOut('$ pm2 restart ' + module_name);
97 return cb ? cb() : false;
98 });
99 });
100 });
101 }
102
103 if (app_name) return samplize(app_name);
104
105 rl.question(cst.PREFIX_MSG_MOD + "Module name: ", function(module_name) {
106 samplize(module_name);
107 });
108}
109
110function publish(opts, cb) {
111 var rl = readline.createInterface({
112 input: process.stdin,
113 output: process.stdout
114 });
115
116 var semver = require('semver');
117
118 var package_file = path.join(process.cwd(), 'package.json');
119
120 var package_json = require(package_file);
121
122 package_json.version = semver.inc(package_json.version, 'minor');
123 Common.printOut(cst.PREFIX_MSG_MOD + 'Incrementing module to: %s@%s',
124 package_json.name,
125 package_json.version);
126
127
128 rl.question("Write & Publish? [Y/N]", function(answer) {
129 if (answer != "Y")
130 return cb();
131
132
133 fs.writeFile(package_file, JSON.stringify(package_json, null, 2), function(err, data) {
134 if (err) return cb(err);
135
136 Common.printOut(cst.PREFIX_MSG_MOD + 'Publishing module - %s@%s',
137 package_json.name,
138 package_json.version);
139
140 sexec('npm publish', function(code) {
141 Common.printOut(cst.PREFIX_MSG_MOD + 'Module - %s@%s successfully published',
142 package_json.name,
143 package_json.version);
144
145 Common.printOut(cst.PREFIX_MSG_MOD + 'Pushing module on Git');
146 sexec('git add . ; git commit -m "' + package_json.version + '"; git push origin master', function(code) {
147
148 Common.printOut(cst.PREFIX_MSG_MOD + 'Installable with pm2 install %s', package_json.name);
149 return cb(null, package_json);
150 });
151 });
152 });
153
154 });
155}
156
157function moduleExistInLocalDB(CLI, module_name, cb) {
158 var modules = Configuration.getSync(cst.MODULE_CONF_PREFIX);
159 if (!modules) return cb(false);
160 var module_name_only = Utility.getCanonicModuleName(module_name)
161 modules = Object.keys(modules);
162 return cb(modules.indexOf(module_name_only) > -1 ? true : false);
163};
164
165function install(CLI, module_name, opts, cb) {
166 moduleExistInLocalDB(CLI, module_name, function (exists) {
167 if (exists) {
168 Common.logMod('Module already installed. Updating.');
169
170 Rollback.backup(module_name);
171
172 return uninstall(CLI, module_name, function () {
173 return continueInstall(CLI, module_name, opts, cb);
174 });
175 }
176 return continueInstall(CLI, module_name, opts, cb);
177 })
178}
179
180// Builtin Node Switch
181function getNPMCommandLine(module_name, install_path) {
182 if (which('npm')) {
183 return spawn.bind(this, cst.IS_WINDOWS ? 'npm.cmd' : 'npm', ['install', module_name, '--loglevel=error', '--prefix', `"${install_path}"` ], {
184 stdio : 'inherit',
185 env: process.env,
186 shell : true
187 })
188 }
189 else {
190 return spawn.bind(this, cst.BUILTIN_NODE_PATH, [cst.BUILTIN_NPM_PATH, 'install', module_name, '--loglevel=error', '--prefix', `"${install_path}"`], {
191 stdio : 'inherit',
192 env: process.env,
193 shell : true
194 })
195 }
196}
197
198function continueInstall(CLI, module_name, opts, cb) {
199 Common.printOut(cst.PREFIX_MSG_MOD + 'Calling ' + chalk.bold.red('[NPM]') + ' to install ' + module_name + ' ...');
200
201 var canonic_module_name = Utility.getCanonicModuleName(module_name);
202 var install_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
203
204 require('mkdirp')(install_path)
205 .then(function() {
206 process.chdir(os.homedir());
207
208 var install_instance = getNPMCommandLine(module_name, install_path)();
209
210 install_instance.on('close', finalizeInstall);
211
212 install_instance.on('error', function (err) {
213 console.error(err.stack || err);
214 });
215 });
216
217 function finalizeInstall(code) {
218 if (code != 0) {
219 // If install has failed, revert to previous module version
220 return Rollback.revert(CLI, module_name, function() {
221 return cb(new Error('Installation failed via NPM, module has been restored to prev version'));
222 });
223 }
224
225 Common.printOut(cst.PREFIX_MSG_MOD + 'Module downloaded');
226
227 var proc_path = path.join(install_path, 'node_modules', canonic_module_name);
228 var package_json_path = path.join(proc_path, 'package.json');
229
230 // Append default configuration to module configuration
231 try {
232 var conf = JSON.parse(fs.readFileSync(package_json_path).toString()).config;
233
234 if (conf) {
235 Object.keys(conf).forEach(function(key) {
236 Configuration.setSyncIfNotExist(canonic_module_name + ':' + key, conf[key]);
237 });
238 }
239 } catch(e) {
240 Common.printError(e);
241 }
242
243 opts = Common.extend(opts, {
244 cmd : package_json_path,
245 development_mode : false,
246 proc_path : proc_path
247 });
248
249 Configuration.set(cst.MODULE_CONF_PREFIX + ':' + canonic_module_name, {
250 uid : opts.uid,
251 gid : opts.gid
252 }, function(err, data) {
253 if (err) return cb(err);
254
255 StartModule(CLI, opts, function(err, dt) {
256 if (err) return cb(err);
257
258 if (process.env.PM2_PROGRAMMATIC === 'true')
259 return cb(null, dt);
260
261 CLI.conf(canonic_module_name, function() {
262 Common.printOut(cst.PREFIX_MSG_MOD + 'Module successfully installed and launched');
263 Common.printOut(cst.PREFIX_MSG_MOD + 'Checkout module options: `$ pm2 conf`');
264 return cb(null, dt);
265 });
266 });
267 });
268 }
269}
270
271function start(PM2, modules, module_name, cb) {
272 Common.printOut(cst.PREFIX_MSG_MOD + 'Starting NPM module ' + module_name);
273
274 var install_path = path.join(cst.DEFAULT_MODULE_PATH, module_name);
275 var proc_path = path.join(install_path, 'node_modules', module_name);
276 var package_json_path = path.join(proc_path, 'package.json');
277
278 var opts = {};
279
280 // Merge with embedded configuration inside module_conf (uid, gid)
281 Common.extend(opts, modules[module_name]);
282
283 // Merge meta data to start module properly
284 Common.extend(opts, {
285 // package.json path
286 cmd : package_json_path,
287 // starting mode
288 development_mode : false,
289 // process cwd
290 proc_path : proc_path
291 });
292
293 StartModule(PM2, opts, function(err, dt) {
294 if (err) console.error(err);
295 return cb();
296 })
297}
298
299function uninstall(CLI, module_name, cb) {
300 var module_name_only = Utility.getCanonicModuleName(module_name)
301 var proc_path = path.join(cst.DEFAULT_MODULE_PATH, module_name_only);
302 Configuration.unsetSync(cst.MODULE_CONF_PREFIX + ':' + module_name_only);
303
304 CLI.deleteModule(module_name_only, function(err, data) {
305 console.log('Deleting', proc_path)
306 if (module_name != '.' && proc_path.includes('modules') === true) {
307 deleteFolderRecursive(proc_path)
308 }
309
310 if (err) {
311 Common.printError(err);
312 return cb(err);
313 }
314
315 return cb(null, data);
316 });
317}
318
319function getModuleConf(app_name) {
320 if (!app_name) throw new Error('No app_name defined');
321
322 var module_conf = Configuration.getAllSync();
323
324 var additional_env = {};
325
326 if (!module_conf[app_name]) {
327 additional_env = {};
328 additional_env[app_name] = {};
329 }
330 else {
331 additional_env = Common.clone(module_conf[app_name]);
332 additional_env[app_name] = JSON.stringify(module_conf[app_name]);
333 }
334 return additional_env;
335}
336
337function StartModule(CLI, opts, cb) {
338 if (!opts.cmd && !opts.package) throw new Error('module package.json not defined');
339 if (!opts.development_mode) opts.development_mode = false;
340
341 var package_json = require(opts.cmd || opts.package);
342
343 /**
344 * Script file detection
345 * 1- *apps* field (default pm2 json configuration)
346 * 2- *bin* field
347 * 3- *main* field
348 */
349 if (!package_json.apps && !package_json.pm2) {
350 package_json.apps = {};
351
352 if (package_json.bin) {
353 var bin = Object.keys(package_json.bin)[0];
354 package_json.apps.script = package_json.bin[bin];
355 }
356 else if (package_json.main) {
357 package_json.apps.script = package_json.main;
358 }
359 }
360
361 Common.extend(opts, {
362 cwd : opts.proc_path,
363 watch : opts.development_mode,
364 force_name : package_json.name,
365 started_as_module : true
366 });
367
368 // Start the module
369 CLI.start(package_json, opts, function(err, data) {
370 if (err) return cb(err);
371
372 if (opts.safe) {
373 Common.printOut(cst.PREFIX_MSG_MOD + 'Monitoring module behavior for potential issue (5secs...)');
374
375 var time = typeof(opts.safe) == 'boolean' ? 3000 : parseInt(opts.safe);
376 return setTimeout(function() {
377 CLI.describe(package_json.name, function(err, apps) {
378 if (err || apps[0].pm2_env.restart_time > 2) {
379 return Rollback.revert(CLI, package_json.name, function() {
380 return cb(new Error('New Module is instable, restored to previous version'));
381 });
382 }
383 return cb(null, data);
384 });
385 }, time);
386 }
387
388 return cb(null, data);
389 });
390};
391
392
393
394var Rollback = {
395 revert : function(CLI, module_name, cb) {
396 var canonic_module_name = Utility.getCanonicModuleName(module_name);
397 var backup_path = path.join(require('os').tmpdir(), canonic_module_name);
398 var module_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
399
400 try {
401 fs.statSync(backup_path)
402 } catch(e) {
403 return cb(new Error('no backup found'));
404 }
405
406 Common.printOut(cst.PREFIX_MSG_MOD + chalk.bold.red('[[[[[ Module installation failure! ]]]]]'));
407 Common.printOut(cst.PREFIX_MSG_MOD + chalk.bold.red('[RESTORING TO PREVIOUS VERSION]'));
408
409 CLI.deleteModule(canonic_module_name, function() {
410 // Delete failing module
411
412 if (module_name.includes('modules') === true)
413 deleteFolderRecursive(module_path)
414 // Restore working version
415 copydirSync(backup_path, path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name));
416
417 var proc_path = path.join(module_path, 'node_modules', canonic_module_name);
418 var package_json_path = path.join(proc_path, 'package.json');
419
420 // Start module
421 StartModule(CLI, {
422 cmd : package_json_path,
423 development_mode : false,
424 proc_path : proc_path
425 }, cb);
426 });
427 },
428 backup : function(module_name) {
429 // Backup current module
430 var tmpdir = require('os').tmpdir();
431 var canonic_module_name = Utility.getCanonicModuleName(module_name);
432 var module_path = path.join(cst.DEFAULT_MODULE_PATH, canonic_module_name);
433 copydirSync(module_path, path.join(tmpdir, canonic_module_name));
434 }
435}