UNPKG

5.13 kBJavaScriptView Raw
1const fs = require('fs').promises
2const p = require('path')
3
4const mkdirp = require('mkdirp')
5const pm2 = require('pm2')
6
7const { HyperdriveClient } = require('hyperdrive-daemon-client')
8const constants = require('hyperdrive-daemon-client/lib/constants')
9
10const HyperdriveDaemon = require('.')
11
12async function start (opts = {}) {
13 const initialOpts = opts
14 opts = { ...constants, ...opts }
15 opts.endpoint = `localhost:${opts.port}`
16
17 if (opts.env && !opts.env.PATH) {
18 opts.env = { ...opts.env, PATH: process.env.PATH }
19 }
20
21 const client = new HyperdriveClient({ endpoint: opts.endpoint, storage: initialOpts.storage || opts.root })
22 const running = await new Promise((resolve, reject) => {
23 client.ready(err => {
24 if (!err) return resolve(true)
25 if (err.versionMismatch) return reject(new Error(`Daemon is already running with incompatible version: ${err.version}`))
26 return resolve(false)
27 })
28 })
29 if (running) return { opts }
30
31 await new Promise((resolve, reject) => {
32 const storagePath = p.join(opts.storage, 'storage')
33 mkdirp(storagePath, err => {
34 if (err) return reject(new Error(`Could not create storage directory: ${storagePath}`))
35 return resolve()
36 })
37 })
38
39 opts.memoryOnly = opts['memory-only']
40 opts.noAnnounce = opts['no-announce']
41 opts.noDebug = opts['no-debug']
42 opts.logLevel = opts['log-level']
43
44 /**
45 * HACK
46 * If 'pm2' detects a space in the 'script' path, it assumes the call is something like "python foo.py".
47 * When that's the case, it transforms the call into `/bin/bash -c python foo.py`.
48 * This creates a problem for some hyperdrive apps because they may have spaces in their install paths.
49 * The resulting call ends up being `${interpreter} /bin/bash -c ${script}`, which is wrong.
50 * (To add a little more complexity, it does *not* do this on Windows.)
51 *
52 * To solve that, we craft the pm2 call to use '/bin/bash -c' correctly.
53 * -prf
54 */
55 const IS_WINDOWS = (process.platform === 'win32' || process.platform === 'win64' || /^(msys|cygwin)$/.test(process.env.OSTYPE))
56 var script = p.join(__dirname, 'index.js')
57
58 var args = []
59 if (opts.port) args.push('--port', opts.port)
60 if (opts.storage) args.push('--storage', opts.storage)
61 if (opts.logLevel) args.push('--log-level', opts.logLevel)
62 if (opts.memoryOnly) args.push('--memory-only')
63 if (opts.noAnnounce) args.push('--no-announce')
64 if (opts.noDebug) args.push('--no-debug')
65
66 if (opts.bootstrap === false) args.push('--bootstrap', false)
67 else if (Array.isArray(opts.bootstrap) && opts.bootstrap.length) args.push('--bootstrap', opts.bootstrap.join(','))
68
69 var interpreter = opts.interpreter || process.execPath
70 var interpreterArgs = [`--max-old-space-size=${opts.heapSize}`]
71 if (!IS_WINDOWS) {
72 const execArg = [interpreter, interpreterArgs, script].concat(args).map(escapeStringArg).join(' ')
73 args = ['-c', execArg]
74 script = 'bash'
75 interpreter = undefined
76 interpreterArgs = undefined
77 }
78
79 const description = {
80 script,
81 args,
82 interpreter,
83 interpreterArgs,
84 name: opts.processName || 'hyperdrive',
85 env: opts.env || process.env,
86 output: opts.unstructuredLog,
87 error: opts.structuredLog,
88 killTimeout: 10000,
89 autorestart: false
90 }
91
92 try {
93 if (opts.structuredLog === constants.structuredLog) {
94 await fs.rename(constants.structuredLog, constants.structuredLog.replace('.json', '.old.json'))
95 }
96 if (opts.unstructuredLog === constants.unstructuredLog) {
97 await fs.rename(constants.unstructuredLog, constants.unstructuredLog.replace('.log', '.old.log'))
98 }
99 } catch (err) {
100 // If the log file couldn't be rotated, it's OK.
101 }
102
103 if (opts.foreground) {
104 return startForeground(description, opts)
105 } else {
106 return startDaemon(description, opts)
107 }
108
109 function startForeground (description, opts) {
110 const daemon = new HyperdriveDaemon({ ...opts, metadata: null, main: true })
111 process.title = 'hyperdrive'
112 process.removeAllListeners('SIGINT')
113 process.removeAllListeners('SIGTERM')
114 daemon.start()
115 return { opts, description }
116 }
117
118 function startDaemon (description, opts) {
119 return new Promise((resolve, reject) => {
120 pm2.connect(!!opts.noPM2DaemonMode, err => {
121 if (err) return reject(new Error('Could not connect to the process manager to start the daemon.'))
122 pm2.start(description, err => {
123 pm2.disconnect()
124 if (err) return reject(err)
125 return resolve({ opts, description })
126 })
127 })
128 })
129 }
130}
131
132async function stop (name, port) {
133 name = name || constants.processName
134 port = port || constants.port
135
136 return new Promise((resolve, reject) => {
137 pm2.connect(err => {
138 if (err) return reject(new Error('Could not connect to the process manager to stop the daemon.'))
139 pm2.delete(name, err => {
140 pm2.disconnect()
141 if (err) return reject(err)
142 return resolve()
143 })
144 })
145 })
146}
147
148module.exports = {
149 start,
150 stop
151}
152
153function escapeStringArg (v) {
154 return (typeof v === 'string' && v.includes(' ')) ? `"${v}"` : v
155}