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.
@@ -29,22 +29,32 @@
29
29
  "additionalProperties": false
30
30
  }
31
31
  },
32
- "flightDeckRootDir": {
32
+ "flightdeckRootDir": {
33
33
  "type": "string"
34
34
  },
35
- "downloadPackageToUpdatesCmd": {
36
- "type": "array",
37
- "items": {
38
- "type": "string"
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
- "flightDeckRootDir",
47
- "downloadPackageToUpdatesCmd"
56
+ "flightdeckRootDir",
57
+ "scripts"
48
58
  ],
49
59
  "additionalProperties": false,
50
60
  "$schema": "http://json-schema.org/draft-07/schema#"
@@ -2,12 +2,17 @@
2
2
 
3
3
  // src/flightdeck.bin.ts
4
4
  import * as path from "node:path";
5
- import {cli, optional, parseArrayOption} from "comline";
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 {existsSync, mkdirSync, renameSync, rmSync} from "node:fs";
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
- restartTimes = [];
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
- currentServiceDir;
37
- updateServiceDir;
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.currentServiceDir = resolve(flightdeckRootDir, options.packageName, `current`);
58
- this.backupServiceDir = resolve(flightdeckRootDir, options.packageName, `backup`);
59
- this.updateServiceDir = resolve(flightdeckRootDir, options.packageName, `update`);
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
- console.log(req.method, url.pathname);
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.fetchLatestRelease();
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
- console.log(`All services are ready to update!`);
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
- console.error(thrown, req.url);
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
- console.log(`Server started on port ${PORT}`);
159
+ this.logger.info(`Server started on port ${PORT}`);
121
160
  });
122
161
  this.startAllServices();
123
162
  }
124
163
  startAllServices() {
125
- console.log(`Starting all services...`);
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
- console.log(`Starting service ${this.options.packageName}::${serviceName}, try ${this.safety}/2...`);
132
- if (this.safety > 2) {
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
- if (!existsSync(this.currentServiceDir)) {
137
- console.log(`Tried to start service but failed: Service ${this.options.packageName} is not yet installed.`);
138
- this.fetchLatestRelease();
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 program = executable.startsWith(`./`) ? resolve(this.currentServiceDir, executable) : executable;
145
- const serviceProcess = spawn(program, args, {
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
- console.log(`${this.options.packageName}::${serviceName} \uD83D\uDCAC`, ...messages);
190
+ this.logger.info(`\uD83D\uDCAC`, ...messages);
152
191
  });
153
192
  this.services[serviceName].on(`readyToUpdate`, () => {
154
- console.log(`Service ${this.options.packageName}::${serviceName} is ready to update.`);
193
+ this.serviceLoggers[serviceName].info(`Ready to update.`);
155
194
  this.servicesReadyToUpdate[serviceName] = true;
156
195
  if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
157
- console.log(`All services are ready to update!`);
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
- console.log(`${this.options.packageName}::${serviceName} exited with code ${exitCode}`);
211
+ this.serviceLoggers[serviceName].info(`Exited with code ${exitCode}`);
173
212
  this.services[serviceName] = null;
174
213
  if (!this.servicesShouldRestart) {
175
- console.log(`Service ${this.options.packageName}::${serviceName} will not be restarted.`);
214
+ this.serviceLoggers[serviceName].info(`Will not be restarted.`);
176
215
  return;
177
216
  }
178
- const updatesAreReady = existsSync(this.updateServiceDir);
217
+ const installFile = resolve(this.persistentStateDir, `install`);
218
+ const updatesAreReady = existsSync(installFile);
179
219
  if (updatesAreReady) {
180
- console.log(`${this.options.packageName}::${serviceName} will be updated before startup...`);
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
- console.log(`Service ${this.options.packageName}::${serviceName} crashed. Restarting...`);
230
+ this.serviceLoggers[serviceName].info(`Crashed. Restarting...`);
191
231
  this.startService(serviceName);
192
232
  } else {
193
- console.log(`Service ${this.options.packageName}::${serviceName} crashed too many times. Not restarting.`);
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
- console.log(`Installing latest version of service ${this.options.packageName}...`);
201
- if (existsSync(this.updateServiceDir)) {
202
- const runningServices = toEntries(this.services).filter(([, service]) => service);
203
- if (runningServices.length > 0) {
204
- console.log(`Tried to apply update to ${this.options.packageName} but failed. The following services are currently running: [${runningServices.map(([serviceName]) => serviceName).join(`, `)}]`);
205
- return;
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
- if (existsSync(this.currentServiceDir)) {
208
- if (!existsSync(this.backupServiceDir)) {
209
- mkdirSync(this.backupServiceDir, { recursive: true });
210
- } else {
211
- rmSync(this.backupServiceDir, { recursive: true });
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
- renameSync(this.updateServiceDir, this.currentServiceDir);
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
- fetchLatestRelease() {
223
- console.log(`Downloading latest version of service ${this.options.packageName}...`);
257
+ getLatestRelease() {
258
+ this.logger.info(`Downloading...`);
224
259
  try {
225
- execSync(this.options.downloadPackageToUpdatesCmd.join(` `));
260
+ execSync(this.options.scripts.download);
261
+ this.logger.info(`Downloaded!`);
226
262
  } catch (thrown) {
227
263
  if (thrown instanceof Error) {
228
- console.error(`Failed to fetch the latest release: ${thrown.message}`);
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
- console.log(`Stopping all services...`);
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
- console.log(`Stopping service ${this.options.packageName}::${serviceName}...`);
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
- console.error(`Failed to stop service ${this.options.packageName}::${serviceName}: Service is not running.`);
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
- flightDeckRootDir: z.string(),
269
- downloadPackageToUpdatesCmd: z.array(z.string())
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
- downloadPackageToUpdatesCmd: {
298
- flag: `u`,
337
+ scripts: {
338
+ flag: `r`,
299
339
  required: true,
300
- description: `Command to update the service.`,
301
- example: `--downloadPackageToUpdatesCmd=\"./app\"`,
302
- parse: parseArrayOption
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
- "flightDeckRootDir": {
32
+ "flightdeckRootDir": {
33
33
  "type": "string"
34
34
  },
35
- "downloadPackageToUpdatesCmd": {
36
- "type": "array",
37
- "items": {
38
- "type": "string"
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
- "flightDeckRootDir",
47
- "downloadPackageToUpdatesCmd"
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
- downloadPackageToUpdatesCmd: string[];
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 restartTimes: number[];
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
- readonly currentServiceDir: string;
45
- readonly updateServiceDir: string;
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 fetchLatestRelease(): void;
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 {existsSync, mkdirSync, renameSync, rmSync} from "node:fs";
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
- restartTimes = [];
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
- currentServiceDir;
41
- updateServiceDir;
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.currentServiceDir = resolve(flightdeckRootDir, options.packageName, `current`);
62
- this.backupServiceDir = resolve(flightdeckRootDir, options.packageName, `backup`);
63
- this.updateServiceDir = resolve(flightdeckRootDir, options.packageName, `update`);
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
- console.log(req.method, url.pathname);
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.fetchLatestRelease();
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
- console.log(`All services are ready to update!`);
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
- console.error(thrown, req.url);
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
- console.log(`Server started on port ${PORT}`);
163
+ this.logger.info(`Server started on port ${PORT}`);
125
164
  });
126
165
  this.startAllServices();
127
166
  }
128
167
  startAllServices() {
129
- console.log(`Starting all services...`);
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
- console.log(`Starting service ${this.options.packageName}::${serviceName}, try ${this.safety}/2...`);
136
- if (this.safety > 2) {
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
- if (!existsSync(this.currentServiceDir)) {
141
- console.log(`Tried to start service but failed: Service ${this.options.packageName} is not yet installed.`);
142
- this.fetchLatestRelease();
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 program = executable.startsWith(`./`) ? resolve(this.currentServiceDir, executable) : executable;
149
- const serviceProcess = spawn(program, args, {
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
- console.log(`${this.options.packageName}::${serviceName} \uD83D\uDCAC`, ...messages);
194
+ this.logger.info(`\uD83D\uDCAC`, ...messages);
156
195
  });
157
196
  this.services[serviceName].on(`readyToUpdate`, () => {
158
- console.log(`Service ${this.options.packageName}::${serviceName} is ready to update.`);
197
+ this.serviceLoggers[serviceName].info(`Ready to update.`);
159
198
  this.servicesReadyToUpdate[serviceName] = true;
160
199
  if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
161
- console.log(`All services are ready to update!`);
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
- console.log(`${this.options.packageName}::${serviceName} exited with code ${exitCode}`);
215
+ this.serviceLoggers[serviceName].info(`Exited with code ${exitCode}`);
177
216
  this.services[serviceName] = null;
178
217
  if (!this.servicesShouldRestart) {
179
- console.log(`Service ${this.options.packageName}::${serviceName} will not be restarted.`);
218
+ this.serviceLoggers[serviceName].info(`Will not be restarted.`);
180
219
  return;
181
220
  }
182
- const updatesAreReady = existsSync(this.updateServiceDir);
221
+ const installFile = resolve(this.persistentStateDir, `install`);
222
+ const updatesAreReady = existsSync(installFile);
183
223
  if (updatesAreReady) {
184
- console.log(`${this.options.packageName}::${serviceName} will be updated before startup...`);
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
- console.log(`Service ${this.options.packageName}::${serviceName} crashed. Restarting...`);
234
+ this.serviceLoggers[serviceName].info(`Crashed. Restarting...`);
195
235
  this.startService(serviceName);
196
236
  } else {
197
- console.log(`Service ${this.options.packageName}::${serviceName} crashed too many times. Not restarting.`);
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
- console.log(`Installing latest version of service ${this.options.packageName}...`);
205
- if (existsSync(this.updateServiceDir)) {
206
- const runningServices = toEntries(this.services).filter(([, service]) => service);
207
- if (runningServices.length > 0) {
208
- console.log(`Tried to apply update to ${this.options.packageName} but failed. The following services are currently running: [${runningServices.map(([serviceName]) => serviceName).join(`, `)}]`);
209
- return;
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
- if (existsSync(this.currentServiceDir)) {
212
- if (!existsSync(this.backupServiceDir)) {
213
- mkdirSync(this.backupServiceDir, { recursive: true });
214
- } else {
215
- rmSync(this.backupServiceDir, { recursive: true });
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
- renameSync(this.updateServiceDir, this.currentServiceDir);
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
- fetchLatestRelease() {
227
- console.log(`Downloading latest version of service ${this.options.packageName}...`);
261
+ getLatestRelease() {
262
+ this.logger.info(`Downloading...`);
228
263
  try {
229
- execSync(this.options.downloadPackageToUpdatesCmd.join(` `));
264
+ execSync(this.options.scripts.download);
265
+ this.logger.info(`Downloaded!`);
230
266
  } catch (thrown) {
231
267
  if (thrown instanceof Error) {
232
- console.error(`Failed to fetch the latest release: ${thrown.message}`);
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
- console.log(`Stopping all services...`);
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
- console.log(`Stopping service ${this.options.packageName}::${serviceName}...`);
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
- console.error(`Failed to stop service ${this.options.packageName}::${serviceName}: Service is not running.`);
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",
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.2.4",
34
+ "tsup": "8.3.0",
35
35
  "rimraf": "6.0.1",
36
36
  "vitest": "2.1.1"
37
37
  },
@@ -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 "~/packages/flightdeck/src/flightdeck.lib"
10
- import { FlightDeck } from "~/packages/flightdeck/src/flightdeck.lib"
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
- flightDeckRootDir: z.string(),
20
- downloadPackageToUpdatesCmd: z.array(z.string()),
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
- downloadPackageToUpdatesCmd: {
49
- flag: `u`,
51
+ scripts: {
52
+ flag: `r`,
50
53
  required: true,
51
- description: `Command to update the service.`,
52
- example: `--downloadPackageToUpdatesCmd=\"./app\"`,
53
- parse: parseArrayOption,
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>
@@ -1,5 +1,11 @@
1
1
  import { execSync, spawn } from "node:child_process"
2
- import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs"
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
- downloadPackageToUpdatesCmd: string[]
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 restartTimes: number[] = []
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
- public readonly currentServiceDir: string
45
- public readonly updateServiceDir: string
46
- public readonly backupServiceDir: string
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.currentServiceDir = resolve(
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
- console.log(req.method, url.pathname)
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
- this.fetchLatestRelease()
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
- console.log(`All services are ready to update!`)
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
- console.error(thrown, req.url)
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
- console.log(`Server started on port ${PORT}`)
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
- console.log(`Starting all services...`)
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
- console.log(
221
+ this.logger.info(
169
222
  `Starting service ${this.options.packageName}::${serviceName}, try ${this.safety}/2...`,
170
223
  )
171
- if (this.safety > 2) {
224
+ if (this.safety >= 2) {
172
225
  throw new Error(`Out of tries...`)
173
226
  }
174
227
  this.safety++
175
- if (!existsSync(this.currentServiceDir)) {
176
- console.log(
177
- `Tried to start service but failed: Service ${this.options.packageName} is not yet installed.`,
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.fetchLatestRelease()
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 program = executable.startsWith(`./`)
188
- ? resolve(this.currentServiceDir, executable)
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
- console.log(`${this.options.packageName}::${serviceName} 💬`, ...messages)
251
+ this.logger.info(`💬`, ...messages)
201
252
  })
202
253
  this.services[serviceName].on(`readyToUpdate`, () => {
203
- console.log(
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
- console.log(`All services are ready to update!`)
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
- console.log(
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
- console.log(
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 updatesAreReady = existsSync(this.updateServiceDir)
278
+ const installFile = resolve(this.persistentStateDir, `install`)
279
+ const updatesAreReady = existsSync(installFile)
234
280
  if (updatesAreReady) {
235
- console.log(
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
- console.log(
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
- console.log(
256
- `Service ${this.options.packageName}::${serviceName} crashed too many times. Not restarting.`,
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
- console.log(
266
- `Installing latest version of service ${this.options.packageName}...`,
267
- )
307
+ this.logger.info(`Installing...`)
268
308
 
269
- if (existsSync(this.updateServiceDir)) {
270
- const runningServices = toEntries(this.services).filter(
271
- ([, service]) => service,
272
- )
273
- if (runningServices.length > 0) {
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
- if (existsSync(this.currentServiceDir)) {
281
- if (!existsSync(this.backupServiceDir)) {
282
- mkdirSync(this.backupServiceDir, { recursive: true })
283
- } else {
284
- rmSync(this.backupServiceDir, { recursive: true })
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 fetchLatestRelease(): void {
298
- console.log(
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.downloadPackageToUpdatesCmd.join(` `))
330
+ execSync(this.options.scripts.download)
331
+ this.logger.info(`Downloaded!`)
304
332
  } catch (thrown) {
305
333
  if (thrown instanceof Error) {
306
- console.error(`Failed to fetch the latest release: ${thrown.message}`)
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
- console.log(`Stopping all services...`)
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
- console.log(
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
- console.error(
334
- `Failed to stop service ${this.options.packageName}::${serviceName}: Service is not running.`,
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
  }