@highstate/backend 0.9.15 → 0.9.16
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/chunk-RCB4AFGD.js +159 -0
- package/dist/chunk-RCB4AFGD.js.map +1 -0
- package/dist/chunk-WHALQHEZ.js +2017 -0
- package/dist/chunk-WHALQHEZ.js.map +1 -0
- package/dist/highstate.manifest.json +3 -3
- package/dist/index.js +6158 -2178
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +47 -155
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +159 -41
- package/package.json +25 -7
- package/src/artifact/abstractions.ts +46 -0
- package/src/artifact/encryption.ts +69 -0
- package/src/artifact/factory.ts +36 -0
- package/src/artifact/index.ts +3 -0
- package/src/artifact/local.ts +142 -0
- package/src/business/api-key.ts +65 -0
- package/src/business/artifact.ts +288 -0
- package/src/business/backend-unlock.ts +10 -0
- package/src/business/index.ts +9 -0
- package/src/business/instance-lock.ts +124 -0
- package/src/business/instance-state.ts +292 -0
- package/src/business/operation.ts +251 -0
- package/src/business/project-unlock.ts +242 -0
- package/src/business/secret.ts +187 -0
- package/src/business/worker.ts +161 -0
- package/src/common/index.ts +2 -1
- package/src/common/performance.ts +44 -0
- package/src/common/tree.ts +33 -0
- package/src/common/utils.ts +40 -1
- package/src/config.ts +14 -10
- package/src/hotstate/abstractions.ts +48 -0
- package/src/hotstate/factory.ts +17 -0
- package/src/{secret → hotstate}/index.ts +1 -0
- package/src/hotstate/manager.ts +192 -0
- package/src/hotstate/memory.ts +100 -0
- package/src/hotstate/validation.ts +101 -0
- package/src/index.ts +2 -1
- package/src/library/abstractions.ts +10 -23
- package/src/library/factory.ts +2 -2
- package/src/library/local.ts +89 -102
- package/src/library/worker/evaluator.ts +9 -42
- package/src/library/worker/loader.lite.ts +41 -0
- package/src/library/worker/main.ts +14 -65
- package/src/library/worker/protocol.ts +8 -24
- package/src/lock/abstractions.ts +6 -0
- package/src/lock/factory.ts +15 -0
- package/src/{workspace → lock}/index.ts +1 -0
- package/src/lock/manager.ts +82 -0
- package/src/lock/memory.ts +19 -0
- package/src/orchestrator/manager.ts +129 -82
- package/src/orchestrator/operation-workset.ts +168 -77
- package/src/orchestrator/operation.ts +967 -284
- package/src/project/abstractions.ts +20 -7
- package/src/project/factory.ts +1 -1
- package/src/project/index.ts +0 -1
- package/src/project/local.ts +73 -17
- package/src/project/manager.ts +272 -131
- package/src/pubsub/abstractions.ts +13 -0
- package/src/pubsub/factory.ts +19 -0
- package/src/pubsub/index.ts +3 -0
- package/src/pubsub/local.ts +36 -0
- package/src/pubsub/manager.ts +100 -0
- package/src/pubsub/validation.ts +33 -0
- package/src/runner/abstractions.ts +135 -68
- package/src/runner/artifact-env.ts +160 -0
- package/src/runner/factory.ts +20 -5
- package/src/runner/force-abort.ts +117 -0
- package/src/runner/local.ts +281 -371
- package/src/{common → runner}/pulumi.ts +86 -37
- package/src/services.ts +193 -35
- package/src/shared/index.ts +3 -11
- package/src/shared/models/backend/index.ts +3 -0
- package/src/shared/models/backend/project.ts +63 -0
- package/src/shared/models/backend/unlock-method.ts +20 -0
- package/src/shared/models/base.ts +151 -0
- package/src/shared/models/errors.ts +5 -0
- package/src/shared/models/index.ts +4 -0
- package/src/shared/models/project/api-key.ts +62 -0
- package/src/shared/models/project/artifact.ts +113 -0
- package/src/shared/models/project/component.ts +45 -0
- package/src/shared/models/project/index.ts +14 -0
- package/src/shared/{project.ts → models/project/instance.ts} +12 -0
- package/src/shared/models/project/lock.ts +91 -0
- package/src/shared/{operation.ts → models/project/operation.ts} +28 -7
- package/src/shared/models/project/page.ts +57 -0
- package/src/shared/models/project/secret.ts +112 -0
- package/src/shared/models/project/service-account.ts +22 -0
- package/src/shared/models/project/state.ts +432 -0
- package/src/shared/models/project/terminal.ts +99 -0
- package/src/shared/models/project/trigger.ts +56 -0
- package/src/shared/models/project/unlock-method.ts +31 -0
- package/src/shared/models/project/worker.ts +105 -0
- package/src/shared/resolvers/graph-resolver.ts +28 -0
- package/src/shared/resolvers/index.ts +5 -0
- package/src/shared/resolvers/input-hash.ts +53 -15
- package/src/shared/resolvers/input.ts +1 -9
- package/src/shared/resolvers/registry.ts +3 -2
- package/src/shared/resolvers/state.ts +2 -2
- package/src/shared/resolvers/validation.ts +61 -20
- package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
- package/src/shared/utils/hash.ts +6 -0
- package/src/shared/utils/index.ts +3 -0
- package/src/shared/utils/promise-tracker.ts +23 -0
- package/src/state/abstractions.ts +330 -101
- package/src/state/encryption.ts +59 -0
- package/src/state/factory.ts +3 -5
- package/src/state/index.ts +3 -0
- package/src/state/keyring.ts +22 -0
- package/src/state/local/backend.ts +299 -0
- package/src/state/local/collection.ts +342 -0
- package/src/state/local/index.ts +2 -0
- package/src/state/manager.ts +804 -18
- package/src/state/repository/index.ts +2 -0
- package/src/state/repository/repository.index.ts +193 -0
- package/src/state/repository/repository.ts +458 -0
- package/src/terminal/{shared.ts → abstractions.ts} +3 -3
- package/src/terminal/docker.ts +18 -14
- package/src/terminal/factory.ts +3 -3
- package/src/terminal/index.ts +1 -1
- package/src/terminal/manager.ts +131 -79
- package/src/terminal/run.sh.ts +21 -11
- package/src/worker/abstractions.ts +42 -0
- package/src/worker/docker.ts +83 -0
- package/src/worker/factory.ts +20 -0
- package/src/worker/index.ts +3 -0
- package/src/worker/manager.ts +139 -0
- package/dist/chunk-KTGKNSKM.js +0 -979
- package/dist/chunk-KTGKNSKM.js.map +0 -1
- package/dist/chunk-WXDYCRTT.js +0 -234
- package/dist/chunk-WXDYCRTT.js.map +0 -1
- package/src/library/worker/loader.ts +0 -114
- package/src/preferences/shared.ts +0 -1
- package/src/project/lock.ts +0 -39
- package/src/secret/abstractions.ts +0 -59
- package/src/secret/factory.ts +0 -22
- package/src/secret/local.ts +0 -152
- package/src/shared/state.ts +0 -247
- package/src/shared/terminal.ts +0 -14
- package/src/state/local.ts +0 -612
- package/src/workspace/abstractions.ts +0 -41
- package/src/workspace/factory.ts +0 -14
- package/src/workspace/local.ts +0 -54
- /package/src/shared/{library.ts → models/backend/library.ts} +0 -0
package/src/terminal/docker.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TerminalRunOptions, TerminalBackend } from "./
|
|
1
|
+
import type { TerminalRunOptions, TerminalBackend } from "./abstractions"
|
|
2
2
|
import type { Logger } from "pino"
|
|
3
3
|
import { Readable } from "node:stream"
|
|
4
4
|
import { tmpdir } from "node:os"
|
|
@@ -9,9 +9,9 @@ import spawn from "nano-spawn"
|
|
|
9
9
|
import { runScript } from "./run.sh"
|
|
10
10
|
|
|
11
11
|
export const dockerTerminalBackendConfig = z.object({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
HIGHSTATE_TERMINAL_BACKEND_DOCKER_BINARY: z.string().default("docker"),
|
|
13
|
+
HIGHSTATE_TERMINAL_BACKEND_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
|
|
14
|
+
HIGHSTATE_TERMINAL_BACKEND_DOCKER_HOST: z.string().optional(),
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
export class DockerTerminalBackend implements TerminalBackend {
|
|
@@ -22,7 +22,7 @@ export class DockerTerminalBackend implements TerminalBackend {
|
|
|
22
22
|
private readonly logger: Logger,
|
|
23
23
|
) {}
|
|
24
24
|
|
|
25
|
-
async run({
|
|
25
|
+
async run({ spec, stdin, stdout, screenSize, signal }: TerminalRunOptions): Promise<void> {
|
|
26
26
|
const hsTempDir = resolve(tmpdir(), "highstate")
|
|
27
27
|
await mkdir(hsTempDir, { recursive: true })
|
|
28
28
|
|
|
@@ -34,19 +34,19 @@ export class DockerTerminalBackend implements TerminalBackend {
|
|
|
34
34
|
"-i",
|
|
35
35
|
"-v",
|
|
36
36
|
`${runScriptPath}:/run.sh:ro`,
|
|
37
|
-
|
|
37
|
+
spec.image,
|
|
38
38
|
"/bin/bash",
|
|
39
39
|
"/run.sh",
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
const initData = {
|
|
43
|
-
command:
|
|
44
|
-
cwd:
|
|
43
|
+
command: spec.command,
|
|
44
|
+
cwd: spec.cwd,
|
|
45
45
|
env: {
|
|
46
|
-
...
|
|
46
|
+
...spec.env,
|
|
47
47
|
TERM: "xterm-256color",
|
|
48
48
|
},
|
|
49
|
-
files:
|
|
49
|
+
files: spec.files ?? {},
|
|
50
50
|
screenSize,
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -71,7 +71,11 @@ export class DockerTerminalBackend implements TerminalBackend {
|
|
|
71
71
|
childProcess.stdout!.pipe(stdout)
|
|
72
72
|
childProcess.stderr!.pipe(stdout)
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
if (!childProcess.pid) {
|
|
75
|
+
throw new Error(`Failed to start Docker container without clear response from child process.`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.logger.info({ processId: childProcess.pid }, "process started")
|
|
75
79
|
|
|
76
80
|
await process
|
|
77
81
|
}
|
|
@@ -81,9 +85,9 @@ export class DockerTerminalBackend implements TerminalBackend {
|
|
|
81
85
|
logger: Logger,
|
|
82
86
|
): DockerTerminalBackend {
|
|
83
87
|
return new DockerTerminalBackend(
|
|
84
|
-
config.
|
|
85
|
-
config.
|
|
86
|
-
config.
|
|
88
|
+
config.HIGHSTATE_TERMINAL_BACKEND_DOCKER_BINARY,
|
|
89
|
+
config.HIGHSTATE_TERMINAL_BACKEND_DOCKER_USE_SUDO,
|
|
90
|
+
config.HIGHSTATE_TERMINAL_BACKEND_DOCKER_HOST,
|
|
87
91
|
logger.child({ backend: "TerminalBackend", service: "DockerTerminalBackend" }),
|
|
88
92
|
)
|
|
89
93
|
}
|
package/src/terminal/factory.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Logger } from "pino"
|
|
2
|
-
import type { TerminalBackend } from "./
|
|
2
|
+
import type { TerminalBackend } from "./abstractions"
|
|
3
3
|
import { z } from "zod"
|
|
4
4
|
import { DockerTerminalBackend, dockerTerminalBackendConfig } from "./docker"
|
|
5
5
|
|
|
6
6
|
export const terminalBackendConfig = z.object({
|
|
7
|
-
|
|
7
|
+
HIGHSTATE_TERMINAL_BACKEND_TYPE: z.enum(["docker"]).default("docker"),
|
|
8
8
|
...dockerTerminalBackendConfig.shape,
|
|
9
9
|
})
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ export function createTerminalBackend(
|
|
|
12
12
|
config: z.infer<typeof terminalBackendConfig>,
|
|
13
13
|
logger: Logger,
|
|
14
14
|
): TerminalBackend {
|
|
15
|
-
switch (config.
|
|
15
|
+
switch (config.HIGHSTATE_TERMINAL_BACKEND_TYPE) {
|
|
16
16
|
case "docker": {
|
|
17
17
|
return DockerTerminalBackend.create(config, logger)
|
|
18
18
|
}
|
package/src/terminal/index.ts
CHANGED
package/src/terminal/manager.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import type { ScreenSize, TerminalBackend } from "./
|
|
1
|
+
import type { ScreenSize, TerminalBackend } from "./abstractions"
|
|
2
2
|
import type { Logger } from "pino"
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
3
|
+
import type { PubSubManager } from "../pubsub"
|
|
4
|
+
import type { ProjectUnlockService } from "../business"
|
|
5
|
+
import type { StateManager } from "../state"
|
|
5
6
|
import { PassThrough, type Stream, type Writable } from "node:stream"
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { v7 as uuidv7 } from "uuid"
|
|
8
|
+
import {
|
|
9
|
+
type TerminalSession,
|
|
10
|
+
type TerminalSpec,
|
|
11
|
+
createAsyncBatcher,
|
|
12
|
+
type AsyncBatcher,
|
|
13
|
+
} from "../shared"
|
|
10
14
|
import { isAbortErrorLike } from "../common"
|
|
11
15
|
|
|
12
16
|
type ManagedTerminal = {
|
|
@@ -14,30 +18,31 @@ type ManagedTerminal = {
|
|
|
14
18
|
readonly abortController: AbortController
|
|
15
19
|
readonly stdin: PassThrough
|
|
16
20
|
readonly stdout: PassThrough
|
|
21
|
+
readonly logBatcher: AsyncBatcher<string>
|
|
17
22
|
|
|
18
23
|
refCount: number
|
|
19
24
|
started: boolean
|
|
20
25
|
start(screenSize: ScreenSize): void
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
type TerminalEvents = {
|
|
24
|
-
[sessionId: string]: [terminal: TerminalSession]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
28
|
const notAttachedTerminalLifetime = 5 * 60 * 1000 // 5 minutes
|
|
28
29
|
|
|
29
30
|
export class TerminalManager {
|
|
30
31
|
private readonly managedTerminals = new Map<string, ManagedTerminal>()
|
|
31
32
|
private readonly existingSessions = new Map<string, TerminalSession>()
|
|
32
|
-
private readonly terminalEE = new EventEmitter<TerminalEvents>()
|
|
33
33
|
|
|
34
34
|
private constructor(
|
|
35
35
|
private readonly terminalBackend: TerminalBackend,
|
|
36
|
-
private readonly
|
|
37
|
-
private readonly
|
|
36
|
+
private readonly stateManager: StateManager,
|
|
37
|
+
private readonly pubsubManager: PubSubManager,
|
|
38
|
+
private readonly projectUnlockService: ProjectUnlockService,
|
|
38
39
|
private readonly logger: Logger,
|
|
39
40
|
) {
|
|
40
|
-
|
|
41
|
+
this.projectUnlockService.registerUnlockTask(
|
|
42
|
+
//
|
|
43
|
+
"mark-finished-terminal-sessions",
|
|
44
|
+
projectId => this.markFinishedSessions(projectId),
|
|
45
|
+
)
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
public isSessionActive(sessionId: string): boolean {
|
|
@@ -46,7 +51,6 @@ export class TerminalManager {
|
|
|
46
51
|
|
|
47
52
|
public async *watchSession(
|
|
48
53
|
projectId: string,
|
|
49
|
-
instanceId: string,
|
|
50
54
|
sessionId: string,
|
|
51
55
|
signal?: AbortSignal,
|
|
52
56
|
): AsyncIterable<TerminalSession> {
|
|
@@ -54,7 +58,7 @@ export class TerminalManager {
|
|
|
54
58
|
|
|
55
59
|
// if terminal session is not active, just fetch it, return and stop watching
|
|
56
60
|
if (!managedTerminal) {
|
|
57
|
-
const session = await this.
|
|
61
|
+
const session = await this.stateManager.getTerminalSessionRepository(projectId).get(sessionId)
|
|
58
62
|
|
|
59
63
|
if (!session) {
|
|
60
64
|
throw new Error(`Terminal session "${sessionId}" not found`)
|
|
@@ -66,67 +70,86 @@ export class TerminalManager {
|
|
|
66
70
|
|
|
67
71
|
yield managedTerminal.session
|
|
68
72
|
|
|
69
|
-
for await (const
|
|
73
|
+
for await (const terminalSession of this.pubsubManager.subscribe(
|
|
74
|
+
["active-terminal-session", projectId, sessionId],
|
|
70
75
|
signal,
|
|
71
|
-
|
|
76
|
+
)) {
|
|
72
77
|
yield terminalSession
|
|
73
78
|
|
|
74
|
-
if (terminalSession.finishedAt) {
|
|
79
|
+
if (terminalSession.meta.finishedAt) {
|
|
75
80
|
// terminal session is finished, stop watching
|
|
76
81
|
return
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
public async createSession(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
): Promise<TerminalSession> {
|
|
86
|
-
const [instanceType, instanceName] = parseInstanceId(instanceId)
|
|
87
|
-
|
|
88
|
-
const factory = await this.runnerBackend.getTerminalFactory(
|
|
89
|
-
{ projectId, instanceType, instanceName },
|
|
90
|
-
terminalName,
|
|
91
|
-
)
|
|
86
|
+
public async createSession(projectId: string, terminalId: string): Promise<TerminalSession> {
|
|
87
|
+
// Get the terminal info from info repository for meta data
|
|
88
|
+
const terminalInfoRepo = this.stateManager.getTerminalRepository(projectId)
|
|
89
|
+
const terminal = await terminalInfoRepo.get(terminalId)
|
|
92
90
|
|
|
93
|
-
if (!
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Terminal factory for instance "${instanceId}" with name "${terminalName}" not found`,
|
|
96
|
-
)
|
|
91
|
+
if (!terminal) {
|
|
92
|
+
throw new Error(`Terminal "${terminalId}" not found`)
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
const terminalSession: TerminalSession = {
|
|
100
96
|
id: uuidv7(),
|
|
101
97
|
projectId,
|
|
102
|
-
instanceId,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
instanceId: terminal.instanceId,
|
|
99
|
+
terminalId,
|
|
100
|
+
meta: {
|
|
101
|
+
title: terminal.meta.title,
|
|
102
|
+
description: terminal.meta.description,
|
|
103
|
+
icon: terminal.meta.icon,
|
|
104
|
+
},
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
|
|
107
|
+
// Get the terminal spec for execution
|
|
108
|
+
const terminalSpec = await this.stateManager
|
|
109
|
+
.getTerminalSpecRepository(projectId)
|
|
110
|
+
.get(terminalId)
|
|
111
|
+
|
|
112
|
+
if (!terminalSpec) {
|
|
113
|
+
throw new Error(`No spec found for terminal "${terminalId}"`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const batch = this.stateManager.batch()
|
|
117
|
+
const promises: Promise<void>[] = [
|
|
118
|
+
this.stateManager.getTerminalSessionRepository(projectId).putItem(terminalSession, batch),
|
|
119
|
+
this.stateManager
|
|
120
|
+
.getActiveTerminalSessionIndexRepository(projectId)
|
|
121
|
+
.indexRepository.put(terminalSession.id, terminalSession.id, batch),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
if (terminal.instanceId) {
|
|
125
|
+
promises.push(
|
|
126
|
+
this.stateManager
|
|
127
|
+
.getInstanceTerminalSessionIndexRepository(projectId, terminal.instanceId)
|
|
128
|
+
.indexRepository.put(terminalSession.id, terminalSession.id, batch),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await Promise.allSettled(promises)
|
|
133
|
+
await batch.write()
|
|
110
134
|
this.logger.info({ msg: "terminal session created", id: terminalSession.id })
|
|
111
135
|
|
|
112
|
-
this.createManagedTerminal(
|
|
136
|
+
this.createManagedTerminal(terminalSpec, terminalSession)
|
|
113
137
|
|
|
114
138
|
return terminalSession
|
|
115
139
|
}
|
|
116
140
|
|
|
117
141
|
public async getOrCreateSession(
|
|
118
142
|
projectId: string,
|
|
119
|
-
|
|
120
|
-
terminalName: string,
|
|
143
|
+
terminalId: string,
|
|
121
144
|
newSession = false,
|
|
122
145
|
): Promise<TerminalSession> {
|
|
123
|
-
const existingSession = this.existingSessions.get(`${projectId}:${
|
|
146
|
+
const existingSession = this.existingSessions.get(`${projectId}:${terminalId}`)
|
|
124
147
|
|
|
125
148
|
if (existingSession && !newSession) {
|
|
126
149
|
return existingSession
|
|
127
150
|
}
|
|
128
151
|
|
|
129
|
-
return await this.createSession(projectId,
|
|
152
|
+
return await this.createSession(projectId, terminalId)
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
public close(sessionId: string): void {
|
|
@@ -189,13 +212,11 @@ export class TerminalManager {
|
|
|
189
212
|
}
|
|
190
213
|
|
|
191
214
|
private async updateActiveTerminalSessions(): Promise<void> {
|
|
192
|
-
|
|
193
|
-
Array.from(this.managedTerminals.values()).map(t => t.session),
|
|
194
|
-
)
|
|
215
|
+
// TODO
|
|
195
216
|
}
|
|
196
217
|
|
|
197
218
|
private createManagedTerminal(
|
|
198
|
-
|
|
219
|
+
spec: TerminalSpec,
|
|
199
220
|
terminalSession: TerminalSession,
|
|
200
221
|
): ManagedTerminal {
|
|
201
222
|
const managedTerminal: ManagedTerminal = {
|
|
@@ -204,13 +225,21 @@ export class TerminalManager {
|
|
|
204
225
|
stdin: new PassThrough(),
|
|
205
226
|
stdout: new PassThrough(),
|
|
206
227
|
|
|
228
|
+
logBatcher: createAsyncBatcher(async (entries: string[]) => {
|
|
229
|
+
this.logger.trace({ msg: "persisting terminal log entries", count: entries.length })
|
|
230
|
+
|
|
231
|
+
await this.stateManager
|
|
232
|
+
.getTerminalSessionLineRepository(terminalSession.projectId, terminalSession.id)
|
|
233
|
+
.putMany(entries.map(entry => [uuidv7(), entry]))
|
|
234
|
+
}),
|
|
235
|
+
|
|
207
236
|
refCount: 0,
|
|
208
237
|
started: false,
|
|
209
238
|
|
|
210
239
|
start: (screenSize: ScreenSize) => {
|
|
211
240
|
void this.terminalBackend
|
|
212
241
|
.run({
|
|
213
|
-
|
|
242
|
+
spec,
|
|
214
243
|
stdin: managedTerminal.stdin,
|
|
215
244
|
stdout: managedTerminal.stdout,
|
|
216
245
|
screenSize,
|
|
@@ -231,19 +260,25 @@ export class TerminalManager {
|
|
|
231
260
|
this.logger.info({ msg: "managed terminal closed", id: managedTerminal.session.id })
|
|
232
261
|
this.managedTerminals.delete(managedTerminal.session.id)
|
|
233
262
|
this.existingSessions.delete(
|
|
234
|
-
`${managedTerminal.session.projectId}:${managedTerminal.session.
|
|
263
|
+
`${managedTerminal.session.projectId}:${managedTerminal.session.terminalId}`,
|
|
235
264
|
)
|
|
236
265
|
|
|
237
|
-
managedTerminal.session.finishedAt =
|
|
238
|
-
this.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
266
|
+
managedTerminal.session.meta.finishedAt = Date.now()
|
|
267
|
+
void this.pubsubManager.publish(
|
|
268
|
+
[
|
|
269
|
+
"active-terminal-session",
|
|
270
|
+
managedTerminal.session.projectId,
|
|
271
|
+
managedTerminal.session.id,
|
|
272
|
+
],
|
|
243
273
|
managedTerminal.session,
|
|
244
274
|
)
|
|
245
275
|
|
|
276
|
+
void this.stateManager
|
|
277
|
+
.getTerminalSessionRepository(managedTerminal.session.projectId)
|
|
278
|
+
.putItem(managedTerminal.session)
|
|
279
|
+
|
|
246
280
|
void this.updateActiveTerminalSessions()
|
|
281
|
+
void managedTerminal.logBatcher.flush()
|
|
247
282
|
})
|
|
248
283
|
|
|
249
284
|
setTimeout(
|
|
@@ -258,12 +293,12 @@ export class TerminalManager {
|
|
|
258
293
|
managedTerminal.stdout.on("data", data => {
|
|
259
294
|
const entry = String(data)
|
|
260
295
|
|
|
261
|
-
|
|
296
|
+
managedTerminal.logBatcher.call(entry)
|
|
262
297
|
})
|
|
263
298
|
|
|
264
299
|
this.managedTerminals.set(managedTerminal.session.id, managedTerminal)
|
|
265
300
|
this.existingSessions.set(
|
|
266
|
-
`${managedTerminal.session.projectId}:${managedTerminal.session.
|
|
301
|
+
`${managedTerminal.session.projectId}:${managedTerminal.session.terminalId}`,
|
|
267
302
|
managedTerminal.session,
|
|
268
303
|
)
|
|
269
304
|
|
|
@@ -286,9 +321,7 @@ export class TerminalManager {
|
|
|
286
321
|
|
|
287
322
|
terminal.abortController.abort()
|
|
288
323
|
this.managedTerminals.delete(terminal.session.id)
|
|
289
|
-
this.existingSessions.delete(
|
|
290
|
-
`${terminal.session.projectId}:${terminal.session.instanceId}.${terminal.session.terminalName}`,
|
|
291
|
-
)
|
|
324
|
+
this.existingSessions.delete(`${terminal.session.projectId}:${terminal.session.terminalId}`)
|
|
292
325
|
return
|
|
293
326
|
}
|
|
294
327
|
|
|
@@ -297,36 +330,55 @@ export class TerminalManager {
|
|
|
297
330
|
|
|
298
331
|
static create(
|
|
299
332
|
terminalBackend: TerminalBackend,
|
|
300
|
-
stateBackend:
|
|
301
|
-
|
|
333
|
+
stateBackend: StateManager,
|
|
334
|
+
pubsubManager: PubSubManager,
|
|
335
|
+
projectUnlockService: ProjectUnlockService,
|
|
302
336
|
logger: Logger,
|
|
303
337
|
): TerminalManager {
|
|
304
338
|
return new TerminalManager(
|
|
305
339
|
terminalBackend,
|
|
306
340
|
stateBackend,
|
|
307
|
-
|
|
341
|
+
pubsubManager,
|
|
342
|
+
projectUnlockService,
|
|
308
343
|
logger.child({ service: "TerminalManager" }),
|
|
309
344
|
)
|
|
310
345
|
}
|
|
311
346
|
|
|
312
|
-
private
|
|
313
|
-
|
|
347
|
+
private async markFinishedSessions(projectId: string): Promise<void> {
|
|
348
|
+
const activeSessions = await this.stateManager
|
|
349
|
+
.getActiveTerminalSessionIndexRepository(projectId)
|
|
350
|
+
.getAllItems()
|
|
314
351
|
|
|
315
|
-
|
|
352
|
+
if (activeSessions.length === 0) {
|
|
353
|
+
this.logger.debug({ projectId }, "no lost terminal sessions found")
|
|
354
|
+
return
|
|
355
|
+
}
|
|
316
356
|
|
|
317
|
-
|
|
318
|
-
})
|
|
357
|
+
const batch = this.stateManager.batch()
|
|
319
358
|
|
|
320
|
-
|
|
321
|
-
|
|
359
|
+
for (const session of activeSessions) {
|
|
360
|
+
if (session.meta.finishedAt) {
|
|
361
|
+
// Already marked as finished
|
|
362
|
+
this.logger.debug({ sessionId: session.id }, "session already marked as finished")
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
322
365
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
366
|
+
session.meta.finishedAt = Date.now()
|
|
367
|
+
|
|
368
|
+
// mark session as finished and delete from active index
|
|
369
|
+
await Promise.all([
|
|
370
|
+
this.stateManager.getTerminalSessionRepository(projectId).putItem(session, batch),
|
|
371
|
+
this.stateManager
|
|
372
|
+
.getActiveTerminalSessionIndexRepository(projectId)
|
|
373
|
+
.indexRepository.delete(session.id, batch),
|
|
374
|
+
])
|
|
328
375
|
}
|
|
329
376
|
|
|
330
|
-
await
|
|
377
|
+
await batch.write()
|
|
378
|
+
|
|
379
|
+
this.logger.debug(
|
|
380
|
+
{ projectId, count: activeSessions.length },
|
|
381
|
+
"marked terminal sessions as finished",
|
|
382
|
+
)
|
|
331
383
|
}
|
|
332
384
|
}
|
package/src/terminal/run.sh.ts
CHANGED
|
@@ -16,20 +16,30 @@ done
|
|
|
16
16
|
|
|
17
17
|
# Create files
|
|
18
18
|
for key in "\${filesKeys[@]}"; do
|
|
19
|
-
|
|
20
|
-
content=$(jq -r ".files[\\"$key\\"].content" <<<"$data")
|
|
21
|
-
mode=$(jq -r ".files[\\"$key\\"].mode // 0" <<<"$data")
|
|
19
|
+
contentType=$(jq -r ".files[\\"$key\\"].content.type" <<<"$data")
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
echo "$content" | base64 -d > "$key"
|
|
27
|
-
else
|
|
28
|
-
echo "$content" > "$key"
|
|
21
|
+
# Skip artifact type files for now
|
|
22
|
+
if [ "$contentType" = "artifact" ]; then
|
|
23
|
+
continue
|
|
29
24
|
fi
|
|
25
|
+
|
|
26
|
+
# Handle embedded content
|
|
27
|
+
if [ "$contentType" = "embedded" ]; then
|
|
28
|
+
content=$(jq -r ".files[\\"$key\\"].content.value" <<<"$data")
|
|
29
|
+
isBinary=$(jq -r ".files[\\"$key\\"].meta.isBinary // false" <<<"$data")
|
|
30
|
+
mode=$(jq -r ".files[\\"$key\\"].meta.mode // 0" <<<"$data")
|
|
31
|
+
|
|
32
|
+
mkdir -p "$(dirname "$key")"
|
|
33
|
+
|
|
34
|
+
if [ "$isBinary" = "true" ]; then
|
|
35
|
+
echo "$content" | base64 -d > "$key"
|
|
36
|
+
else
|
|
37
|
+
echo "$content" > "$key"
|
|
38
|
+
fi
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
if [ "$mode" -ne 0 ]; then
|
|
41
|
+
chmod $(printf "%o" "$mode") "$key"
|
|
42
|
+
fi
|
|
33
43
|
fi
|
|
34
44
|
done
|
|
35
45
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Worker } from "../shared"
|
|
2
|
+
|
|
3
|
+
export type WorkerRunOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* The ID of the project to run the worker for.
|
|
6
|
+
*/
|
|
7
|
+
projectId: string
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The worker to run.
|
|
11
|
+
*/
|
|
12
|
+
worker: Worker
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The token of the API key to pass to the worker.
|
|
16
|
+
*/
|
|
17
|
+
apiKey: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The path of the API socket to pass to the worker.
|
|
21
|
+
*/
|
|
22
|
+
apiPath: string
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The output stream.
|
|
26
|
+
*/
|
|
27
|
+
stdout: NodeJS.WritableStream
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The signal to abort the worker.
|
|
31
|
+
*/
|
|
32
|
+
signal?: AbortSignal
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WorkerBackend {
|
|
36
|
+
/**
|
|
37
|
+
* Runs a worker with the given options.
|
|
38
|
+
*
|
|
39
|
+
* @param options The options.
|
|
40
|
+
*/
|
|
41
|
+
run(options: WorkerRunOptions): Promise<void>
|
|
42
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import type { WorkerBackend, WorkerRunOptions } from "./abstractions"
|
|
3
|
+
import { Readable } from "node:stream"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import spawn from "nano-spawn"
|
|
6
|
+
|
|
7
|
+
export const dockerWorkerBackendConfig = z.object({
|
|
8
|
+
HIGHSTATE_WORKER_BACKEND_DOCKER_BINARY: z.string().default("docker"),
|
|
9
|
+
HIGHSTATE_WORKER_BACKEND_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
|
|
10
|
+
HIGHSTATE_WORKER_BACKEND_DOCKER_HOST: z.string().optional(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export class DockerWorkerBackend implements WorkerBackend {
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly binary: string,
|
|
16
|
+
private readonly useSudo: boolean,
|
|
17
|
+
private readonly host: string | undefined,
|
|
18
|
+
private readonly logger: Logger,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
async run({
|
|
22
|
+
projectId,
|
|
23
|
+
worker,
|
|
24
|
+
apiKey,
|
|
25
|
+
apiPath,
|
|
26
|
+
stdout,
|
|
27
|
+
signal,
|
|
28
|
+
}: WorkerRunOptions): Promise<void> {
|
|
29
|
+
const args = [
|
|
30
|
+
"run",
|
|
31
|
+
"-i",
|
|
32
|
+
"-v",
|
|
33
|
+
`${apiPath}:/var/run/highstate.sock`,
|
|
34
|
+
worker.image,
|
|
35
|
+
"/bin/bash",
|
|
36
|
+
"/run.sh",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const initData = {
|
|
40
|
+
projectId,
|
|
41
|
+
workerId: worker.id,
|
|
42
|
+
apiKey,
|
|
43
|
+
apiUrl: "/var/run/highstate.sock",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const initDataStream = Readable.from(JSON.stringify(initData) + "\n")
|
|
47
|
+
|
|
48
|
+
if (this.useSudo) {
|
|
49
|
+
args.unshift(this.binary)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const process = spawn(this.useSudo ? "sudo" : this.binary, args, {
|
|
53
|
+
env: {
|
|
54
|
+
DOCKER_HOST: this.host,
|
|
55
|
+
},
|
|
56
|
+
signal,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const childProcess = await process.nodeChildProcess
|
|
60
|
+
|
|
61
|
+
initDataStream.pipe(childProcess.stdin!)
|
|
62
|
+
|
|
63
|
+
childProcess.stdout!.pipe(stdout)
|
|
64
|
+
childProcess.stderr!.pipe(stdout)
|
|
65
|
+
|
|
66
|
+
if (!childProcess.pid) {
|
|
67
|
+
throw new Error(`Failed to start Docker container without clear response from child process.`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.logger.info({ processId: childProcess.pid }, "process started")
|
|
71
|
+
|
|
72
|
+
await process
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static create(config: z.infer<typeof dockerWorkerBackendConfig>, logger: Logger): WorkerBackend {
|
|
76
|
+
return new DockerWorkerBackend(
|
|
77
|
+
config.HIGHSTATE_WORKER_BACKEND_DOCKER_BINARY,
|
|
78
|
+
config.HIGHSTATE_WORKER_BACKEND_DOCKER_USE_SUDO,
|
|
79
|
+
config.HIGHSTATE_WORKER_BACKEND_DOCKER_HOST,
|
|
80
|
+
logger.child({ backend: "WorkerBackend", service: "DockerWorkerBackend" }),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import type { WorkerBackend } from "./abstractions"
|
|
3
|
+
import { z } from "zod"
|
|
4
|
+
import { DockerWorkerBackend, dockerWorkerBackendConfig } from "./docker"
|
|
5
|
+
|
|
6
|
+
export const workerBackendConfig = z.object({
|
|
7
|
+
HIGHSTATE_WORKER_BACKEND_TYPE: z.enum(["docker"]).default("docker"),
|
|
8
|
+
...dockerWorkerBackendConfig.shape,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export function createWorkerBackend(
|
|
12
|
+
config: z.infer<typeof workerBackendConfig>,
|
|
13
|
+
logger: Logger,
|
|
14
|
+
): WorkerBackend {
|
|
15
|
+
switch (config.HIGHSTATE_WORKER_BACKEND_TYPE) {
|
|
16
|
+
case "docker": {
|
|
17
|
+
return DockerWorkerBackend.create(config, logger)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|