1 | const xTime = require('x-time')
|
2 | const net = require('net')
|
3 | const cp = require('child_process')
|
4 | const path = require('path')
|
5 | const promisify = require('smart-promisify')
|
6 | const {
|
7 | once,
|
8 | BetterEvents
|
9 | } = require('better-events')
|
10 | const Connection = require('./Connection')
|
11 | const parseTimeout = require('../parse-timeout')
|
12 |
|
13 | /**
|
14 | * A class representing a set of commands to control z1.
|
15 | * @class
|
16 | */
|
17 | class 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 |
|
434 | module.exports = Remote
|