flightdeck 0.1.3 → 0.2.1
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 +5 -1
- package/dist/flightdeck.bin.js +5708 -105
- package/dist/flightdeck.main.schema.json +5 -1
- package/dist/klaxon.bin.js +7 -4
- package/dist/lib.d.ts +39 -9
- package/dist/lib.js +5717 -107
- package/package.json +7 -6
- package/src/filesystem-storage.ts +67 -0
- package/src/flightdeck.bin.ts +2 -2
- package/src/flightdeck.lib.ts +197 -129
- package/src/klaxon.lib.ts +6 -2
- package/src/lib.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flightdeck",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jeremy Banka",
|
|
@@ -22,19 +22,20 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@t3-oss/env-core": "0.11.1",
|
|
25
|
+
"cron": "3.2.0",
|
|
25
26
|
"zod": "3.23.8",
|
|
26
27
|
"atom.io": "0.30.2",
|
|
27
28
|
"comline": "0.1.6"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@types/node": "22.
|
|
31
|
+
"@types/node": "22.9.0",
|
|
31
32
|
"@types/tmp": "0.2.6",
|
|
32
|
-
"bun-types": "1.1.
|
|
33
|
-
"concurrently": "9.0
|
|
33
|
+
"bun-types": "1.1.34",
|
|
34
|
+
"concurrently": "9.1.0",
|
|
34
35
|
"rimraf": "6.0.1",
|
|
35
36
|
"tmp": "0.2.3",
|
|
36
|
-
"tsup": "8.3.
|
|
37
|
-
"vitest": "2.1.
|
|
37
|
+
"tsup": "8.3.5",
|
|
38
|
+
"vitest": "2.1.4"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
41
|
"build": "rimraf dist && concurrently \"bun:build:*\" && concurrently \"bun:schema:*\"",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs"
|
|
10
|
+
import { resolve } from "node:path"
|
|
11
|
+
|
|
12
|
+
export type FilesystemStorageOptions = {
|
|
13
|
+
path: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class FilesystemStorage<
|
|
17
|
+
T extends Record<string, string> = Record<string, string>,
|
|
18
|
+
> implements Storage
|
|
19
|
+
{
|
|
20
|
+
public rootDir: string
|
|
21
|
+
|
|
22
|
+
public constructor(options: FilesystemStorageOptions) {
|
|
23
|
+
this.rootDir = options.path
|
|
24
|
+
if (!existsSync(this.rootDir)) {
|
|
25
|
+
mkdirSync(this.rootDir, { recursive: true })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public getItem<K extends string & keyof T>(key: K): T[K] | null {
|
|
30
|
+
const filePath = resolve(this.rootDir, key)
|
|
31
|
+
if (existsSync(filePath)) {
|
|
32
|
+
return readFileSync(filePath, `utf-8`) as T[K]
|
|
33
|
+
}
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public setItem<K extends string & keyof T>(key: K, value: T[K]): void {
|
|
38
|
+
const filePath = resolve(this.rootDir, key)
|
|
39
|
+
writeFileSync(filePath, value)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public removeItem<K extends string & keyof T>(key: K): void {
|
|
43
|
+
const filePath = resolve(this.rootDir, key)
|
|
44
|
+
if (existsSync(filePath)) {
|
|
45
|
+
rmSync(filePath)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public key(index: number): (string & keyof T) | null {
|
|
50
|
+
const filePaths = readdirSync(this.rootDir)
|
|
51
|
+
const filePathsByDateCreated = filePaths.sort((a, b) => {
|
|
52
|
+
const aStat = statSync(a)
|
|
53
|
+
const bStat = statSync(b)
|
|
54
|
+
return bStat.ctimeMs - aStat.ctimeMs
|
|
55
|
+
})
|
|
56
|
+
return (filePathsByDateCreated[index] as string & keyof T) ?? null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public clear(): void {
|
|
60
|
+
rmSync(this.rootDir, { recursive: true })
|
|
61
|
+
mkdirSync(this.rootDir, { recursive: true })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public get length(): number {
|
|
65
|
+
return readdirSync(this.rootDir).length
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/flightdeck.bin.ts
CHANGED
|
@@ -18,6 +18,7 @@ const FLIGHTDECK_MANUAL = {
|
|
|
18
18
|
scripts: z.object({
|
|
19
19
|
download: z.string(),
|
|
20
20
|
install: z.string(),
|
|
21
|
+
checkAvailability: z.string(),
|
|
21
22
|
}),
|
|
22
23
|
}),
|
|
23
24
|
options: {
|
|
@@ -105,8 +106,7 @@ switch (inputs.case) {
|
|
|
105
106
|
default: {
|
|
106
107
|
const flightDeck = new FlightDeck(inputs.opts)
|
|
107
108
|
process.on(`close`, async () => {
|
|
108
|
-
flightDeck.stopAllServices()
|
|
109
|
-
await flightDeck.dead
|
|
109
|
+
await flightDeck.stopAllServices()
|
|
110
110
|
})
|
|
111
111
|
}
|
|
112
112
|
}
|
package/src/flightdeck.lib.ts
CHANGED
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
import { execSync, spawn } from "node:child_process"
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
3
2
|
import type { Server } from "node:http"
|
|
4
3
|
import { createServer } from "node:http"
|
|
5
4
|
import { homedir } from "node:os"
|
|
6
5
|
import { resolve } from "node:path"
|
|
7
6
|
|
|
8
7
|
import { Future } from "atom.io/internal"
|
|
8
|
+
import { discoverType } from "atom.io/introspection"
|
|
9
9
|
import { fromEntries, toEntries } from "atom.io/json"
|
|
10
10
|
import { ChildSocket } from "atom.io/realtime-server"
|
|
11
|
+
import { CronJob } from "cron"
|
|
11
12
|
|
|
13
|
+
import { FilesystemStorage } from "./filesystem-storage"
|
|
12
14
|
import { env } from "./flightdeck.env"
|
|
13
15
|
|
|
16
|
+
export const FLIGHTDECK_SETUP_PHASES = [`downloaded`, `installed`] as const
|
|
17
|
+
|
|
18
|
+
export type FlightDeckSetupPhase = (typeof FLIGHTDECK_SETUP_PHASES)[number]
|
|
19
|
+
|
|
20
|
+
export const FLIGHTDECK_UPDATE_PHASES = [`notified`, `confirmed`] as const
|
|
21
|
+
|
|
22
|
+
export type FlightDeckUpdatePhase = (typeof FLIGHTDECK_UPDATE_PHASES)[number]
|
|
23
|
+
|
|
24
|
+
export function isVersionNumber(version: string): boolean {
|
|
25
|
+
return (
|
|
26
|
+
/^\d+\.\d+\.\d+$/.test(version) || !Number.isNaN(Number.parseFloat(version))
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
export type FlightDeckOptions<S extends string = string> = {
|
|
15
31
|
packageName: string
|
|
16
32
|
services: { [service in S]: { run: string; waitFor: boolean } }
|
|
17
33
|
scripts: {
|
|
18
34
|
download: string
|
|
19
35
|
install: string
|
|
36
|
+
checkAvailability?: string
|
|
20
37
|
}
|
|
21
38
|
port?: number | undefined
|
|
22
39
|
flightdeckRootDir?: string | undefined
|
|
@@ -25,23 +42,30 @@ export type FlightDeckOptions<S extends string = string> = {
|
|
|
25
42
|
export class FlightDeck<S extends string = string> {
|
|
26
43
|
protected safety = 0
|
|
27
44
|
|
|
45
|
+
protected storage: FilesystemStorage<{
|
|
46
|
+
setupPhase: FlightDeckSetupPhase
|
|
47
|
+
updatePhase: FlightDeckUpdatePhase
|
|
48
|
+
updateAwaitedVersion: string
|
|
49
|
+
}>
|
|
28
50
|
protected webhookServer: Server
|
|
29
51
|
protected services: {
|
|
30
52
|
[service in S]: ChildSocket<
|
|
31
|
-
{ updatesReady: [] },
|
|
53
|
+
{ timeToStop: []; updatesReady: [] },
|
|
32
54
|
{ readyToUpdate: []; alive: [] }
|
|
33
55
|
> | null
|
|
34
56
|
}
|
|
35
57
|
protected serviceIdx: { readonly [service in S]: number }
|
|
36
58
|
public defaultServicesReadyToUpdate: { readonly [service in S]: boolean }
|
|
37
59
|
public servicesReadyToUpdate: { [service in S]: boolean }
|
|
38
|
-
public
|
|
60
|
+
public autoRespawnDeadServices: boolean
|
|
39
61
|
|
|
40
62
|
protected logger: Pick<Console, `error` | `info` | `warn`>
|
|
41
63
|
protected serviceLoggers: {
|
|
42
64
|
readonly [service in S]: Pick<Console, `error` | `info` | `warn`>
|
|
43
65
|
}
|
|
44
66
|
|
|
67
|
+
protected updateAvailabilityChecker: CronJob | null = null
|
|
68
|
+
|
|
45
69
|
public servicesLive: Future<void>[]
|
|
46
70
|
public servicesDead: Future<void>[]
|
|
47
71
|
public live = new Future(() => {})
|
|
@@ -49,11 +73,9 @@ export class FlightDeck<S extends string = string> {
|
|
|
49
73
|
|
|
50
74
|
protected restartTimes: number[] = []
|
|
51
75
|
|
|
52
|
-
protected persistentStateDir: string
|
|
53
|
-
|
|
54
76
|
public constructor(public readonly options: FlightDeckOptions<S>) {
|
|
55
77
|
const { FLIGHTDECK_SECRET } = env
|
|
56
|
-
const { flightdeckRootDir = resolve(homedir(), `
|
|
78
|
+
const { flightdeckRootDir = resolve(homedir(), `.flightdeck`) } = options
|
|
57
79
|
const port = options.port ?? 8080
|
|
58
80
|
const origin = `http://localhost:${port}`
|
|
59
81
|
|
|
@@ -71,7 +93,7 @@ export class FlightDeck<S extends string = string> {
|
|
|
71
93
|
]),
|
|
72
94
|
)
|
|
73
95
|
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate }
|
|
74
|
-
this.
|
|
96
|
+
this.autoRespawnDeadServices = true
|
|
75
97
|
|
|
76
98
|
this.logger = {
|
|
77
99
|
info: (...args: any[]) => {
|
|
@@ -109,14 +131,9 @@ export class FlightDeck<S extends string = string> {
|
|
|
109
131
|
this.live.use(Promise.all(this.servicesLive))
|
|
110
132
|
this.dead.use(Promise.all(this.servicesDead))
|
|
111
133
|
|
|
112
|
-
this.
|
|
113
|
-
flightdeckRootDir,
|
|
114
|
-
|
|
115
|
-
options.packageName,
|
|
116
|
-
)
|
|
117
|
-
if (!existsSync(this.persistentStateDir)) {
|
|
118
|
-
mkdirSync(this.persistentStateDir, { recursive: true })
|
|
119
|
-
}
|
|
134
|
+
this.storage = new FilesystemStorage({
|
|
135
|
+
path: resolve(flightdeckRootDir, `storage`, options.packageName),
|
|
136
|
+
})
|
|
120
137
|
|
|
121
138
|
if (FLIGHTDECK_SECRET === undefined) {
|
|
122
139
|
this.logger.warn(
|
|
@@ -142,63 +159,34 @@ export class FlightDeck<S extends string = string> {
|
|
|
142
159
|
}
|
|
143
160
|
const url = new URL(req.url, origin)
|
|
144
161
|
this.logger.info(req.method, url.pathname)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
toEntries(this.servicesReadyToUpdate).every(
|
|
174
|
-
([, isReady]) => isReady,
|
|
175
|
-
)
|
|
176
|
-
) {
|
|
177
|
-
this.logger.info(`All services are ready to update!`)
|
|
178
|
-
this.stopAllServices()
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
for (const entry of toEntries(this.services)) {
|
|
182
|
-
const [serviceName, service] = entry
|
|
183
|
-
if (service) {
|
|
184
|
-
if (this.options.services[serviceName].waitFor) {
|
|
185
|
-
service.emit(`updatesReady`)
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
this.startService(serviceName)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
break
|
|
193
|
-
|
|
194
|
-
default:
|
|
195
|
-
throw 404
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
break
|
|
199
|
-
|
|
200
|
-
default:
|
|
201
|
-
throw 405
|
|
162
|
+
|
|
163
|
+
const versionForeignInput = Buffer.concat(data).toString()
|
|
164
|
+
if (!isVersionNumber(versionForeignInput)) {
|
|
165
|
+
throw 400
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
res.writeHead(200)
|
|
169
|
+
res.end()
|
|
170
|
+
|
|
171
|
+
this.storage.setItem(`updatePhase`, `notified`)
|
|
172
|
+
this.storage.setItem(`updateAwaitedVersion`, versionForeignInput)
|
|
173
|
+
const { checkAvailability } = options.scripts
|
|
174
|
+
if (checkAvailability) {
|
|
175
|
+
this.updateAvailabilityChecker?.stop()
|
|
176
|
+
this.seekUpdate(versionForeignInput)
|
|
177
|
+
const updatePhase = this.storage.getItem(`updatePhase`)
|
|
178
|
+
this.logger.info(`> storage("updatePhase") >`, updatePhase)
|
|
179
|
+
if (updatePhase === `notified`) {
|
|
180
|
+
this.updateAvailabilityChecker = new CronJob(
|
|
181
|
+
`30 * * * * *`,
|
|
182
|
+
() => {
|
|
183
|
+
this.seekUpdate(versionForeignInput)
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
this.updateAvailabilityChecker.start()
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
this.downloadPackage()
|
|
202
190
|
}
|
|
203
191
|
} catch (thrown) {
|
|
204
192
|
this.logger.error(thrown, req.url)
|
|
@@ -216,12 +204,101 @@ export class FlightDeck<S extends string = string> {
|
|
|
216
204
|
}
|
|
217
205
|
|
|
218
206
|
this.startAllServices()
|
|
207
|
+
.then(() => {
|
|
208
|
+
this.logger.info(`All services started.`)
|
|
209
|
+
})
|
|
210
|
+
.catch((thrown) => {
|
|
211
|
+
if (thrown instanceof Error) {
|
|
212
|
+
this.logger.error(`Failed to start all services:`, thrown.message)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
protected seekUpdate(version: string): void {
|
|
218
|
+
this.logger.info(`Checking for updates...`)
|
|
219
|
+
const { checkAvailability } = this.options.scripts
|
|
220
|
+
if (!checkAvailability) {
|
|
221
|
+
this.logger.info(`No checkAvailability script found.`)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const out = execSync(`${checkAvailability} ${version}`)
|
|
226
|
+
this.logger.info(`Check stdout:`, out.toString())
|
|
227
|
+
this.updateAvailabilityChecker?.stop()
|
|
228
|
+
this.storage.setItem(`updatePhase`, `confirmed`)
|
|
229
|
+
this.downloadPackage()
|
|
230
|
+
this.announceUpdate()
|
|
231
|
+
} catch (thrown) {
|
|
232
|
+
if (thrown instanceof Error) {
|
|
233
|
+
this.logger.error(`Check failed:`, thrown.message)
|
|
234
|
+
} else {
|
|
235
|
+
const thrownType = discoverType(thrown)
|
|
236
|
+
this.logger.error(`Check threw`, thrownType, thrown)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
protected announceUpdate(): void {
|
|
242
|
+
for (const entry of toEntries(this.services)) {
|
|
243
|
+
const [serviceName, service] = entry
|
|
244
|
+
if (service) {
|
|
245
|
+
if (this.options.services[serviceName].waitFor) {
|
|
246
|
+
service.emit(`updatesReady`)
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
this.startService(serviceName)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
219
252
|
}
|
|
220
253
|
|
|
221
|
-
protected
|
|
254
|
+
protected tryUpdate(): void {
|
|
255
|
+
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
256
|
+
this.logger.info(`All services are ready to update.`)
|
|
257
|
+
this.stopAllServices()
|
|
258
|
+
.then(() => {
|
|
259
|
+
this.logger.info(`All services stopped; starting up fresh...`)
|
|
260
|
+
this.startAllServices()
|
|
261
|
+
.then(() => {
|
|
262
|
+
this.logger.info(`All services started; we're back online.`)
|
|
263
|
+
})
|
|
264
|
+
.catch((thrown) => {
|
|
265
|
+
if (thrown instanceof Error) {
|
|
266
|
+
this.logger.error(
|
|
267
|
+
`Failed to start all services:`,
|
|
268
|
+
thrown.message,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
.catch((thrown) => {
|
|
274
|
+
if (thrown instanceof Error) {
|
|
275
|
+
this.logger.error(`Failed to stop all services:`, thrown.message)
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
protected startAllServices(): Future<unknown> {
|
|
222
282
|
this.logger.info(`Starting all services...`)
|
|
223
|
-
|
|
224
|
-
|
|
283
|
+
this.autoRespawnDeadServices = true
|
|
284
|
+
const setupPhase = this.storage.getItem(`setupPhase`)
|
|
285
|
+
this.logger.info(`> storage("setupPhase") >`, setupPhase)
|
|
286
|
+
switch (setupPhase) {
|
|
287
|
+
case null:
|
|
288
|
+
this.logger.info(`Starting from scratch.`)
|
|
289
|
+
this.downloadPackage()
|
|
290
|
+
this.installPackage()
|
|
291
|
+
return this.startAllServices()
|
|
292
|
+
case `downloaded`:
|
|
293
|
+
this.logger.info(`Found package downloaded but not installed.`)
|
|
294
|
+
this.installPackage()
|
|
295
|
+
return this.startAllServices()
|
|
296
|
+
case `installed`: {
|
|
297
|
+
for (const [serviceName] of toEntries(this.services)) {
|
|
298
|
+
this.startService(serviceName)
|
|
299
|
+
}
|
|
300
|
+
return this.live
|
|
301
|
+
}
|
|
225
302
|
}
|
|
226
303
|
}
|
|
227
304
|
|
|
@@ -233,17 +310,6 @@ export class FlightDeck<S extends string = string> {
|
|
|
233
310
|
throw new Error(`Out of tries...`)
|
|
234
311
|
}
|
|
235
312
|
this.safety++
|
|
236
|
-
const readyFile = resolve(this.persistentStateDir, `ready`)
|
|
237
|
-
if (!existsSync(readyFile)) {
|
|
238
|
-
this.logger.info(
|
|
239
|
-
`Tried to start service but failed: could not find readyFile: ${readyFile}`,
|
|
240
|
-
)
|
|
241
|
-
this.getLatestRelease()
|
|
242
|
-
this.applyUpdate()
|
|
243
|
-
this.startService(serviceName)
|
|
244
|
-
|
|
245
|
-
return
|
|
246
|
-
}
|
|
247
313
|
|
|
248
314
|
const [exe, ...args] = this.options.services[serviceName].run.split(` `)
|
|
249
315
|
const serviceProcess = spawn(exe, args, {
|
|
@@ -256,17 +322,12 @@ export class FlightDeck<S extends string = string> {
|
|
|
256
322
|
console,
|
|
257
323
|
)
|
|
258
324
|
this.services[serviceName].onAny((...messages) => {
|
|
259
|
-
this.
|
|
325
|
+
this.serviceLoggers[serviceName].info(`💬`, ...messages)
|
|
260
326
|
})
|
|
261
327
|
this.services[serviceName].on(`readyToUpdate`, () => {
|
|
262
|
-
this.
|
|
328
|
+
this.logger.info(`Service "${serviceName}" is ready to update.`)
|
|
263
329
|
this.servicesReadyToUpdate[serviceName] = true
|
|
264
|
-
|
|
265
|
-
toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)
|
|
266
|
-
) {
|
|
267
|
-
this.logger.info(`All services are ready to update.`)
|
|
268
|
-
this.stopAllServices()
|
|
269
|
-
}
|
|
330
|
+
this.tryUpdate()
|
|
270
331
|
})
|
|
271
332
|
this.services[serviceName].on(`alive`, () => {
|
|
272
333
|
this.servicesLive[this.serviceIdx[serviceName]].use(Promise.resolve())
|
|
@@ -276,19 +337,22 @@ export class FlightDeck<S extends string = string> {
|
|
|
276
337
|
}
|
|
277
338
|
this.dead.use(Promise.all(this.servicesDead))
|
|
278
339
|
})
|
|
279
|
-
this.services[serviceName].process.
|
|
280
|
-
this.
|
|
340
|
+
this.services[serviceName].process.once(`close`, (exitCode) => {
|
|
341
|
+
this.logger.info(
|
|
342
|
+
`Auto-respawn saw "${serviceName}" exit with code ${exitCode}`,
|
|
343
|
+
)
|
|
281
344
|
this.services[serviceName] = null
|
|
282
|
-
if (!this.
|
|
283
|
-
this.
|
|
345
|
+
if (!this.autoRespawnDeadServices) {
|
|
346
|
+
this.logger.info(`Auto-respawn is off; "${serviceName}" rests.`)
|
|
284
347
|
return
|
|
285
348
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
349
|
+
const updatePhase = this.storage.getItem(`updatePhase`)
|
|
350
|
+
this.logger.info(`> storage("updatePhase") >`, updatePhase)
|
|
351
|
+
const updatesAreReady = updatePhase === `confirmed`
|
|
288
352
|
if (updatesAreReady) {
|
|
289
353
|
this.serviceLoggers[serviceName].info(`Updating before startup...`)
|
|
290
354
|
this.restartTimes = []
|
|
291
|
-
this.
|
|
355
|
+
this.installPackage()
|
|
292
356
|
this.startService(serviceName)
|
|
293
357
|
} else {
|
|
294
358
|
const now = Date.now()
|
|
@@ -311,18 +375,13 @@ export class FlightDeck<S extends string = string> {
|
|
|
311
375
|
this.safety = 0
|
|
312
376
|
}
|
|
313
377
|
|
|
314
|
-
protected
|
|
315
|
-
this.logger.info(`
|
|
316
|
-
|
|
378
|
+
protected downloadPackage(): void {
|
|
379
|
+
this.logger.info(`Downloading...`)
|
|
317
380
|
try {
|
|
318
|
-
execSync(this.options.scripts.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
const readyFile = resolve(this.persistentStateDir, `ready`)
|
|
324
|
-
writeFileSync(readyFile, ``)
|
|
325
|
-
this.logger.info(`Installed!`)
|
|
381
|
+
const out = execSync(this.options.scripts.download)
|
|
382
|
+
this.logger.info(`Download stdout:`, out.toString())
|
|
383
|
+
this.storage.setItem(`setupPhase`, `downloaded`)
|
|
384
|
+
this.logger.info(`Downloaded!`)
|
|
326
385
|
} catch (thrown) {
|
|
327
386
|
if (thrown instanceof Error) {
|
|
328
387
|
this.logger.error(`Failed to get the latest release: ${thrown.message}`)
|
|
@@ -331,12 +390,14 @@ export class FlightDeck<S extends string = string> {
|
|
|
331
390
|
}
|
|
332
391
|
}
|
|
333
392
|
|
|
334
|
-
protected
|
|
335
|
-
this.logger.info(`
|
|
393
|
+
protected installPackage(): void {
|
|
394
|
+
this.logger.info(`Installing...`)
|
|
336
395
|
|
|
337
396
|
try {
|
|
338
|
-
execSync(this.options.scripts.
|
|
339
|
-
this.logger.info(`
|
|
397
|
+
const out = execSync(this.options.scripts.install)
|
|
398
|
+
this.logger.info(`Install stdout:`, out.toString())
|
|
399
|
+
this.storage.setItem(`setupPhase`, `installed`)
|
|
400
|
+
this.logger.info(`Installed!`)
|
|
340
401
|
} catch (thrown) {
|
|
341
402
|
if (thrown instanceof Error) {
|
|
342
403
|
this.logger.error(`Failed to get the latest release: ${thrown.message}`)
|
|
@@ -345,19 +406,32 @@ export class FlightDeck<S extends string = string> {
|
|
|
345
406
|
}
|
|
346
407
|
}
|
|
347
408
|
|
|
348
|
-
public stopAllServices():
|
|
349
|
-
this.logger.info(`Stopping all services
|
|
409
|
+
public stopAllServices(): Future<unknown> {
|
|
410
|
+
this.logger.info(`Stopping all services... auto-respawn disabled.`)
|
|
411
|
+
this.autoRespawnDeadServices = false
|
|
350
412
|
for (const [serviceName] of toEntries(this.services)) {
|
|
351
413
|
this.stopService(serviceName)
|
|
352
414
|
}
|
|
415
|
+
return this.dead
|
|
353
416
|
}
|
|
354
417
|
|
|
355
418
|
public stopService(serviceName: S): void {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.
|
|
359
|
-
this.
|
|
360
|
-
|
|
419
|
+
const service = this.services[serviceName]
|
|
420
|
+
if (service) {
|
|
421
|
+
this.logger.info(`Stopping service "${serviceName}"...`)
|
|
422
|
+
this.servicesDead[this.serviceIdx[serviceName]].use(
|
|
423
|
+
new Promise((pass) => {
|
|
424
|
+
service.emit(`timeToStop`)
|
|
425
|
+
service.process.once(`close`, (exitCode) => {
|
|
426
|
+
this.logger.info(
|
|
427
|
+
`Stopped service "${serviceName}"; exited with code ${exitCode}`,
|
|
428
|
+
)
|
|
429
|
+
this.services[serviceName] = null
|
|
430
|
+
pass()
|
|
431
|
+
})
|
|
432
|
+
}),
|
|
433
|
+
)
|
|
434
|
+
this.dead.use(Promise.all(this.servicesDead))
|
|
361
435
|
this.servicesLive[this.serviceIdx[serviceName]] = new Future(() => {})
|
|
362
436
|
if (this.live.done) {
|
|
363
437
|
this.live = new Future(() => {})
|
|
@@ -369,10 +443,4 @@ export class FlightDeck<S extends string = string> {
|
|
|
369
443
|
)
|
|
370
444
|
}
|
|
371
445
|
}
|
|
372
|
-
|
|
373
|
-
public shutdown(): void {
|
|
374
|
-
this.logger.info(`Shutting down...`)
|
|
375
|
-
this.servicesShouldRestart = false
|
|
376
|
-
this.stopAllServices()
|
|
377
|
-
}
|
|
378
446
|
}
|
package/src/klaxon.lib.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
export type AlertOptions = {
|
|
2
2
|
secret: string
|
|
3
3
|
endpoint: string
|
|
4
|
+
version: string
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export async function alert({
|
|
7
8
|
secret,
|
|
8
9
|
endpoint,
|
|
10
|
+
version,
|
|
9
11
|
}: AlertOptions): Promise<Response> {
|
|
10
12
|
const response = await fetch(endpoint, {
|
|
11
13
|
method: `POST`,
|
|
12
14
|
headers: {
|
|
13
|
-
"Content-Type": `
|
|
15
|
+
"Content-Type": `text/plain;charset=UTF-8`,
|
|
14
16
|
Authorization: `Bearer ${secret}`,
|
|
15
17
|
},
|
|
18
|
+
body: version,
|
|
16
19
|
})
|
|
17
20
|
|
|
18
21
|
return response
|
|
@@ -64,7 +67,8 @@ export async function scramble<K extends string = string>({
|
|
|
64
67
|
const name = publishedPackage.name as K
|
|
65
68
|
const { endpoint } = packageConfig[name]
|
|
66
69
|
const secret = secretsConfig[name]
|
|
67
|
-
const
|
|
70
|
+
const version = publishedPackage.version
|
|
71
|
+
const alertResultPromise = alert({ secret, endpoint, version }).then(
|
|
68
72
|
(alertResult) => [name, alertResult] as const,
|
|
69
73
|
)
|
|
70
74
|
alertResults.push(alertResultPromise)
|
package/src/lib.ts
CHANGED