@highstate/backend 0.9.14 → 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 +6146 -2174
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +51 -159
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +159 -43
- 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 +14 -47
- 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 +131 -82
- package/src/orchestrator/operation-workset.ts +188 -77
- package/src/orchestrator/operation.ts +975 -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 -372
- 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} +43 -8
- 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 +74 -13
- 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 +7 -2
- package/src/shared/resolvers/state.ts +12 -0
- 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 +134 -80
- package/src/terminal/run.sh.ts +22 -10
- 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-C2TJAQAD.js +0 -937
- package/dist/chunk-C2TJAQAD.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 -270
- package/src/shared/terminal.ts +0 -13
- 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,65 +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
|
-
id:
|
|
96
|
+
id: uuidv7(),
|
|
101
97
|
projectId,
|
|
102
|
-
instanceId,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
instanceId: terminal.instanceId,
|
|
99
|
+
terminalId,
|
|
100
|
+
meta: {
|
|
101
|
+
title: terminal.meta.title,
|
|
102
|
+
description: terminal.meta.description,
|
|
103
|
+
icon: terminal.meta.icon,
|
|
104
|
+
},
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
|
|
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()
|
|
109
134
|
this.logger.info({ msg: "terminal session created", id: terminalSession.id })
|
|
110
135
|
|
|
111
|
-
this.createManagedTerminal(
|
|
136
|
+
this.createManagedTerminal(terminalSpec, terminalSession)
|
|
112
137
|
|
|
113
138
|
return terminalSession
|
|
114
139
|
}
|
|
115
140
|
|
|
116
141
|
public async getOrCreateSession(
|
|
117
142
|
projectId: string,
|
|
118
|
-
|
|
119
|
-
|
|
143
|
+
terminalId: string,
|
|
144
|
+
newSession = false,
|
|
120
145
|
): Promise<TerminalSession> {
|
|
121
|
-
const existingSession = this.existingSessions.get(`${projectId}:${
|
|
146
|
+
const existingSession = this.existingSessions.get(`${projectId}:${terminalId}`)
|
|
122
147
|
|
|
123
|
-
if (existingSession) {
|
|
148
|
+
if (existingSession && !newSession) {
|
|
124
149
|
return existingSession
|
|
125
150
|
}
|
|
126
151
|
|
|
127
|
-
return await this.createSession(projectId,
|
|
152
|
+
return await this.createSession(projectId, terminalId)
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
public close(sessionId: string): void {
|
|
@@ -187,13 +212,11 @@ export class TerminalManager {
|
|
|
187
212
|
}
|
|
188
213
|
|
|
189
214
|
private async updateActiveTerminalSessions(): Promise<void> {
|
|
190
|
-
|
|
191
|
-
Array.from(this.managedTerminals.values()).map(t => t.session),
|
|
192
|
-
)
|
|
215
|
+
// TODO
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
private createManagedTerminal(
|
|
196
|
-
|
|
219
|
+
spec: TerminalSpec,
|
|
197
220
|
terminalSession: TerminalSession,
|
|
198
221
|
): ManagedTerminal {
|
|
199
222
|
const managedTerminal: ManagedTerminal = {
|
|
@@ -202,13 +225,21 @@ export class TerminalManager {
|
|
|
202
225
|
stdin: new PassThrough(),
|
|
203
226
|
stdout: new PassThrough(),
|
|
204
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
|
+
|
|
205
236
|
refCount: 0,
|
|
206
237
|
started: false,
|
|
207
238
|
|
|
208
239
|
start: (screenSize: ScreenSize) => {
|
|
209
240
|
void this.terminalBackend
|
|
210
241
|
.run({
|
|
211
|
-
|
|
242
|
+
spec,
|
|
212
243
|
stdin: managedTerminal.stdin,
|
|
213
244
|
stdout: managedTerminal.stdout,
|
|
214
245
|
screenSize,
|
|
@@ -229,19 +260,25 @@ export class TerminalManager {
|
|
|
229
260
|
this.logger.info({ msg: "managed terminal closed", id: managedTerminal.session.id })
|
|
230
261
|
this.managedTerminals.delete(managedTerminal.session.id)
|
|
231
262
|
this.existingSessions.delete(
|
|
232
|
-
`${managedTerminal.session.projectId}:${managedTerminal.session.
|
|
263
|
+
`${managedTerminal.session.projectId}:${managedTerminal.session.terminalId}`,
|
|
233
264
|
)
|
|
234
265
|
|
|
235
|
-
managedTerminal.session.finishedAt =
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
],
|
|
241
273
|
managedTerminal.session,
|
|
242
274
|
)
|
|
243
275
|
|
|
276
|
+
void this.stateManager
|
|
277
|
+
.getTerminalSessionRepository(managedTerminal.session.projectId)
|
|
278
|
+
.putItem(managedTerminal.session)
|
|
279
|
+
|
|
244
280
|
void this.updateActiveTerminalSessions()
|
|
281
|
+
void managedTerminal.logBatcher.flush()
|
|
245
282
|
})
|
|
246
283
|
|
|
247
284
|
setTimeout(
|
|
@@ -256,12 +293,12 @@ export class TerminalManager {
|
|
|
256
293
|
managedTerminal.stdout.on("data", data => {
|
|
257
294
|
const entry = String(data)
|
|
258
295
|
|
|
259
|
-
|
|
296
|
+
managedTerminal.logBatcher.call(entry)
|
|
260
297
|
})
|
|
261
298
|
|
|
262
299
|
this.managedTerminals.set(managedTerminal.session.id, managedTerminal)
|
|
263
300
|
this.existingSessions.set(
|
|
264
|
-
`${managedTerminal.session.projectId}:${managedTerminal.session.
|
|
301
|
+
`${managedTerminal.session.projectId}:${managedTerminal.session.terminalId}`,
|
|
265
302
|
managedTerminal.session,
|
|
266
303
|
)
|
|
267
304
|
|
|
@@ -284,9 +321,7 @@ export class TerminalManager {
|
|
|
284
321
|
|
|
285
322
|
terminal.abortController.abort()
|
|
286
323
|
this.managedTerminals.delete(terminal.session.id)
|
|
287
|
-
this.existingSessions.delete(
|
|
288
|
-
`${terminal.session.projectId}:${terminal.session.instanceId}.${terminal.session.terminalName}`,
|
|
289
|
-
)
|
|
324
|
+
this.existingSessions.delete(`${terminal.session.projectId}:${terminal.session.terminalId}`)
|
|
290
325
|
return
|
|
291
326
|
}
|
|
292
327
|
|
|
@@ -295,36 +330,55 @@ export class TerminalManager {
|
|
|
295
330
|
|
|
296
331
|
static create(
|
|
297
332
|
terminalBackend: TerminalBackend,
|
|
298
|
-
stateBackend:
|
|
299
|
-
|
|
333
|
+
stateBackend: StateManager,
|
|
334
|
+
pubsubManager: PubSubManager,
|
|
335
|
+
projectUnlockService: ProjectUnlockService,
|
|
300
336
|
logger: Logger,
|
|
301
337
|
): TerminalManager {
|
|
302
338
|
return new TerminalManager(
|
|
303
339
|
terminalBackend,
|
|
304
340
|
stateBackend,
|
|
305
|
-
|
|
341
|
+
pubsubManager,
|
|
342
|
+
projectUnlockService,
|
|
306
343
|
logger.child({ service: "TerminalManager" }),
|
|
307
344
|
)
|
|
308
345
|
}
|
|
309
346
|
|
|
310
|
-
private
|
|
311
|
-
|
|
347
|
+
private async markFinishedSessions(projectId: string): Promise<void> {
|
|
348
|
+
const activeSessions = await this.stateManager
|
|
349
|
+
.getActiveTerminalSessionIndexRepository(projectId)
|
|
350
|
+
.getAllItems()
|
|
312
351
|
|
|
313
|
-
|
|
352
|
+
if (activeSessions.length === 0) {
|
|
353
|
+
this.logger.debug({ projectId }, "no lost terminal sessions found")
|
|
354
|
+
return
|
|
355
|
+
}
|
|
314
356
|
|
|
315
|
-
|
|
316
|
-
})
|
|
357
|
+
const batch = this.stateManager.batch()
|
|
317
358
|
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
}
|
|
320
365
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
])
|
|
326
375
|
}
|
|
327
376
|
|
|
328
|
-
await
|
|
377
|
+
await batch.write()
|
|
378
|
+
|
|
379
|
+
this.logger.debug(
|
|
380
|
+
{ projectId, count: activeSessions.length },
|
|
381
|
+
"marked terminal sessions as finished",
|
|
382
|
+
)
|
|
329
383
|
}
|
|
330
384
|
}
|
package/src/terminal/run.sh.ts
CHANGED
|
@@ -16,18 +16,30 @@ done
|
|
|
16
16
|
|
|
17
17
|
# Create files
|
|
18
18
|
for key in "\${filesKeys[@]}"; do
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
echo "$content" | base64 -d > "$key"
|
|
25
|
-
else
|
|
26
|
-
echo "$content" > "$key"
|
|
19
|
+
contentType=$(jq -r ".files[\\"$key\\"].content.type" <<<"$data")
|
|
20
|
+
|
|
21
|
+
# Skip artifact type files for now
|
|
22
|
+
if [ "$contentType" = "artifact" ]; then
|
|
23
|
+
continue
|
|
27
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
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
if [ "$mode" -ne 0 ]; then
|
|
41
|
+
chmod $(printf "%o" "$mode") "$key"
|
|
42
|
+
fi
|
|
31
43
|
fi
|
|
32
44
|
done
|
|
33
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
|
+
}
|