forked from TryGhost/Ghost-CLI
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsystem.js
378 lines (322 loc) · 11.1 KB
/
system.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
'use strict';
const fs = require('fs-extra');
const os = require('os');
const path = require('path');
const findKey = require('lodash/findKey');
const uniqueId = require('lodash/uniqueId');
const each = require('lodash/each');
const some = require('lodash/some');
const Promise = require('bluebird');
const Config = require('./utils/config');
const Instance = require('./instance');
const Extension = require('./extension');
const ProcessManager = require('./process-manager');
/**
* System class. Responsible for managing the environment of a running CLI
* process, as well as all of the various CLI instances on a system
*
* @class System
*/
class System {
/**
* UI Instance
* @property ui
* @type UI
* @public
*/
get platform() {
if (!this._platform) {
const platform = os.platform();
this._platform = {
linux: platform === 'linux',
macos: platform === 'darwin',
windows: platform === 'win32'
};
}
return this._platform;
}
/**
* Version of the CLI
*
* @property cliVersion
* @type string
* @public
*/
get cliVersion() {
if (!this._version) {
this._version = require('../package.json').version;
}
return this._version;
}
/**
* Global config (lives at ~/.ghost/config). This config is primarily
* responsible for storing all of the Ghost instances on a particular system.
*
* @property globalConfig
* @type Config
* @public
*/
get globalConfig() {
if (!this._globalConfig) {
fs.ensureDirSync(this.constructor.globalDir);
this._globalConfig = new Config(path.join(this.constructor.globalDir, 'config'));
}
return this._globalConfig;
}
/**
* Returns the running operating system
*
* @property operatingSystem
* @type Object
* @public
*/
get operatingSystem() {
if (!this._operatingSystem) {
const getOS = require('./utils/get-os');
this._operatingSystem = getOS(this.platform);
return this._operatingSystem;
}
return this._operatingSystem;
}
/**
* Constructs the System class
*
* @param {UI} UI instance
* @param {Array<Object>} Array of loaded extensions
*/
constructor(ui, extensions) {
this.ui = ui;
this._instanceCache = {};
this._extensions = extensions.map(Extension.getInstance.bind(Extension, ui, this)).filter(Boolean);
}
/**
* Sets the environment of the CLI running process
*
* @param {Boolean} isDevelopmentMode Set this to true for development
* @param {Boolean} setNodeEnv Whether or not to set the NODE_ENV variable
* @method setEnvironment
* @public
*/
setEnvironment(isDevelopmentMode, setNodeEnv) {
this.environment = isDevelopmentMode ? 'development' : 'production';
this.development = isDevelopmentMode;
this.production = !isDevelopmentMode;
if (setNodeEnv) {
process.env.NODE_ENV = this.environment;
}
}
/**
* Returns an instance by name, or the instance of the current
* working directory
*
* @param {string} Optional name of instance to retrieve
* @return {Instance}
* @method getInstance
* @public
*/
getInstance(name) {
if (!name) {
return this.cachedInstance(process.cwd());
}
const instance = this.globalConfig.get(`instances.${name}`);
if (!instance) {
return null;
}
return this.cachedInstance(instance.cwd);
}
/**
* Adds an instance to the global instance list. Also
* de-duplicates the process name
*
* @param {Instance} Instance to add
* @method addInstance
* @public
*/
addInstance(instance) {
const instances = this.globalConfig.get('instances', {});
const existingInstance = findKey(instances, cfg => cfg.cwd === instance.dir);
if (existingInstance) {
instance.name = existingInstance;
return;
}
let currentName = instance.name;
const existingNames = Object.keys(instances);
// Loop through the names, incrementing it by 1 each time until
// we hit one that doesn't already exist
while (existingNames.includes(currentName)) {
currentName = uniqueId(currentName.match(/-\d+$/) ? currentName.replace(/-\d+$/, '-') : `${currentName}-`);
}
// Only change the instance name if we ended up needing to de-duplicate
if (currentName !== instance.name) {
instance.name = currentName;
}
this.globalConfig.set(`instances.${currentName}`, {cwd: instance.dir}).save();
}
/**
* Remove an instance from the global instance list.
*
* @param {Instance} Instance to remove
* @method removeInstance
* @public
*/
removeInstance(instance) {
const instances = this.globalConfig.get('instances', {});
delete instances[instance.name];
this.globalConfig.set('instances', instances).save();
}
/**
* Checks whether or not the system knows about an instance
* @param {Instance} instance
*/
hasInstance(instance) {
const instances = this.globalConfig.get('instances', {});
return some(instances, ({cwd}, name) => cwd === instance.dir && name === instance.name);
}
/**
* Gets an instance from cache. Populates cache with new instance if none found
*
* @param {string} dir
* @param {Boolean} skipEnvCheck
* @method cachedInstance
* @private
*/
cachedInstance(dir) {
if (!this._instanceCache[dir]) {
this._instanceCache[dir] = new Instance(this.ui, this, dir);
}
return this._instanceCache[dir];
}
/**
* Gets all listed instances. If running is true, returns only running instances
* Also removes any instances that no longer exist
*
* @param {Boolean} running If true, returns only running instances
* @return {Promise<Instance>} instances
* @method getAllInstances
* @public
*/
getAllInstances(running) {
const instances = this.globalConfig.get('instances', {});
const names = Object.keys(instances);
const namesToRemove = [];
// Remove nonexistent instances
const result = names.map((name) => {
if (!fs.existsSync(path.join(instances[name].cwd, '.ghost-cli'))) {
namesToRemove.push(name);
return null;
}
return this.getInstance(name);
}).filter(Boolean);
if (namesToRemove.length) {
namesToRemove.forEach(name => delete instances[name]);
this.globalConfig.set('instances', instances).save();
}
// If we're not filtering out stopped instances, just return the result
if (!running) {
return Promise.resolve(result);
}
// Return the result filtered by whether or not the instance is running
return Promise.filter(result, instance => instance.running());
}
/**
* Calls a hook on any loaded extensions. Internal, used by CLI commands
*
* @param {string} hookName name of hook to call
* @param {...any} Arguments to pass to the hook
* @return {Promise<void>} Promise that resolves after the hook has been called
* on each of the extensions
* @method hook
* @private
*/
hook() {
const args = Array.from(arguments);
const hookName = args.shift();
if (!hookName) {
return Promise.reject(new Error('Hook name must be supplied.'));
}
return Promise.mapSeries(this._extensions, (extension) => {
if (!extension[hookName]) {
return Promise.resolve();
}
return Promise.resolve(extension[hookName].apply(extension, args));
});
}
/**
* Get the process manager by name. Process manager classes can come
* from any one of the loaded extensions. If no process manager is found for
* the given name or the located process manager is invalid, the local
* process manager is returned
*
* @param {string} name Name of the process manager
* @return {Object} Object with two properties
* - Class: Class of the process manager
* - Name: Name of the process manager as given by the extension
* @method getProcessManager
* @public
*/
getProcessManager(name) {
if (!name || name === 'local') {
return {Class: require('./utils/local-process'), name: 'local'};
}
const availableProcessManagers = this._getAvailableProcessManagers();
if (!availableProcessManagers[name]) {
this.ui.log(`Process manager '${name}' does not exist, defaulting to 'local'`, 'yellow');
return {Class: require('./utils/local-process'), name: 'local'};
}
const Klass = require(availableProcessManagers[name]);
const valid = ProcessManager.isValid(Klass);
if (valid !== true) {
const msg = valid !== false ? `missing required fields: ${valid.join(', ')}` : 'does not inherit from base class';
this.ui.log(`Process manager '${name}' ${msg}, defaulting to 'local'`, 'yellow');
return {Class: require('./utils/local-process'), name: 'local'};
}
if (!Klass.willRun()) {
this.ui.log(`Process manager '${name}' will not run on this system, defaulting to 'local'`, 'yellow');
return {Class: require('./utils/local-process'), name: 'local'};
}
return {Class: Klass, name: name};
}
/**
* Gets a list of the available process managers from all of the extensions
*
* @return {Object} hash of all process managers indexed by name
* @method _getAvailableProcessManagers
* @private
*/
_getAvailableProcessManagers() {
const available = {};
this._extensions.forEach((ext) => {
each(ext.processManagers, (pm, name) => {
if (!fs.existsSync(pm)) {
return;
}
available[name] = pm;
});
});
return available;
}
/**
* Writes an error message to a global log file.
*
* @param {string} error Error to write to log file
* @return {string} path to generated log file
* @method writeErrorLog
* @public
*/
writeErrorLog(error) {
// Ensure logs dir exists
fs.ensureDirSync(path.join(this.constructor.globalDir, 'logs'));
const date = new Date();
const logFilePath = path.join(this.constructor.globalDir, 'logs', `ghost-cli-debug-${date.toISOString().replace(/:|\./g, '_')}.log`);
fs.writeFileSync(logFilePath, error);
return logFilePath;
}
/**
* @static
* @property globalDir
* @type string
* @private
*/
}
System.globalDir = path.join(os.homedir(), '.ghost');
module.exports = System;