flightdeck 0.0.2 → 0.0.4

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/src/flightdeck.ts DELETED
@@ -1,235 +0,0 @@
1
- import { execSync, spawn } from "node:child_process"
2
- import { existsSync, mkdirSync, renameSync, rmSync, unlinkSync } from "node:fs"
3
- import type { Http2Server } from "node:http2"
4
- import { createServer } from "node:http2"
5
- import { homedir } from "node:os"
6
- import { resolve } from "node:path"
7
-
8
- import { Future } from "atom.io/internal"
9
- import { ChildSocket } from "atom.io/realtime-server"
10
-
11
- export type FlightDeckOptions = {
12
- secret: string
13
- repo: string
14
- app: string
15
- runCmd: string[]
16
- updateCmd: string[]
17
- serviceDir?: string | undefined
18
- }
19
-
20
- let safety = 0
21
- const PORT = process.env.PORT ?? 8080
22
- const ORIGIN = `http://localhost:${PORT}`
23
- export class FlightDeck {
24
- public get serviceName(): string {
25
- return `${this.options.repo}/${this.options.app}`
26
- }
27
-
28
- protected webhookServer: Http2Server
29
- protected service: ChildSocket<
30
- { updatesReady: [] },
31
- { readyToUpdate: []; alive: [] }
32
- > | null = null
33
- protected restartTimes: number[] = []
34
-
35
- public alive = new Future(() => {})
36
- public dead = new Future(() => {})
37
-
38
- public readonly currentServiceDir: string
39
- public readonly updateServiceDir: string
40
- public readonly backupServiceDir: string
41
-
42
- public constructor(public readonly options: FlightDeckOptions) {
43
- const {
44
- secret,
45
- serviceDir = resolve(
46
- homedir(),
47
- `services`,
48
- `sample/repo`,
49
- `my-app`,
50
- `current`,
51
- ),
52
- } = options
53
-
54
- this.currentServiceDir = resolve(serviceDir, `current`)
55
- this.backupServiceDir = resolve(serviceDir, `backup`)
56
- this.updateServiceDir = resolve(serviceDir, `update`)
57
-
58
- createServer((req, res) => {
59
- let data: Uint8Array[] = []
60
- req
61
- .on(`data`, (chunk) => {
62
- data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk))
63
- })
64
- .on(`end`, () => {
65
- console.log(req.headers)
66
- const authHeader = req.headers.authorization
67
- try {
68
- if (authHeader !== `Bearer ${secret}`) throw 401
69
- const url = new URL(req.url, ORIGIN)
70
- console.log(req.method, url.pathname)
71
- switch (req.method) {
72
- case `POST`:
73
- {
74
- console.log(`received post, url is ${url.pathname}`)
75
- switch (url.pathname) {
76
- case `/`:
77
- {
78
- res.writeHead(200)
79
- res.end()
80
- this.fetchLatestRelease()
81
- if (this.service) {
82
- this.service.emit(`updatesReady`)
83
- } else {
84
- this.applyUpdate()
85
- this.startService()
86
- }
87
- }
88
- break
89
-
90
- default:
91
- throw 404
92
- }
93
- }
94
- break
95
-
96
- default:
97
- throw 405
98
- }
99
- } catch (thrown) {
100
- console.error(thrown, req.url)
101
- if (typeof thrown === `number`) {
102
- res.writeHead(thrown)
103
- res.end()
104
- }
105
- } finally {
106
- data = []
107
- }
108
- })
109
- }).listen(PORT, () => {
110
- console.log(`Server started on port ${PORT}`)
111
- })
112
-
113
- this.startService()
114
- }
115
-
116
- protected startService(): void {
117
- safety++
118
- if (safety > 10) {
119
- throw new Error(`safety exceeded`)
120
- }
121
- if (!existsSync(this.currentServiceDir)) {
122
- console.log(
123
- `Tried to start service but failed: Service ${this.serviceName} is not yet installed.`,
124
- )
125
- this.fetchLatestRelease()
126
- this.applyUpdate()
127
- this.startService()
128
-
129
- return
130
- }
131
-
132
- const [executable, ...args] = this.options.runCmd
133
- const program = executable.startsWith(`./`)
134
- ? resolve(this.currentServiceDir, executable)
135
- : executable
136
- const serviceProcess = spawn(program, args, {
137
- cwd: this.currentServiceDir,
138
- env: import.meta.env,
139
- })
140
- this.service = new ChildSocket(serviceProcess, this.serviceName, console)
141
- this.service.onAny((...messages) => {
142
- console.log(`🛰 `, ...messages)
143
- })
144
- this.service.on(`readyToUpdate`, () => {
145
- this.stopService()
146
- })
147
- this.service.on(`alive`, () => {
148
- this.alive.use(Promise.resolve())
149
- this.dead = new Future(() => {})
150
- })
151
- this.service.process.on(`close`, (exitCode) => {
152
- console.log(`Service ${this.serviceName} exited with code ${exitCode}`)
153
- this.service = null
154
- const updatesAreReady = existsSync(this.updateServiceDir)
155
- if (updatesAreReady) {
156
- console.log(`Updates are ready; applying and restarting...`)
157
- this.restartTimes = []
158
- this.applyUpdate()
159
- this.startService()
160
- } else {
161
- if (exitCode !== 0) {
162
- const now = Date.now()
163
- const fiveMinutesAgo = now - 5 * 60 * 1000
164
- this.restartTimes = this.restartTimes.filter(
165
- (time) => time > fiveMinutesAgo,
166
- )
167
- this.restartTimes.push(now)
168
-
169
- if (this.restartTimes.length < 5) {
170
- console.log(`Service ${this.serviceName} crashed. Restarting...`)
171
- this.startService()
172
- } else {
173
- console.log(
174
- `Service ${this.serviceName} crashed too many times. Not restarting.`,
175
- )
176
- }
177
- }
178
- }
179
- })
180
- }
181
-
182
- protected applyUpdate(): void {
183
- console.log(`Installing latest version of service ${this.serviceName}...`)
184
-
185
- if (existsSync(this.updateServiceDir)) {
186
- if (this.service) {
187
- console.log(
188
- `Tried to apply update but failed: Service ${this.serviceName} is currently running.`,
189
- )
190
- return
191
- }
192
-
193
- if (existsSync(this.currentServiceDir)) {
194
- if (!existsSync(this.backupServiceDir)) {
195
- mkdirSync(this.backupServiceDir, { recursive: true })
196
- } else {
197
- rmSync(this.backupServiceDir, { recursive: true })
198
- }
199
- renameSync(this.currentServiceDir, this.backupServiceDir)
200
- }
201
-
202
- renameSync(this.updateServiceDir, this.currentServiceDir)
203
- this.restartTimes = []
204
- } else {
205
- console.log(`Service ${this.serviceName} is already up to date.`)
206
- }
207
- }
208
-
209
- protected fetchLatestRelease(): void {
210
- console.log(`Downloading latest version of service ${this.serviceName}...`)
211
-
212
- try {
213
- execSync(this.options.updateCmd.join(` `))
214
- } catch (thrown) {
215
- if (thrown instanceof Error) {
216
- console.error(`Failed to fetch the latest release: ${thrown.message}`)
217
- }
218
- return
219
- }
220
- }
221
-
222
- public stopService(): void {
223
- if (this.service) {
224
- console.log(`Stopping service ${this.serviceName}...`)
225
- this.service.process.kill()
226
- this.service = null
227
- this.dead.use(Promise.resolve())
228
- this.alive = new Future(() => {})
229
- } else {
230
- console.error(
231
- `Failed to stop service ${this.serviceName}: Service is not running.`,
232
- )
233
- }
234
- }
235
- }