UNPKG

11.8 kBJavaScriptView Raw
1const xTime = require('x-time')
2const net = require('net')
3const cp = require('child_process')
4const path = require('path')
5const promisify = require('smart-promisify')
6const {
7 once,
8 BetterEvents
9} = require('better-events')
10const Connection = require('./Connection')
11const parseTimeout = require('../parse-timeout')
12
13/**
14 * A class representing a set of commands to control z1.
15 * @class
16 */
17class Remote extends BetterEvents {
18 /**
19 * Create a new Remote instance.
20 * @param {string} socketFile - Path to the socket file of the z1 daemon.
21 */
22 constructor(socketFile) {
23 super()
24 /** @type {string} */
25 this.socketFile = socketFile
26 }
27
28 /**
29 * Sends the "ready" signal to the z1 daemon.
30 * @returns {Promise.<void>}
31 */
32 async ready() {
33 if (typeof process.send !== 'function') {
34 throw new Error('Can not send the "ready" signal to z1 because process.send() is not defined.')
35 }
36
37 console.log('ready signal sent')
38 const send = promisify(process.send, process)
39 await send('ready')
40 }
41
42 /**
43 * @typedef resurrectResult
44 * @property {number} started - Number of started workers.
45 */
46
47 /**
48 * Start the apps that were started before exit.
49 * @param {boolean} [immediate] -- Resolve the returned promise immediately after the command has been transmitted.
50 * @returns {Promise.<resurrectResult>}
51 */
52 async resurrect(immediate = false) {
53 this._impossibleInZ1()
54
55 return this._connectAndSend({
56 name: 'resurrect',
57 immediate
58 })
59 }
60
61 /**
62 * @typedef appOptions
63 * @property {string} [name] - The name of the app.
64 * @property {number[]} [ports] - The prots that yoru app listens to.
65 * @property {number} [workers] - The number of workers to start for your app. (default: number of CPUs)
66 * @property {string} [output] - A directory for the log files. (default: ~/.z1/<appname>)
67 */
68
69 /**
70 * @typedef startResult
71 * @property {string} app - The name of the app.
72 * @property {string} dir - The directory of the app.
73 * @property {number} started - The number of started workers.
74 * @property {number[]} ports - The ports that your app listens to.
75 */
76
77 /**
78 * Start the app in the given directory.
79 * @param {string} dir - Directory of the app.
80 * @param {string[]} [args] - Arguments for the app.
81 * @param {appOptions} [opt] - Options that overwrite the ones from the package.json.
82 * @param {{string: string}} [env] - Environment variables for the app.
83 * @param {boolean} [immediate] - Resolve the returned promise immediately after the command has been transmitted.
84 * @returns {Promise.<startResult>}
85 */
86 async start(dir, args = [], opt = {}, env = {}, immediate = false) {
87 const envi = Object.assign({}, process.env, env)
88 return this._connectAndSend({
89 name: 'start',
90 dir: path.resolve(dir || ''),
91 args,
92 opt,
93 env: envi,
94 immediate
95 })
96 }
97
98 /**
99 * @typedef killOptions
100 * @property {string} [signal] - The kill signal for the workers.
101 * @property {number} [timeout] - The time (in ms) until the workers get force-killed.
102 */
103
104 /**
105 * @typedef stopResult
106 * @property {string} app - The name of the app.
107 * @property {number} killed - The number of killed workers.
108 */
109
110 /**
111 * Stop all workers of an app.
112 * @param {string} app - The name of th app.
113 * @param {killOptions} [opt] - Options for the command.
114 * @param {boolean} [immediateResolve] - Resolve the returned promise immediately after the command has been transmitted.
115 * @returns {Promise.<stopResult>}
116 */
117 async stop(app, opt = {}, immediate = false) {
118 opt.timeout = parseTimeout(opt.timeout)
119 return this._connectAndSend({
120 name: 'stop',
121 app,
122 opt,
123 immediate
124 })
125 }
126
127 /**
128 * @typedef restartResult
129 * @property {string} app - The name of the app.
130 * @property {string} dir - The directory of the app.
131 * @property {number} started - The number of started workers.
132 * @property {number} killed - The number of killed workers
133 * @property {number[]} ports - The ports that your app listens to.
134 */
135
136 /**
137 * Restart an app.
138 * @param {string} app - The name of the app.
139 * @param {killOptions} [opt] - Options for the command.
140 * @param {boolean} [immediate] - Resolve the returned promise immediately after the command has been transmitted.
141 * @returns {Promise.<restartResult>}
142 */
143 async restart(app, opt = {}, immediate = false) {
144 opt.timeout = parseTimeout(opt.timeout)
145 return this._connectAndSend({
146 name: 'restart',
147 app,
148 opt,
149 immediate
150 })
151 }
152
153 /**
154 * @typedef restartAllResult
155 * @property {number} started - The number of started workers.
156 * @property {number} killed - The number of killed workers
157 */
158
159 /**
160 * Restart all apps.
161 * @param {killOptions} [opt] - Options for the command.
162 * @param {boolean} [immediate] - Resolve the returned promise immediately after the command has been transmitted.
163 * @returns {Promise.<restartAllResult>}
164 */
165 async restartAll(opt = {}, immediate = false) {
166 opt.timeout = parseTimeout(opt.timeout)
167 return this._connectAndSend({
168 name: 'restart-all',
169 opt,
170 immediate
171 })
172 }
173
174 /**
175 * Stop the z1 daemon.
176 * @returns {Promise.<void>}
177 */
178 async exit() {
179 await this._connectAndSend({
180 name: 'exit'
181 })
182
183 await this._waitForDisconnect()
184 }
185
186 /**
187 * Upgrade the z1 daemon to a new version. Do not call this in a child process of z1!
188 * @returns {Promise.<void>}
189 */
190 async upgrade() {
191 this._impossibleInZ1()
192
193 await this.exit()
194 await this.resurrect()
195 }
196
197 /**
198 * @typedef infoResult
199 * @property {string} name - The name of the app.
200 * @property {string} dir - Directory of the app.
201 * @property {number[]} ports - Ports that the app uses.
202 * @property {number} pending - Number of pending workers.
203 * @property {number} available - Number of available workers.
204 * @property {number} killed - Number of killed workers.
205 * @property {number} reviveCount - Shows how often the app has been revived.
206 */
207
208 /**
209 * Get detailed information about an app.
210 * @param {string} app - The name of the app.
211 * @returns {Promise.<infoResult>}
212 */
213 async info(app) {
214 return this._connectAndSend({
215 name: 'info',
216 app
217 })
218 }
219
220 /**
221 * @typedef listAppStats
222 * @property {string} dir - Directory of the app.
223 * @property {number[]} ports - Ports that the app uses.
224 * @property {number} pending - Number of pending workers.
225 * @property {number} available - Number of available workers.
226 * @property {number} killed - Number of killed workers.
227 */
228
229 /**
230 * @typedef listResult
231 * @property {boolean} isResurrectable - Is true if the resurrect command can be used.
232 * @property {{string: listAppStats}} stats - Statistics for each app.
233 */
234
235 /**
236 * Get a list of all running apps.
237 * @returns {Promise.<listResult>}
238 */
239 async list() {
240 return this._connectAndSend({
241 name: 'list'
242 })
243 }
244
245 async logs(app) {
246 return this._connectAndSend({
247 name: 'logs',
248 app
249 }, connection => {
250 connection.shareSIGINT()
251 })
252 }
253
254 /**
255 * Throws an error if called within a subprocess/worker of z1.
256 * @returns {void}
257 */
258 _impossibleInZ1() {
259 if (process.env.APPNAME && this.ready) {
260 throw new Error('It is impossible to use this operation within apps that are managed with z1')
261 }
262 }
263
264 /**
265 * Returns a promise that resolves when the ping command was successful.
266 * @returns {Promise.<void>}
267 */
268 async _ping() {
269 await this._send({
270 name: 'ping'
271 })
272 }
273
274 /**
275 * Returns a promise that resolves when the daemon is not available anymore.
276 * @returns {Promise.<void>}
277 */
278 async _waitForDisconnect() {
279 while (1) {
280 try {
281 await this._ping()
282 await xTime(100)
283 } catch (err) {
284 if (err.code === 'ECONNREFUSED' || err.code === 'ENOENT') {
285 break
286 }
287 throw err
288 }
289 }
290 }
291
292 /**
293 * Returns a promise that resolves as soon as the daemon is available.
294 * @returns {Promise.<void>}
295 */
296 async _waitForConnection() {
297 while (1) {
298 try {
299 await this._ping()
300 return
301 } catch (err) {
302 if (err.code === 'ECONNREFUSED' || err.code === 'ENOENT') {
303 await xTime(100)
304 continue
305 }
306 throw err
307 }
308 }
309 }
310
311 /**
312 * Sends a command to the server.
313 * @param {Object} object - An object representing the command.
314 * @param {function} connectionHandler - Call this with the connection object.
315 * @returns {Promise.<*>} - The result of the command.
316 */
317 async _send(object, connectionHandler) {
318 return new Promise((resolve, reject) => {
319 const socket = net.connect(this.socketFile, () => {
320 object.type = 'command'
321 socket.write(JSON.stringify(object) + '\n')
322
323 const connection = new Connection(socket)
324
325 connection.on('message', message => {
326 if (message.type === 'result') {
327 return resolve(message.result)
328 }
329
330 if (message.type === 'log') {
331 return this.emit('log', message.log, message)
332 }
333
334 if (message.type === 'stdout' || message.type === 'stderr') {
335 let chunk = message.chunk
336 if (typeof message.chunk === 'object' && message.chunk.type === 'Buffer') {
337 chunk = Buffer.from(message.chunk.data)
338 }
339
340 this.emit(message.type, chunk)
341 }
342
343 if (message.type === 'error') {
344 // reassemble the error
345 const error = new Error(message.error.message)
346 error.stack = message.error.stack
347 error.code = message.error.code
348
349 reject(error)
350 }
351 })
352
353 connection.once('error', reject)
354
355 if (typeof connectionHandler === 'function') {
356 connectionHandler(connection)
357 }
358 })
359
360 socket.once('error', reject)
361 })
362 }
363
364 /**
365 * Sends a command to the daemon. It starts the daemon if it is not running.
366 * @param {Object} object - An object representing the command.
367 * @param {function} connectionHandler - Call this with the connection object.
368 * @returns {Promise.<*>} - The result of the command.
369 */
370 async _connectAndSend(object, connectionHandler) {
371 await this._connect()
372 return this._send(object, connectionHandler)
373 }
374
375 /**
376 * Returns true if the daemon is online.
377 * @returns {Promise.<boolean>}
378 */
379 async _isOnline() {
380 try {
381 await this._ping()
382 return true
383 } catch (err) {
384 if (err.code !== 'ECONNREFUSED' && err.code !== 'ENOENT') {
385 throw err
386 }
387
388 return false
389 }
390 }
391
392 /**
393 * Tries to connect to the daemon. It starts the daemon if it is not running.
394 * @returns {Promise.<void>}
395 */
396 async _connect() {
397 const online = await this._isOnline()
398
399 if (!online) {
400 await this._startDaemon()
401 }
402 }
403
404 /**
405 * Start the daemon.
406 * @returns {Promise.<void>} - Returns a promise that resolves after the daemon is started.
407 */
408 async _startDaemon(options) {
409 const z1Path = path.join(__dirname, '../../..')
410 const file = path.join(z1Path, 'daemon', 'main.js')
411 let node = process.argv[0]
412
413 const spawnOptions = Object.assign({
414 stdio: 'ignore',
415 detached: true
416 }, options)
417
418 const p = cp.spawn(node, [file], spawnOptions)
419
420 const error = once(p, 'error')
421
422 const exit = once(p, 'exit').then(code => {
423 if (code) {
424 throw new Error(`daemon exited with code: "${code}"`)
425 }
426 })
427
428 p.unref()
429
430 await Promise.race([error, exit, this._waitForConnection()])
431 }
432}
433
434module.exports = Remote