flightdeck 0.0.1 → 0.0.3
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 +27 -15
- package/dist/flightdeck.bin.js +350 -0
- package/dist/flightdeck.main.schema.json +27 -15
- package/dist/klaxon.bin.js +87 -0
- package/dist/lib.d.ts +96 -23
- package/dist/lib.js +180 -66
- package/package.json +12 -10
- package/src/{bin.ts → flightdeck.bin.ts} +24 -28
- package/src/flightdeck.lib.ts +343 -0
- package/src/klaxon.bin.ts +58 -0
- package/src/klaxon.lib.ts +78 -0
- package/src/lib.ts +4 -1
- package/dist/bin.js +0 -102
- package/src/flightdeck.ts +0 -235
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
|
-
}
|