flightdeck 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flightdeck.$configPath.schema.json +18 -8
- package/dist/flightdeck.bin.js +106 -66
- package/dist/flightdeck.main.schema.json +18 -8
- package/dist/lib.d.ts +11 -6
- package/dist/lib.js +95 -58
- package/package.json +2 -2
- package/src/flightdeck.bin.ts +12 -9
- package/src/flightdeck.lib.ts +122 -95
|
@@ -29,22 +29,32 @@
|
|
|
29
29
|
"additionalProperties": false
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
|
-
"
|
|
32
|
+
"flightdeckRootDir": {
|
|
33
33
|
"type": "string"
|
|
34
34
|
},
|
|
35
|
-
"
|
|
36
|
-
"type": "
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
35
|
+
"scripts": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"download": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
},
|
|
41
|
+
"install": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": [
|
|
46
|
+
"download",
|
|
47
|
+
"install"
|
|
48
|
+
],
|
|
49
|
+
"additionalProperties": false
|
|
40
50
|
}
|
|
41
51
|
},
|
|
42
52
|
"required": [
|
|
43
53
|
"secret",
|
|
44
54
|
"packageName",
|
|
45
55
|
"services",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
56
|
+
"flightdeckRootDir",
|
|
57
|
+
"scripts"
|
|
48
58
|
],
|
|
49
59
|
"additionalProperties": false,
|
|
50
60
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
package/dist/flightdeck.bin.js
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
// src/flightdeck.bin.ts
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import {cli, optional
|
|
5
|
+
import {cli, optional} from "comline";
|
|
6
6
|
import {z} from "zod";
|
|
7
7
|
|
|
8
8
|
// src/flightdeck.lib.ts
|
|
9
9
|
import {execSync, spawn} from "node:child_process";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
writeFileSync
|
|
15
|
+
} from "node:fs";
|
|
11
16
|
import {createServer} from "node:http";
|
|
12
17
|
import {homedir} from "node:os";
|
|
13
18
|
import {resolve} from "node:path";
|
|
@@ -26,16 +31,16 @@ class FlightDeck {
|
|
|
26
31
|
defaultServicesReadyToUpdate;
|
|
27
32
|
servicesReadyToUpdate;
|
|
28
33
|
servicesShouldRestart;
|
|
29
|
-
|
|
34
|
+
logger;
|
|
35
|
+
serviceLoggers;
|
|
30
36
|
servicesLive;
|
|
31
37
|
servicesDead;
|
|
32
38
|
live = new Future(() => {
|
|
33
39
|
});
|
|
34
40
|
dead = new Future(() => {
|
|
35
41
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
backupServiceDir;
|
|
42
|
+
restartTimes = [];
|
|
43
|
+
persistentStateDir;
|
|
39
44
|
constructor(options) {
|
|
40
45
|
this.options = options;
|
|
41
46
|
const { secret, flightdeckRootDir = resolve(homedir(), `services`) } = options;
|
|
@@ -48,21 +53,46 @@ class FlightDeck {
|
|
|
48
53
|
]));
|
|
49
54
|
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate };
|
|
50
55
|
this.servicesShouldRestart = true;
|
|
56
|
+
this.logger = {
|
|
57
|
+
info: (...args) => {
|
|
58
|
+
console.log(`${this.options.packageName}:`, ...args);
|
|
59
|
+
},
|
|
60
|
+
warn: (...args) => {
|
|
61
|
+
console.warn(`${this.options.packageName}:`, ...args);
|
|
62
|
+
},
|
|
63
|
+
error: (...args) => {
|
|
64
|
+
console.error(`${this.options.packageName}:`, ...args);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
this.serviceLoggers = fromEntries(servicesEntries.map(([serviceName]) => [
|
|
68
|
+
serviceName,
|
|
69
|
+
{
|
|
70
|
+
info: (...args) => {
|
|
71
|
+
console.log(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
72
|
+
},
|
|
73
|
+
warn: (...args) => {
|
|
74
|
+
console.warn(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
75
|
+
},
|
|
76
|
+
error: (...args) => {
|
|
77
|
+
console.error(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]));
|
|
51
81
|
this.servicesLive = servicesEntries.map(() => new Future(() => {
|
|
52
82
|
}));
|
|
53
83
|
this.servicesDead = servicesEntries.map(() => new Future(() => {
|
|
54
84
|
}));
|
|
55
85
|
this.live.use(Promise.all(this.servicesLive));
|
|
56
86
|
this.dead.use(Promise.all(this.servicesDead));
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
87
|
+
this.persistentStateDir = resolve(flightdeckRootDir, `.state`, options.packageName);
|
|
88
|
+
if (!existsSync(this.persistentStateDir)) {
|
|
89
|
+
mkdirSync(this.persistentStateDir, { recursive: true });
|
|
90
|
+
}
|
|
60
91
|
createServer((req, res) => {
|
|
61
92
|
let data = [];
|
|
62
93
|
req.on(`data`, (chunk) => {
|
|
63
94
|
data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
|
|
64
95
|
}).on(`end`, () => {
|
|
65
|
-
console.log(req.headers);
|
|
66
96
|
const authHeader = req.headers.authorization;
|
|
67
97
|
try {
|
|
68
98
|
if (typeof req.url === `undefined`)
|
|
@@ -70,19 +100,28 @@ class FlightDeck {
|
|
|
70
100
|
if (authHeader !== `Bearer ${secret}`)
|
|
71
101
|
throw 401;
|
|
72
102
|
const url = new URL(req.url, ORIGIN);
|
|
73
|
-
|
|
103
|
+
this.logger.info(req.method, url.pathname);
|
|
74
104
|
switch (req.method) {
|
|
75
105
|
case `POST`:
|
|
76
106
|
{
|
|
77
|
-
console.log(`received post, url is ${url.pathname}`);
|
|
78
107
|
switch (url.pathname) {
|
|
79
108
|
case `/`:
|
|
80
109
|
{
|
|
81
110
|
res.writeHead(200);
|
|
82
111
|
res.end();
|
|
83
|
-
this.
|
|
112
|
+
const installFile = resolve(this.persistentStateDir, `install`);
|
|
113
|
+
const readyFile = resolve(this.persistentStateDir, `ready`);
|
|
114
|
+
if (!existsSync(installFile)) {
|
|
115
|
+
this.logger.info(`Install file does not exist yet. Creating...`);
|
|
116
|
+
writeFileSync(installFile, ``);
|
|
117
|
+
}
|
|
118
|
+
if (existsSync(readyFile)) {
|
|
119
|
+
this.logger.info(`Ready file exists. Removing...`);
|
|
120
|
+
rmSync(readyFile);
|
|
121
|
+
}
|
|
122
|
+
this.getLatestRelease();
|
|
84
123
|
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
85
|
-
|
|
124
|
+
this.logger.info(`All services are ready to update!`);
|
|
86
125
|
this.stopAllServices();
|
|
87
126
|
return;
|
|
88
127
|
}
|
|
@@ -107,7 +146,7 @@ class FlightDeck {
|
|
|
107
146
|
throw 405;
|
|
108
147
|
}
|
|
109
148
|
} catch (thrown) {
|
|
110
|
-
|
|
149
|
+
this.logger.error(thrown, req.url);
|
|
111
150
|
if (typeof thrown === `number`) {
|
|
112
151
|
res.writeHead(thrown);
|
|
113
152
|
res.end();
|
|
@@ -117,44 +156,44 @@ class FlightDeck {
|
|
|
117
156
|
}
|
|
118
157
|
});
|
|
119
158
|
}).listen(PORT, () => {
|
|
120
|
-
|
|
159
|
+
this.logger.info(`Server started on port ${PORT}`);
|
|
121
160
|
});
|
|
122
161
|
this.startAllServices();
|
|
123
162
|
}
|
|
124
163
|
startAllServices() {
|
|
125
|
-
|
|
164
|
+
this.logger.info(`Starting all services...`);
|
|
126
165
|
for (const [serviceName] of toEntries(this.services)) {
|
|
127
166
|
this.startService(serviceName);
|
|
128
167
|
}
|
|
129
168
|
}
|
|
130
169
|
startService(serviceName) {
|
|
131
|
-
|
|
132
|
-
if (this.safety
|
|
170
|
+
this.logger.info(`Starting service ${this.options.packageName}::${serviceName}, try ${this.safety}/2...`);
|
|
171
|
+
if (this.safety >= 2) {
|
|
133
172
|
throw new Error(`Out of tries...`);
|
|
134
173
|
}
|
|
135
174
|
this.safety++;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.
|
|
175
|
+
const readyFile = resolve(this.persistentStateDir, `ready`);
|
|
176
|
+
if (!existsSync(readyFile)) {
|
|
177
|
+
this.logger.info(`Tried to start service but failed: could not find readyFile: ${readyFile}`);
|
|
178
|
+
this.getLatestRelease();
|
|
139
179
|
this.applyUpdate();
|
|
140
180
|
this.startService(serviceName);
|
|
141
181
|
return;
|
|
142
182
|
}
|
|
143
183
|
const [executable, ...args] = this.options.services[serviceName].run;
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
cwd: this.currentServiceDir,
|
|
184
|
+
const serviceProcess = spawn(executable, args, {
|
|
185
|
+
cwd: this.options.flightdeckRootDir,
|
|
147
186
|
env: import.meta.env
|
|
148
187
|
});
|
|
149
188
|
this.services[serviceName] = new ChildSocket(serviceProcess, `${this.options.packageName}::${serviceName}`, console);
|
|
150
189
|
this.services[serviceName].onAny((...messages) => {
|
|
151
|
-
|
|
190
|
+
this.logger.info(`\uD83D\uDCAC`, ...messages);
|
|
152
191
|
});
|
|
153
192
|
this.services[serviceName].on(`readyToUpdate`, () => {
|
|
154
|
-
|
|
193
|
+
this.serviceLoggers[serviceName].info(`Ready to update.`);
|
|
155
194
|
this.servicesReadyToUpdate[serviceName] = true;
|
|
156
195
|
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
157
|
-
|
|
196
|
+
this.logger.info(`All services are ready to update.`);
|
|
158
197
|
this.stopAllServices();
|
|
159
198
|
}
|
|
160
199
|
});
|
|
@@ -169,15 +208,16 @@ class FlightDeck {
|
|
|
169
208
|
this.dead.use(Promise.all(this.servicesDead));
|
|
170
209
|
});
|
|
171
210
|
this.services[serviceName].process.on(`close`, (exitCode) => {
|
|
172
|
-
|
|
211
|
+
this.serviceLoggers[serviceName].info(`Exited with code ${exitCode}`);
|
|
173
212
|
this.services[serviceName] = null;
|
|
174
213
|
if (!this.servicesShouldRestart) {
|
|
175
|
-
|
|
214
|
+
this.serviceLoggers[serviceName].info(`Will not be restarted.`);
|
|
176
215
|
return;
|
|
177
216
|
}
|
|
178
|
-
const
|
|
217
|
+
const installFile = resolve(this.persistentStateDir, `install`);
|
|
218
|
+
const updatesAreReady = existsSync(installFile);
|
|
179
219
|
if (updatesAreReady) {
|
|
180
|
-
|
|
220
|
+
this.serviceLoggers[serviceName].info(`Updating before startup...`);
|
|
181
221
|
this.restartTimes = [];
|
|
182
222
|
this.applyUpdate();
|
|
183
223
|
this.startService(serviceName);
|
|
@@ -187,58 +227,54 @@ class FlightDeck {
|
|
|
187
227
|
this.restartTimes = this.restartTimes.filter((time) => time > fiveMinutesAgo);
|
|
188
228
|
this.restartTimes.push(now);
|
|
189
229
|
if (this.restartTimes.length < 5) {
|
|
190
|
-
|
|
230
|
+
this.serviceLoggers[serviceName].info(`Crashed. Restarting...`);
|
|
191
231
|
this.startService(serviceName);
|
|
192
232
|
} else {
|
|
193
|
-
|
|
233
|
+
this.serviceLoggers[serviceName].info(`Crashed 5 times in 5 minutes. Not restarting.`);
|
|
194
234
|
}
|
|
195
235
|
}
|
|
196
236
|
});
|
|
197
237
|
this.safety = 0;
|
|
198
238
|
}
|
|
199
239
|
applyUpdate() {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
240
|
+
this.logger.info(`Installing...`);
|
|
241
|
+
try {
|
|
242
|
+
execSync(this.options.scripts.install);
|
|
243
|
+
const installFile = resolve(this.persistentStateDir, `install`);
|
|
244
|
+
if (existsSync(installFile)) {
|
|
245
|
+
rmSync(installFile);
|
|
206
246
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
renameSync(this.currentServiceDir, this.backupServiceDir);
|
|
247
|
+
const readyFile = resolve(this.persistentStateDir, `ready`);
|
|
248
|
+
writeFileSync(readyFile, ``);
|
|
249
|
+
this.logger.info(`Installed!`);
|
|
250
|
+
} catch (thrown) {
|
|
251
|
+
if (thrown instanceof Error) {
|
|
252
|
+
this.logger.error(`Failed to get the latest release: ${thrown.message}`);
|
|
214
253
|
}
|
|
215
|
-
|
|
216
|
-
this.restartTimes = [];
|
|
217
|
-
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate };
|
|
218
|
-
} else {
|
|
219
|
-
console.log(`Service ${this.options.packageName} is already up to date.`);
|
|
254
|
+
return;
|
|
220
255
|
}
|
|
221
256
|
}
|
|
222
|
-
|
|
223
|
-
|
|
257
|
+
getLatestRelease() {
|
|
258
|
+
this.logger.info(`Downloading...`);
|
|
224
259
|
try {
|
|
225
|
-
execSync(this.options.
|
|
260
|
+
execSync(this.options.scripts.download);
|
|
261
|
+
this.logger.info(`Downloaded!`);
|
|
226
262
|
} catch (thrown) {
|
|
227
263
|
if (thrown instanceof Error) {
|
|
228
|
-
|
|
264
|
+
this.logger.error(`Failed to get the latest release: ${thrown.message}`);
|
|
229
265
|
}
|
|
230
266
|
return;
|
|
231
267
|
}
|
|
232
268
|
}
|
|
233
269
|
stopAllServices() {
|
|
234
|
-
|
|
270
|
+
this.logger.info(`Stopping all services...`);
|
|
235
271
|
for (const [serviceName] of toEntries(this.services)) {
|
|
236
272
|
this.stopService(serviceName);
|
|
237
273
|
}
|
|
238
274
|
}
|
|
239
275
|
stopService(serviceName) {
|
|
240
276
|
if (this.services[serviceName]) {
|
|
241
|
-
|
|
277
|
+
this.serviceLoggers[serviceName].info(`Stopping service...`);
|
|
242
278
|
this.services[serviceName].process.kill();
|
|
243
279
|
this.services[serviceName] = null;
|
|
244
280
|
this.servicesDead[this.serviceIdx[serviceName]].use(Promise.resolve());
|
|
@@ -250,10 +286,11 @@ class FlightDeck {
|
|
|
250
286
|
}
|
|
251
287
|
this.live.use(Promise.all(this.servicesLive));
|
|
252
288
|
} else {
|
|
253
|
-
|
|
289
|
+
this.serviceLoggers[serviceName].error(`Tried to stop service, but it wasn't running.`);
|
|
254
290
|
}
|
|
255
291
|
}
|
|
256
292
|
shutdown() {
|
|
293
|
+
this.logger.info(`Shutting down...`);
|
|
257
294
|
this.servicesShouldRestart = false;
|
|
258
295
|
this.stopAllServices();
|
|
259
296
|
}
|
|
@@ -265,8 +302,11 @@ var FLIGHTDECK_MANUAL = {
|
|
|
265
302
|
secret: z.string(),
|
|
266
303
|
packageName: z.string(),
|
|
267
304
|
services: z.record(z.object({ run: z.array(z.string()), waitFor: z.boolean() })),
|
|
268
|
-
|
|
269
|
-
|
|
305
|
+
flightdeckRootDir: z.string(),
|
|
306
|
+
scripts: z.object({
|
|
307
|
+
download: z.string(),
|
|
308
|
+
install: z.string()
|
|
309
|
+
})
|
|
270
310
|
}),
|
|
271
311
|
options: {
|
|
272
312
|
secret: {
|
|
@@ -294,12 +334,12 @@ var FLIGHTDECK_MANUAL = {
|
|
|
294
334
|
description: `Directory where the service is stored.`,
|
|
295
335
|
example: `--flightdeckRootDir=\"./services/sample/repo/my-app/current\"`
|
|
296
336
|
},
|
|
297
|
-
|
|
298
|
-
flag: `
|
|
337
|
+
scripts: {
|
|
338
|
+
flag: `r`,
|
|
299
339
|
required: true,
|
|
300
|
-
description: `
|
|
301
|
-
example: `--
|
|
302
|
-
parse:
|
|
340
|
+
description: `Map of scripts to run.`,
|
|
341
|
+
example: `--scripts="{\\"download\\":\\"npm i",\\"install\\":\\"npm run build\\"}"`,
|
|
342
|
+
parse: JSON.parse
|
|
303
343
|
}
|
|
304
344
|
}
|
|
305
345
|
};
|
|
@@ -29,22 +29,32 @@
|
|
|
29
29
|
"additionalProperties": false
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
|
-
"
|
|
32
|
+
"flightdeckRootDir": {
|
|
33
33
|
"type": "string"
|
|
34
34
|
},
|
|
35
|
-
"
|
|
36
|
-
"type": "
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
35
|
+
"scripts": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"download": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
},
|
|
41
|
+
"install": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": [
|
|
46
|
+
"download",
|
|
47
|
+
"install"
|
|
48
|
+
],
|
|
49
|
+
"additionalProperties": false
|
|
40
50
|
}
|
|
41
51
|
},
|
|
42
52
|
"required": [
|
|
43
53
|
"secret",
|
|
44
54
|
"packageName",
|
|
45
55
|
"services",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
56
|
+
"flightdeckRootDir",
|
|
57
|
+
"scripts"
|
|
48
58
|
],
|
|
49
59
|
"additionalProperties": false,
|
|
50
60
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
package/dist/lib.d.ts
CHANGED
|
@@ -11,7 +11,10 @@ type FlightDeckOptions<S extends string = string> = {
|
|
|
11
11
|
waitFor: boolean;
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
|
-
|
|
14
|
+
scripts: {
|
|
15
|
+
download: string;
|
|
16
|
+
install: string;
|
|
17
|
+
};
|
|
15
18
|
flightdeckRootDir?: string | undefined;
|
|
16
19
|
};
|
|
17
20
|
declare class FlightDeck<S extends string = string> {
|
|
@@ -36,19 +39,21 @@ declare class FlightDeck<S extends string = string> {
|
|
|
36
39
|
[service in S]: boolean;
|
|
37
40
|
};
|
|
38
41
|
servicesShouldRestart: boolean;
|
|
39
|
-
protected
|
|
42
|
+
protected logger: Pick<Console, `error` | `info` | `warn`>;
|
|
43
|
+
protected serviceLoggers: {
|
|
44
|
+
readonly [service in S]: Pick<Console, `error` | `info` | `warn`>;
|
|
45
|
+
};
|
|
40
46
|
servicesLive: Future<void>[];
|
|
41
47
|
servicesDead: Future<void>[];
|
|
42
48
|
live: Future<unknown>;
|
|
43
49
|
dead: Future<unknown>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
readonly backupServiceDir: string;
|
|
50
|
+
protected restartTimes: number[];
|
|
51
|
+
protected persistentStateDir: string;
|
|
47
52
|
constructor(options: FlightDeckOptions<S>);
|
|
48
53
|
protected startAllServices(): void;
|
|
49
54
|
protected startService(serviceName: S): void;
|
|
50
55
|
protected applyUpdate(): void;
|
|
51
|
-
protected
|
|
56
|
+
protected getLatestRelease(): void;
|
|
52
57
|
stopAllServices(): void;
|
|
53
58
|
stopService(serviceName: S): void;
|
|
54
59
|
shutdown(): void;
|
package/dist/lib.js
CHANGED
|
@@ -11,7 +11,12 @@ var __export = (target, all) => {
|
|
|
11
11
|
|
|
12
12
|
// src/flightdeck.lib.ts
|
|
13
13
|
import {execSync, spawn} from "node:child_process";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
writeFileSync
|
|
19
|
+
} from "node:fs";
|
|
15
20
|
import {createServer} from "node:http";
|
|
16
21
|
import {homedir} from "node:os";
|
|
17
22
|
import {resolve} from "node:path";
|
|
@@ -30,16 +35,16 @@ class FlightDeck {
|
|
|
30
35
|
defaultServicesReadyToUpdate;
|
|
31
36
|
servicesReadyToUpdate;
|
|
32
37
|
servicesShouldRestart;
|
|
33
|
-
|
|
38
|
+
logger;
|
|
39
|
+
serviceLoggers;
|
|
34
40
|
servicesLive;
|
|
35
41
|
servicesDead;
|
|
36
42
|
live = new Future(() => {
|
|
37
43
|
});
|
|
38
44
|
dead = new Future(() => {
|
|
39
45
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
backupServiceDir;
|
|
46
|
+
restartTimes = [];
|
|
47
|
+
persistentStateDir;
|
|
43
48
|
constructor(options) {
|
|
44
49
|
this.options = options;
|
|
45
50
|
const { secret, flightdeckRootDir = resolve(homedir(), `services`) } = options;
|
|
@@ -52,21 +57,46 @@ class FlightDeck {
|
|
|
52
57
|
]));
|
|
53
58
|
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate };
|
|
54
59
|
this.servicesShouldRestart = true;
|
|
60
|
+
this.logger = {
|
|
61
|
+
info: (...args) => {
|
|
62
|
+
console.log(`${this.options.packageName}:`, ...args);
|
|
63
|
+
},
|
|
64
|
+
warn: (...args) => {
|
|
65
|
+
console.warn(`${this.options.packageName}:`, ...args);
|
|
66
|
+
},
|
|
67
|
+
error: (...args) => {
|
|
68
|
+
console.error(`${this.options.packageName}:`, ...args);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
this.serviceLoggers = fromEntries(servicesEntries.map(([serviceName]) => [
|
|
72
|
+
serviceName,
|
|
73
|
+
{
|
|
74
|
+
info: (...args) => {
|
|
75
|
+
console.log(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
76
|
+
},
|
|
77
|
+
warn: (...args) => {
|
|
78
|
+
console.warn(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
79
|
+
},
|
|
80
|
+
error: (...args) => {
|
|
81
|
+
console.error(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
]));
|
|
55
85
|
this.servicesLive = servicesEntries.map(() => new Future(() => {
|
|
56
86
|
}));
|
|
57
87
|
this.servicesDead = servicesEntries.map(() => new Future(() => {
|
|
58
88
|
}));
|
|
59
89
|
this.live.use(Promise.all(this.servicesLive));
|
|
60
90
|
this.dead.use(Promise.all(this.servicesDead));
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
this.persistentStateDir = resolve(flightdeckRootDir, `.state`, options.packageName);
|
|
92
|
+
if (!existsSync(this.persistentStateDir)) {
|
|
93
|
+
mkdirSync(this.persistentStateDir, { recursive: true });
|
|
94
|
+
}
|
|
64
95
|
createServer((req, res) => {
|
|
65
96
|
let data = [];
|
|
66
97
|
req.on(`data`, (chunk) => {
|
|
67
98
|
data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
|
|
68
99
|
}).on(`end`, () => {
|
|
69
|
-
console.log(req.headers);
|
|
70
100
|
const authHeader = req.headers.authorization;
|
|
71
101
|
try {
|
|
72
102
|
if (typeof req.url === `undefined`)
|
|
@@ -74,19 +104,28 @@ class FlightDeck {
|
|
|
74
104
|
if (authHeader !== `Bearer ${secret}`)
|
|
75
105
|
throw 401;
|
|
76
106
|
const url = new URL(req.url, ORIGIN);
|
|
77
|
-
|
|
107
|
+
this.logger.info(req.method, url.pathname);
|
|
78
108
|
switch (req.method) {
|
|
79
109
|
case `POST`:
|
|
80
110
|
{
|
|
81
|
-
console.log(`received post, url is ${url.pathname}`);
|
|
82
111
|
switch (url.pathname) {
|
|
83
112
|
case `/`:
|
|
84
113
|
{
|
|
85
114
|
res.writeHead(200);
|
|
86
115
|
res.end();
|
|
87
|
-
this.
|
|
116
|
+
const installFile = resolve(this.persistentStateDir, `install`);
|
|
117
|
+
const readyFile = resolve(this.persistentStateDir, `ready`);
|
|
118
|
+
if (!existsSync(installFile)) {
|
|
119
|
+
this.logger.info(`Install file does not exist yet. Creating...`);
|
|
120
|
+
writeFileSync(installFile, ``);
|
|
121
|
+
}
|
|
122
|
+
if (existsSync(readyFile)) {
|
|
123
|
+
this.logger.info(`Ready file exists. Removing...`);
|
|
124
|
+
rmSync(readyFile);
|
|
125
|
+
}
|
|
126
|
+
this.getLatestRelease();
|
|
88
127
|
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
89
|
-
|
|
128
|
+
this.logger.info(`All services are ready to update!`);
|
|
90
129
|
this.stopAllServices();
|
|
91
130
|
return;
|
|
92
131
|
}
|
|
@@ -111,7 +150,7 @@ class FlightDeck {
|
|
|
111
150
|
throw 405;
|
|
112
151
|
}
|
|
113
152
|
} catch (thrown) {
|
|
114
|
-
|
|
153
|
+
this.logger.error(thrown, req.url);
|
|
115
154
|
if (typeof thrown === `number`) {
|
|
116
155
|
res.writeHead(thrown);
|
|
117
156
|
res.end();
|
|
@@ -121,44 +160,44 @@ class FlightDeck {
|
|
|
121
160
|
}
|
|
122
161
|
});
|
|
123
162
|
}).listen(PORT, () => {
|
|
124
|
-
|
|
163
|
+
this.logger.info(`Server started on port ${PORT}`);
|
|
125
164
|
});
|
|
126
165
|
this.startAllServices();
|
|
127
166
|
}
|
|
128
167
|
startAllServices() {
|
|
129
|
-
|
|
168
|
+
this.logger.info(`Starting all services...`);
|
|
130
169
|
for (const [serviceName] of toEntries(this.services)) {
|
|
131
170
|
this.startService(serviceName);
|
|
132
171
|
}
|
|
133
172
|
}
|
|
134
173
|
startService(serviceName) {
|
|
135
|
-
|
|
136
|
-
if (this.safety
|
|
174
|
+
this.logger.info(`Starting service ${this.options.packageName}::${serviceName}, try ${this.safety}/2...`);
|
|
175
|
+
if (this.safety >= 2) {
|
|
137
176
|
throw new Error(`Out of tries...`);
|
|
138
177
|
}
|
|
139
178
|
this.safety++;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
179
|
+
const readyFile = resolve(this.persistentStateDir, `ready`);
|
|
180
|
+
if (!existsSync(readyFile)) {
|
|
181
|
+
this.logger.info(`Tried to start service but failed: could not find readyFile: ${readyFile}`);
|
|
182
|
+
this.getLatestRelease();
|
|
143
183
|
this.applyUpdate();
|
|
144
184
|
this.startService(serviceName);
|
|
145
185
|
return;
|
|
146
186
|
}
|
|
147
187
|
const [executable, ...args] = this.options.services[serviceName].run;
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
cwd: this.currentServiceDir,
|
|
188
|
+
const serviceProcess = spawn(executable, args, {
|
|
189
|
+
cwd: this.options.flightdeckRootDir,
|
|
151
190
|
env: import.meta.env
|
|
152
191
|
});
|
|
153
192
|
this.services[serviceName] = new ChildSocket(serviceProcess, `${this.options.packageName}::${serviceName}`, console);
|
|
154
193
|
this.services[serviceName].onAny((...messages) => {
|
|
155
|
-
|
|
194
|
+
this.logger.info(`\uD83D\uDCAC`, ...messages);
|
|
156
195
|
});
|
|
157
196
|
this.services[serviceName].on(`readyToUpdate`, () => {
|
|
158
|
-
|
|
197
|
+
this.serviceLoggers[serviceName].info(`Ready to update.`);
|
|
159
198
|
this.servicesReadyToUpdate[serviceName] = true;
|
|
160
199
|
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
161
|
-
|
|
200
|
+
this.logger.info(`All services are ready to update.`);
|
|
162
201
|
this.stopAllServices();
|
|
163
202
|
}
|
|
164
203
|
});
|
|
@@ -173,15 +212,16 @@ class FlightDeck {
|
|
|
173
212
|
this.dead.use(Promise.all(this.servicesDead));
|
|
174
213
|
});
|
|
175
214
|
this.services[serviceName].process.on(`close`, (exitCode) => {
|
|
176
|
-
|
|
215
|
+
this.serviceLoggers[serviceName].info(`Exited with code ${exitCode}`);
|
|
177
216
|
this.services[serviceName] = null;
|
|
178
217
|
if (!this.servicesShouldRestart) {
|
|
179
|
-
|
|
218
|
+
this.serviceLoggers[serviceName].info(`Will not be restarted.`);
|
|
180
219
|
return;
|
|
181
220
|
}
|
|
182
|
-
const
|
|
221
|
+
const installFile = resolve(this.persistentStateDir, `install`);
|
|
222
|
+
const updatesAreReady = existsSync(installFile);
|
|
183
223
|
if (updatesAreReady) {
|
|
184
|
-
|
|
224
|
+
this.serviceLoggers[serviceName].info(`Updating before startup...`);
|
|
185
225
|
this.restartTimes = [];
|
|
186
226
|
this.applyUpdate();
|
|
187
227
|
this.startService(serviceName);
|
|
@@ -191,58 +231,54 @@ class FlightDeck {
|
|
|
191
231
|
this.restartTimes = this.restartTimes.filter((time) => time > fiveMinutesAgo);
|
|
192
232
|
this.restartTimes.push(now);
|
|
193
233
|
if (this.restartTimes.length < 5) {
|
|
194
|
-
|
|
234
|
+
this.serviceLoggers[serviceName].info(`Crashed. Restarting...`);
|
|
195
235
|
this.startService(serviceName);
|
|
196
236
|
} else {
|
|
197
|
-
|
|
237
|
+
this.serviceLoggers[serviceName].info(`Crashed 5 times in 5 minutes. Not restarting.`);
|
|
198
238
|
}
|
|
199
239
|
}
|
|
200
240
|
});
|
|
201
241
|
this.safety = 0;
|
|
202
242
|
}
|
|
203
243
|
applyUpdate() {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
244
|
+
this.logger.info(`Installing...`);
|
|
245
|
+
try {
|
|
246
|
+
execSync(this.options.scripts.install);
|
|
247
|
+
const installFile = resolve(this.persistentStateDir, `install`);
|
|
248
|
+
if (existsSync(installFile)) {
|
|
249
|
+
rmSync(installFile);
|
|
210
250
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
renameSync(this.currentServiceDir, this.backupServiceDir);
|
|
251
|
+
const readyFile = resolve(this.persistentStateDir, `ready`);
|
|
252
|
+
writeFileSync(readyFile, ``);
|
|
253
|
+
this.logger.info(`Installed!`);
|
|
254
|
+
} catch (thrown) {
|
|
255
|
+
if (thrown instanceof Error) {
|
|
256
|
+
this.logger.error(`Failed to get the latest release: ${thrown.message}`);
|
|
218
257
|
}
|
|
219
|
-
|
|
220
|
-
this.restartTimes = [];
|
|
221
|
-
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate };
|
|
222
|
-
} else {
|
|
223
|
-
console.log(`Service ${this.options.packageName} is already up to date.`);
|
|
258
|
+
return;
|
|
224
259
|
}
|
|
225
260
|
}
|
|
226
|
-
|
|
227
|
-
|
|
261
|
+
getLatestRelease() {
|
|
262
|
+
this.logger.info(`Downloading...`);
|
|
228
263
|
try {
|
|
229
|
-
execSync(this.options.
|
|
264
|
+
execSync(this.options.scripts.download);
|
|
265
|
+
this.logger.info(`Downloaded!`);
|
|
230
266
|
} catch (thrown) {
|
|
231
267
|
if (thrown instanceof Error) {
|
|
232
|
-
|
|
268
|
+
this.logger.error(`Failed to get the latest release: ${thrown.message}`);
|
|
233
269
|
}
|
|
234
270
|
return;
|
|
235
271
|
}
|
|
236
272
|
}
|
|
237
273
|
stopAllServices() {
|
|
238
|
-
|
|
274
|
+
this.logger.info(`Stopping all services...`);
|
|
239
275
|
for (const [serviceName] of toEntries(this.services)) {
|
|
240
276
|
this.stopService(serviceName);
|
|
241
277
|
}
|
|
242
278
|
}
|
|
243
279
|
stopService(serviceName) {
|
|
244
280
|
if (this.services[serviceName]) {
|
|
245
|
-
|
|
281
|
+
this.serviceLoggers[serviceName].info(`Stopping service...`);
|
|
246
282
|
this.services[serviceName].process.kill();
|
|
247
283
|
this.services[serviceName] = null;
|
|
248
284
|
this.servicesDead[this.serviceIdx[serviceName]].use(Promise.resolve());
|
|
@@ -254,10 +290,11 @@ class FlightDeck {
|
|
|
254
290
|
}
|
|
255
291
|
this.live.use(Promise.all(this.servicesLive));
|
|
256
292
|
} else {
|
|
257
|
-
|
|
293
|
+
this.serviceLoggers[serviceName].error(`Tried to stop service, but it wasn't running.`);
|
|
258
294
|
}
|
|
259
295
|
}
|
|
260
296
|
shutdown() {
|
|
297
|
+
this.logger.info(`Shutting down...`);
|
|
261
298
|
this.servicesShouldRestart = false;
|
|
262
299
|
this.stopAllServices();
|
|
263
300
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flightdeck",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jeremy Banka",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@types/tmp": "0.2.6",
|
|
32
32
|
"concurrently": "9.0.1",
|
|
33
33
|
"tmp": "0.2.3",
|
|
34
|
-
"tsup": "8.
|
|
34
|
+
"tsup": "8.3.0",
|
|
35
35
|
"rimraf": "6.0.1",
|
|
36
36
|
"vitest": "2.1.1"
|
|
37
37
|
},
|
package/src/flightdeck.bin.ts
CHANGED
|
@@ -6,8 +6,8 @@ import type { OptionsGroup } from "comline"
|
|
|
6
6
|
import { cli, optional, parseArrayOption } from "comline"
|
|
7
7
|
import { z } from "zod"
|
|
8
8
|
|
|
9
|
-
import type { FlightDeckOptions } from "
|
|
10
|
-
import { FlightDeck } from "
|
|
9
|
+
import type { FlightDeckOptions } from "./flightdeck.lib"
|
|
10
|
+
import { FlightDeck } from "./flightdeck.lib"
|
|
11
11
|
|
|
12
12
|
const FLIGHTDECK_MANUAL = {
|
|
13
13
|
optionsSchema: z.object({
|
|
@@ -16,8 +16,11 @@ const FLIGHTDECK_MANUAL = {
|
|
|
16
16
|
services: z.record(
|
|
17
17
|
z.object({ run: z.array(z.string()), waitFor: z.boolean() }),
|
|
18
18
|
),
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
flightdeckRootDir: z.string(),
|
|
20
|
+
scripts: z.object({
|
|
21
|
+
download: z.string(),
|
|
22
|
+
install: z.string(),
|
|
23
|
+
}),
|
|
21
24
|
}),
|
|
22
25
|
options: {
|
|
23
26
|
secret: {
|
|
@@ -45,12 +48,12 @@ const FLIGHTDECK_MANUAL = {
|
|
|
45
48
|
description: `Directory where the service is stored.`,
|
|
46
49
|
example: `--flightdeckRootDir=\"./services/sample/repo/my-app/current\"`,
|
|
47
50
|
},
|
|
48
|
-
|
|
49
|
-
flag: `
|
|
51
|
+
scripts: {
|
|
52
|
+
flag: `r`,
|
|
50
53
|
required: true,
|
|
51
|
-
description: `
|
|
52
|
-
example: `--
|
|
53
|
-
parse:
|
|
54
|
+
description: `Map of scripts to run.`,
|
|
55
|
+
example: `--scripts="{\\"download\\":\\"npm i",\\"install\\":\\"npm run build\\"}"`,
|
|
56
|
+
parse: JSON.parse,
|
|
54
57
|
},
|
|
55
58
|
},
|
|
56
59
|
} satisfies OptionsGroup<FlightDeckOptions>
|
package/src/flightdeck.lib.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { execSync, spawn } from "node:child_process"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs"
|
|
3
9
|
import type { Server } from "node:http"
|
|
4
10
|
import { createServer } from "node:http"
|
|
5
11
|
import { homedir } from "node:os"
|
|
@@ -13,7 +19,10 @@ export type FlightDeckOptions<S extends string = string> = {
|
|
|
13
19
|
secret: string
|
|
14
20
|
packageName: string
|
|
15
21
|
services: { [service in S]: { run: string[]; waitFor: boolean } }
|
|
16
|
-
|
|
22
|
+
scripts: {
|
|
23
|
+
download: string
|
|
24
|
+
install: string
|
|
25
|
+
}
|
|
17
26
|
flightdeckRootDir?: string | undefined
|
|
18
27
|
}
|
|
19
28
|
|
|
@@ -34,16 +43,19 @@ export class FlightDeck<S extends string = string> {
|
|
|
34
43
|
public servicesReadyToUpdate: { [service in S]: boolean }
|
|
35
44
|
public servicesShouldRestart: boolean
|
|
36
45
|
|
|
37
|
-
protected
|
|
46
|
+
protected logger: Pick<Console, `error` | `info` | `warn`>
|
|
47
|
+
protected serviceLoggers: {
|
|
48
|
+
readonly [service in S]: Pick<Console, `error` | `info` | `warn`>
|
|
49
|
+
}
|
|
38
50
|
|
|
39
51
|
public servicesLive: Future<void>[]
|
|
40
52
|
public servicesDead: Future<void>[]
|
|
41
53
|
public live = new Future(() => {})
|
|
42
54
|
public dead = new Future(() => {})
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
protected restartTimes: number[] = []
|
|
57
|
+
|
|
58
|
+
protected persistentStateDir: string
|
|
47
59
|
|
|
48
60
|
public constructor(public readonly options: FlightDeckOptions<S>) {
|
|
49
61
|
const { secret, flightdeckRootDir = resolve(homedir(), `services`) } =
|
|
@@ -64,26 +76,51 @@ export class FlightDeck<S extends string = string> {
|
|
|
64
76
|
)
|
|
65
77
|
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate }
|
|
66
78
|
this.servicesShouldRestart = true
|
|
79
|
+
|
|
80
|
+
this.logger = {
|
|
81
|
+
info: (...args: any[]) => {
|
|
82
|
+
console.log(`${this.options.packageName}:`, ...args)
|
|
83
|
+
},
|
|
84
|
+
warn: (...args: any[]) => {
|
|
85
|
+
console.warn(`${this.options.packageName}:`, ...args)
|
|
86
|
+
},
|
|
87
|
+
error: (...args: any[]) => {
|
|
88
|
+
console.error(`${this.options.packageName}:`, ...args)
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
this.serviceLoggers = fromEntries(
|
|
92
|
+
servicesEntries.map(([serviceName]) => [
|
|
93
|
+
serviceName,
|
|
94
|
+
{
|
|
95
|
+
info: (...args: any[]) => {
|
|
96
|
+
console.log(`${this.options.packageName}::${serviceName}:`, ...args)
|
|
97
|
+
},
|
|
98
|
+
warn: (...args: any[]) => {
|
|
99
|
+
console.warn(`${this.options.packageName}::${serviceName}:`, ...args)
|
|
100
|
+
},
|
|
101
|
+
error: (...args: any[]) => {
|
|
102
|
+
console.error(
|
|
103
|
+
`${this.options.packageName}::${serviceName}:`,
|
|
104
|
+
...args,
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
]),
|
|
109
|
+
)
|
|
110
|
+
|
|
67
111
|
this.servicesLive = servicesEntries.map(() => new Future(() => {}))
|
|
68
112
|
this.servicesDead = servicesEntries.map(() => new Future(() => {}))
|
|
69
113
|
this.live.use(Promise.all(this.servicesLive))
|
|
70
114
|
this.dead.use(Promise.all(this.servicesDead))
|
|
71
115
|
|
|
72
|
-
this.
|
|
116
|
+
this.persistentStateDir = resolve(
|
|
73
117
|
flightdeckRootDir,
|
|
118
|
+
`.state`,
|
|
74
119
|
options.packageName,
|
|
75
|
-
`current`,
|
|
76
|
-
)
|
|
77
|
-
this.backupServiceDir = resolve(
|
|
78
|
-
flightdeckRootDir,
|
|
79
|
-
options.packageName,
|
|
80
|
-
`backup`,
|
|
81
|
-
)
|
|
82
|
-
this.updateServiceDir = resolve(
|
|
83
|
-
flightdeckRootDir,
|
|
84
|
-
options.packageName,
|
|
85
|
-
`update`,
|
|
86
120
|
)
|
|
121
|
+
if (!existsSync(this.persistentStateDir)) {
|
|
122
|
+
mkdirSync(this.persistentStateDir, { recursive: true })
|
|
123
|
+
}
|
|
87
124
|
|
|
88
125
|
createServer((req, res) => {
|
|
89
126
|
let data: Uint8Array[] = []
|
|
@@ -92,29 +129,45 @@ export class FlightDeck<S extends string = string> {
|
|
|
92
129
|
data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk))
|
|
93
130
|
})
|
|
94
131
|
.on(`end`, () => {
|
|
95
|
-
console.log(req.headers)
|
|
96
132
|
const authHeader = req.headers.authorization
|
|
97
133
|
try {
|
|
98
134
|
if (typeof req.url === `undefined`) throw 400
|
|
99
135
|
if (authHeader !== `Bearer ${secret}`) throw 401
|
|
100
136
|
const url = new URL(req.url, ORIGIN)
|
|
101
|
-
|
|
137
|
+
this.logger.info(req.method, url.pathname)
|
|
102
138
|
switch (req.method) {
|
|
103
139
|
case `POST`:
|
|
104
140
|
{
|
|
105
|
-
console.log(`received post, url is ${url.pathname}`)
|
|
106
141
|
switch (url.pathname) {
|
|
107
142
|
case `/`:
|
|
108
143
|
{
|
|
109
144
|
res.writeHead(200)
|
|
110
145
|
res.end()
|
|
111
|
-
|
|
146
|
+
const installFile = resolve(
|
|
147
|
+
this.persistentStateDir,
|
|
148
|
+
`install`,
|
|
149
|
+
)
|
|
150
|
+
const readyFile = resolve(
|
|
151
|
+
this.persistentStateDir,
|
|
152
|
+
`ready`,
|
|
153
|
+
)
|
|
154
|
+
if (!existsSync(installFile)) {
|
|
155
|
+
this.logger.info(
|
|
156
|
+
`Install file does not exist yet. Creating...`,
|
|
157
|
+
)
|
|
158
|
+
writeFileSync(installFile, ``)
|
|
159
|
+
}
|
|
160
|
+
if (existsSync(readyFile)) {
|
|
161
|
+
this.logger.info(`Ready file exists. Removing...`)
|
|
162
|
+
rmSync(readyFile)
|
|
163
|
+
}
|
|
164
|
+
this.getLatestRelease()
|
|
112
165
|
if (
|
|
113
166
|
toEntries(this.servicesReadyToUpdate).every(
|
|
114
167
|
([, isReady]) => isReady,
|
|
115
168
|
)
|
|
116
169
|
) {
|
|
117
|
-
|
|
170
|
+
this.logger.info(`All services are ready to update!`)
|
|
118
171
|
this.stopAllServices()
|
|
119
172
|
return
|
|
120
173
|
}
|
|
@@ -141,7 +194,7 @@ export class FlightDeck<S extends string = string> {
|
|
|
141
194
|
throw 405
|
|
142
195
|
}
|
|
143
196
|
} catch (thrown) {
|
|
144
|
-
|
|
197
|
+
this.logger.error(thrown, req.url)
|
|
145
198
|
if (typeof thrown === `number`) {
|
|
146
199
|
res.writeHead(thrown)
|
|
147
200
|
res.end()
|
|
@@ -151,32 +204,33 @@ export class FlightDeck<S extends string = string> {
|
|
|
151
204
|
}
|
|
152
205
|
})
|
|
153
206
|
}).listen(PORT, () => {
|
|
154
|
-
|
|
207
|
+
this.logger.info(`Server started on port ${PORT}`)
|
|
155
208
|
})
|
|
156
209
|
|
|
157
210
|
this.startAllServices()
|
|
158
211
|
}
|
|
159
212
|
|
|
160
213
|
protected startAllServices(): void {
|
|
161
|
-
|
|
214
|
+
this.logger.info(`Starting all services...`)
|
|
162
215
|
for (const [serviceName] of toEntries(this.services)) {
|
|
163
216
|
this.startService(serviceName)
|
|
164
217
|
}
|
|
165
218
|
}
|
|
166
219
|
|
|
167
220
|
protected startService(serviceName: S): void {
|
|
168
|
-
|
|
221
|
+
this.logger.info(
|
|
169
222
|
`Starting service ${this.options.packageName}::${serviceName}, try ${this.safety}/2...`,
|
|
170
223
|
)
|
|
171
|
-
if (this.safety
|
|
224
|
+
if (this.safety >= 2) {
|
|
172
225
|
throw new Error(`Out of tries...`)
|
|
173
226
|
}
|
|
174
227
|
this.safety++
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
228
|
+
const readyFile = resolve(this.persistentStateDir, `ready`)
|
|
229
|
+
if (!existsSync(readyFile)) {
|
|
230
|
+
this.logger.info(
|
|
231
|
+
`Tried to start service but failed: could not find readyFile: ${readyFile}`,
|
|
178
232
|
)
|
|
179
|
-
this.
|
|
233
|
+
this.getLatestRelease()
|
|
180
234
|
this.applyUpdate()
|
|
181
235
|
this.startService(serviceName)
|
|
182
236
|
|
|
@@ -184,11 +238,8 @@ export class FlightDeck<S extends string = string> {
|
|
|
184
238
|
}
|
|
185
239
|
|
|
186
240
|
const [executable, ...args] = this.options.services[serviceName].run
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
: executable
|
|
190
|
-
const serviceProcess = spawn(program, args, {
|
|
191
|
-
cwd: this.currentServiceDir,
|
|
241
|
+
const serviceProcess = spawn(executable, args, {
|
|
242
|
+
cwd: this.options.flightdeckRootDir,
|
|
192
243
|
env: import.meta.env,
|
|
193
244
|
})
|
|
194
245
|
this.services[serviceName] = new ChildSocket(
|
|
@@ -197,17 +248,15 @@ export class FlightDeck<S extends string = string> {
|
|
|
197
248
|
console,
|
|
198
249
|
)
|
|
199
250
|
this.services[serviceName].onAny((...messages) => {
|
|
200
|
-
|
|
251
|
+
this.logger.info(`💬`, ...messages)
|
|
201
252
|
})
|
|
202
253
|
this.services[serviceName].on(`readyToUpdate`, () => {
|
|
203
|
-
|
|
204
|
-
`Service ${this.options.packageName}::${serviceName} is ready to update.`,
|
|
205
|
-
)
|
|
254
|
+
this.serviceLoggers[serviceName].info(`Ready to update.`)
|
|
206
255
|
this.servicesReadyToUpdate[serviceName] = true
|
|
207
256
|
if (
|
|
208
257
|
toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)
|
|
209
258
|
) {
|
|
210
|
-
|
|
259
|
+
this.logger.info(`All services are ready to update.`)
|
|
211
260
|
this.stopAllServices()
|
|
212
261
|
}
|
|
213
262
|
})
|
|
@@ -220,21 +269,16 @@ export class FlightDeck<S extends string = string> {
|
|
|
220
269
|
this.dead.use(Promise.all(this.servicesDead))
|
|
221
270
|
})
|
|
222
271
|
this.services[serviceName].process.on(`close`, (exitCode) => {
|
|
223
|
-
|
|
224
|
-
`${this.options.packageName}::${serviceName} exited with code ${exitCode}`,
|
|
225
|
-
)
|
|
272
|
+
this.serviceLoggers[serviceName].info(`Exited with code ${exitCode}`)
|
|
226
273
|
this.services[serviceName] = null
|
|
227
274
|
if (!this.servicesShouldRestart) {
|
|
228
|
-
|
|
229
|
-
`Service ${this.options.packageName}::${serviceName} will not be restarted.`,
|
|
230
|
-
)
|
|
275
|
+
this.serviceLoggers[serviceName].info(`Will not be restarted.`)
|
|
231
276
|
return
|
|
232
277
|
}
|
|
233
|
-
const
|
|
278
|
+
const installFile = resolve(this.persistentStateDir, `install`)
|
|
279
|
+
const updatesAreReady = existsSync(installFile)
|
|
234
280
|
if (updatesAreReady) {
|
|
235
|
-
|
|
236
|
-
`${this.options.packageName}::${serviceName} will be updated before startup...`,
|
|
237
|
-
)
|
|
281
|
+
this.serviceLoggers[serviceName].info(`Updating before startup...`)
|
|
238
282
|
this.restartTimes = []
|
|
239
283
|
this.applyUpdate()
|
|
240
284
|
this.startService(serviceName)
|
|
@@ -247,13 +291,11 @@ export class FlightDeck<S extends string = string> {
|
|
|
247
291
|
this.restartTimes.push(now)
|
|
248
292
|
|
|
249
293
|
if (this.restartTimes.length < 5) {
|
|
250
|
-
|
|
251
|
-
`Service ${this.options.packageName}::${serviceName} crashed. Restarting...`,
|
|
252
|
-
)
|
|
294
|
+
this.serviceLoggers[serviceName].info(`Crashed. Restarting...`)
|
|
253
295
|
this.startService(serviceName)
|
|
254
296
|
} else {
|
|
255
|
-
|
|
256
|
-
`
|
|
297
|
+
this.serviceLoggers[serviceName].info(
|
|
298
|
+
`Crashed 5 times in 5 minutes. Not restarting.`,
|
|
257
299
|
)
|
|
258
300
|
}
|
|
259
301
|
}
|
|
@@ -262,55 +304,41 @@ export class FlightDeck<S extends string = string> {
|
|
|
262
304
|
}
|
|
263
305
|
|
|
264
306
|
protected applyUpdate(): void {
|
|
265
|
-
|
|
266
|
-
`Installing latest version of service ${this.options.packageName}...`,
|
|
267
|
-
)
|
|
307
|
+
this.logger.info(`Installing...`)
|
|
268
308
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
console.log(
|
|
275
|
-
`Tried to apply update to ${this.options.packageName} but failed. The following services are currently running: [${runningServices.map(([serviceName]) => serviceName).join(`, `)}]`,
|
|
276
|
-
)
|
|
277
|
-
return
|
|
309
|
+
try {
|
|
310
|
+
execSync(this.options.scripts.install)
|
|
311
|
+
const installFile = resolve(this.persistentStateDir, `install`)
|
|
312
|
+
if (existsSync(installFile)) {
|
|
313
|
+
rmSync(installFile)
|
|
278
314
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
renameSync(this.currentServiceDir, this.backupServiceDir)
|
|
315
|
+
const readyFile = resolve(this.persistentStateDir, `ready`)
|
|
316
|
+
writeFileSync(readyFile, ``)
|
|
317
|
+
this.logger.info(`Installed!`)
|
|
318
|
+
} catch (thrown) {
|
|
319
|
+
if (thrown instanceof Error) {
|
|
320
|
+
this.logger.error(`Failed to get the latest release: ${thrown.message}`)
|
|
287
321
|
}
|
|
288
|
-
|
|
289
|
-
renameSync(this.updateServiceDir, this.currentServiceDir)
|
|
290
|
-
this.restartTimes = []
|
|
291
|
-
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate }
|
|
292
|
-
} else {
|
|
293
|
-
console.log(`Service ${this.options.packageName} is already up to date.`)
|
|
322
|
+
return
|
|
294
323
|
}
|
|
295
324
|
}
|
|
296
325
|
|
|
297
|
-
protected
|
|
298
|
-
|
|
299
|
-
`Downloading latest version of service ${this.options.packageName}...`,
|
|
300
|
-
)
|
|
326
|
+
protected getLatestRelease(): void {
|
|
327
|
+
this.logger.info(`Downloading...`)
|
|
301
328
|
|
|
302
329
|
try {
|
|
303
|
-
execSync(this.options.
|
|
330
|
+
execSync(this.options.scripts.download)
|
|
331
|
+
this.logger.info(`Downloaded!`)
|
|
304
332
|
} catch (thrown) {
|
|
305
333
|
if (thrown instanceof Error) {
|
|
306
|
-
|
|
334
|
+
this.logger.error(`Failed to get the latest release: ${thrown.message}`)
|
|
307
335
|
}
|
|
308
336
|
return
|
|
309
337
|
}
|
|
310
338
|
}
|
|
311
339
|
|
|
312
340
|
public stopAllServices(): void {
|
|
313
|
-
|
|
341
|
+
this.logger.info(`Stopping all services...`)
|
|
314
342
|
for (const [serviceName] of toEntries(this.services)) {
|
|
315
343
|
this.stopService(serviceName)
|
|
316
344
|
}
|
|
@@ -318,9 +346,7 @@ export class FlightDeck<S extends string = string> {
|
|
|
318
346
|
|
|
319
347
|
public stopService(serviceName: S): void {
|
|
320
348
|
if (this.services[serviceName]) {
|
|
321
|
-
|
|
322
|
-
`Stopping service ${this.options.packageName}::${serviceName}...`,
|
|
323
|
-
)
|
|
349
|
+
this.serviceLoggers[serviceName].info(`Stopping service...`)
|
|
324
350
|
this.services[serviceName].process.kill()
|
|
325
351
|
this.services[serviceName] = null
|
|
326
352
|
this.servicesDead[this.serviceIdx[serviceName]].use(Promise.resolve())
|
|
@@ -330,13 +356,14 @@ export class FlightDeck<S extends string = string> {
|
|
|
330
356
|
}
|
|
331
357
|
this.live.use(Promise.all(this.servicesLive))
|
|
332
358
|
} else {
|
|
333
|
-
|
|
334
|
-
`
|
|
359
|
+
this.serviceLoggers[serviceName].error(
|
|
360
|
+
`Tried to stop service, but it wasn't running.`,
|
|
335
361
|
)
|
|
336
362
|
}
|
|
337
363
|
}
|
|
338
364
|
|
|
339
365
|
public shutdown(): void {
|
|
366
|
+
this.logger.info(`Shutting down...`)
|
|
340
367
|
this.servicesShouldRestart = false
|
|
341
368
|
this.stopAllServices()
|
|
342
369
|
}
|