@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.
Files changed (144) hide show
  1. package/dist/chunk-RCB4AFGD.js +159 -0
  2. package/dist/chunk-RCB4AFGD.js.map +1 -0
  3. package/dist/chunk-WHALQHEZ.js +2017 -0
  4. package/dist/chunk-WHALQHEZ.js.map +1 -0
  5. package/dist/highstate.manifest.json +3 -3
  6. package/dist/index.js +6146 -2174
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +51 -159
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +159 -43
  11. package/package.json +25 -7
  12. package/src/artifact/abstractions.ts +46 -0
  13. package/src/artifact/encryption.ts +69 -0
  14. package/src/artifact/factory.ts +36 -0
  15. package/src/artifact/index.ts +3 -0
  16. package/src/artifact/local.ts +142 -0
  17. package/src/business/api-key.ts +65 -0
  18. package/src/business/artifact.ts +288 -0
  19. package/src/business/backend-unlock.ts +10 -0
  20. package/src/business/index.ts +9 -0
  21. package/src/business/instance-lock.ts +124 -0
  22. package/src/business/instance-state.ts +292 -0
  23. package/src/business/operation.ts +251 -0
  24. package/src/business/project-unlock.ts +242 -0
  25. package/src/business/secret.ts +187 -0
  26. package/src/business/worker.ts +161 -0
  27. package/src/common/index.ts +2 -1
  28. package/src/common/performance.ts +44 -0
  29. package/src/common/tree.ts +33 -0
  30. package/src/common/utils.ts +40 -1
  31. package/src/config.ts +14 -10
  32. package/src/hotstate/abstractions.ts +48 -0
  33. package/src/hotstate/factory.ts +17 -0
  34. package/src/{secret → hotstate}/index.ts +1 -0
  35. package/src/hotstate/manager.ts +192 -0
  36. package/src/hotstate/memory.ts +100 -0
  37. package/src/hotstate/validation.ts +101 -0
  38. package/src/index.ts +2 -1
  39. package/src/library/abstractions.ts +10 -23
  40. package/src/library/factory.ts +2 -2
  41. package/src/library/local.ts +89 -102
  42. package/src/library/worker/evaluator.ts +14 -47
  43. package/src/library/worker/loader.lite.ts +41 -0
  44. package/src/library/worker/main.ts +14 -65
  45. package/src/library/worker/protocol.ts +8 -24
  46. package/src/lock/abstractions.ts +6 -0
  47. package/src/lock/factory.ts +15 -0
  48. package/src/{workspace → lock}/index.ts +1 -0
  49. package/src/lock/manager.ts +82 -0
  50. package/src/lock/memory.ts +19 -0
  51. package/src/orchestrator/manager.ts +131 -82
  52. package/src/orchestrator/operation-workset.ts +188 -77
  53. package/src/orchestrator/operation.ts +975 -284
  54. package/src/project/abstractions.ts +20 -7
  55. package/src/project/factory.ts +1 -1
  56. package/src/project/index.ts +0 -1
  57. package/src/project/local.ts +73 -17
  58. package/src/project/manager.ts +272 -131
  59. package/src/pubsub/abstractions.ts +13 -0
  60. package/src/pubsub/factory.ts +19 -0
  61. package/src/pubsub/index.ts +3 -0
  62. package/src/pubsub/local.ts +36 -0
  63. package/src/pubsub/manager.ts +100 -0
  64. package/src/pubsub/validation.ts +33 -0
  65. package/src/runner/abstractions.ts +135 -68
  66. package/src/runner/artifact-env.ts +160 -0
  67. package/src/runner/factory.ts +20 -5
  68. package/src/runner/force-abort.ts +117 -0
  69. package/src/runner/local.ts +281 -372
  70. package/src/{common → runner}/pulumi.ts +86 -37
  71. package/src/services.ts +193 -35
  72. package/src/shared/index.ts +3 -11
  73. package/src/shared/models/backend/index.ts +3 -0
  74. package/src/shared/models/backend/project.ts +63 -0
  75. package/src/shared/models/backend/unlock-method.ts +20 -0
  76. package/src/shared/models/base.ts +151 -0
  77. package/src/shared/models/errors.ts +5 -0
  78. package/src/shared/models/index.ts +4 -0
  79. package/src/shared/models/project/api-key.ts +62 -0
  80. package/src/shared/models/project/artifact.ts +113 -0
  81. package/src/shared/models/project/component.ts +45 -0
  82. package/src/shared/models/project/index.ts +14 -0
  83. package/src/shared/{project.ts → models/project/instance.ts} +12 -0
  84. package/src/shared/models/project/lock.ts +91 -0
  85. package/src/shared/{operation.ts → models/project/operation.ts} +43 -8
  86. package/src/shared/models/project/page.ts +57 -0
  87. package/src/shared/models/project/secret.ts +112 -0
  88. package/src/shared/models/project/service-account.ts +22 -0
  89. package/src/shared/models/project/state.ts +432 -0
  90. package/src/shared/models/project/terminal.ts +99 -0
  91. package/src/shared/models/project/trigger.ts +56 -0
  92. package/src/shared/models/project/unlock-method.ts +31 -0
  93. package/src/shared/models/project/worker.ts +105 -0
  94. package/src/shared/resolvers/graph-resolver.ts +74 -13
  95. package/src/shared/resolvers/index.ts +5 -0
  96. package/src/shared/resolvers/input-hash.ts +53 -15
  97. package/src/shared/resolvers/input.ts +1 -9
  98. package/src/shared/resolvers/registry.ts +7 -2
  99. package/src/shared/resolvers/state.ts +12 -0
  100. package/src/shared/resolvers/validation.ts +61 -20
  101. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  102. package/src/shared/utils/hash.ts +6 -0
  103. package/src/shared/utils/index.ts +3 -0
  104. package/src/shared/utils/promise-tracker.ts +23 -0
  105. package/src/state/abstractions.ts +330 -101
  106. package/src/state/encryption.ts +59 -0
  107. package/src/state/factory.ts +3 -5
  108. package/src/state/index.ts +3 -0
  109. package/src/state/keyring.ts +22 -0
  110. package/src/state/local/backend.ts +299 -0
  111. package/src/state/local/collection.ts +342 -0
  112. package/src/state/local/index.ts +2 -0
  113. package/src/state/manager.ts +804 -18
  114. package/src/state/repository/index.ts +2 -0
  115. package/src/state/repository/repository.index.ts +193 -0
  116. package/src/state/repository/repository.ts +458 -0
  117. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  118. package/src/terminal/docker.ts +18 -14
  119. package/src/terminal/factory.ts +3 -3
  120. package/src/terminal/index.ts +1 -1
  121. package/src/terminal/manager.ts +134 -80
  122. package/src/terminal/run.sh.ts +22 -10
  123. package/src/worker/abstractions.ts +42 -0
  124. package/src/worker/docker.ts +83 -0
  125. package/src/worker/factory.ts +20 -0
  126. package/src/worker/index.ts +3 -0
  127. package/src/worker/manager.ts +139 -0
  128. package/dist/chunk-C2TJAQAD.js +0 -937
  129. package/dist/chunk-C2TJAQAD.js.map +0 -1
  130. package/dist/chunk-WXDYCRTT.js +0 -234
  131. package/dist/chunk-WXDYCRTT.js.map +0 -1
  132. package/src/library/worker/loader.ts +0 -114
  133. package/src/preferences/shared.ts +0 -1
  134. package/src/project/lock.ts +0 -39
  135. package/src/secret/abstractions.ts +0 -59
  136. package/src/secret/factory.ts +0 -22
  137. package/src/secret/local.ts +0 -152
  138. package/src/shared/state.ts +0 -270
  139. package/src/shared/terminal.ts +0 -13
  140. package/src/state/local.ts +0 -612
  141. package/src/workspace/abstractions.ts +0 -41
  142. package/src/workspace/factory.ts +0 -14
  143. package/src/workspace/local.ts +0 -54
  144. /package/src/shared/{library.ts → models/backend/library.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { TerminalRunOptions, TerminalBackend } from "./shared"
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
- HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY: z.string().default("docker"),
13
- HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
14
- HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST: z.string().optional(),
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({ factory, stdin, stdout, screenSize, signal }: TerminalRunOptions): Promise<void> {
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
- factory.image,
37
+ spec.image,
38
38
  "/bin/bash",
39
39
  "/run.sh",
40
40
  ]
41
41
 
42
42
  const initData = {
43
- command: factory.command,
44
- cwd: factory.cwd,
43
+ command: spec.command,
44
+ cwd: spec.cwd,
45
45
  env: {
46
- ...factory.env,
46
+ ...spec.env,
47
47
  TERM: "xterm-256color",
48
48
  },
49
- files: factory.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
- this.logger.info({ pid: childProcess.pid }, "process started")
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.HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY,
85
- config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO,
86
- config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST,
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
  }
@@ -1,10 +1,10 @@
1
1
  import type { Logger } from "pino"
2
- import type { TerminalBackend } from "./shared"
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
- HIGHSTATE_BACKEND_TERMINAL_TYPE: z.enum(["docker"]).default("docker"),
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.HIGHSTATE_BACKEND_TERMINAL_TYPE) {
15
+ switch (config.HIGHSTATE_TERMINAL_BACKEND_TYPE) {
16
16
  case "docker": {
17
17
  return DockerTerminalBackend.create(config, logger)
18
18
  }
@@ -1,3 +1,3 @@
1
- export * from "./shared"
1
+ export * from "./abstractions"
2
2
  export * from "./factory"
3
3
  export * from "./manager"
@@ -1,12 +1,16 @@
1
- import type { ScreenSize, TerminalBackend } from "./shared"
1
+ import type { ScreenSize, TerminalBackend } from "./abstractions"
2
2
  import type { Logger } from "pino"
3
- import type { StateBackend, TerminalHistoryEntry } from "../state"
4
- import type { RunnerBackend } from "../runner"
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 { EventEmitter, on } from "node:events"
7
- import { parseInstanceId } from "@highstate/contract"
8
- import { nanoid } from "nanoid"
9
- import { type InstanceTerminal, type TerminalSession, createAsyncBatcher } from "../shared"
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 stateBackend: StateBackend,
37
- private readonly runnerBackend: RunnerBackend,
36
+ private readonly stateManager: StateManager,
37
+ private readonly pubsubManager: PubSubManager,
38
+ private readonly projectUnlockService: ProjectUnlockService,
38
39
  private readonly logger: Logger,
39
40
  ) {
40
- void this.markFinishedSessions()
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.stateBackend.getTerminalSession(projectId, instanceId, sessionId)
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 [terminalSession] of on(this.terminalEE, sessionId, {
73
+ for await (const terminalSession of this.pubsubManager.subscribe(
74
+ ["active-terminal-session", projectId, sessionId],
70
75
  signal,
71
- }) as AsyncIterable<[TerminalSession]>) {
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
- projectId: string,
83
- instanceId: string,
84
- terminalName: string,
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 (!factory) {
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: nanoid(),
96
+ id: uuidv7(),
101
97
  projectId,
102
- instanceId,
103
- terminalName,
104
- terminalTitle: factory.title,
105
- createdAt: new Date(),
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
- await this.stateBackend.putTerminalSession(projectId, instanceId, terminalSession)
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(factory, terminalSession)
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
- instanceId: string,
119
- terminalName: string,
143
+ terminalId: string,
144
+ newSession = false,
120
145
  ): Promise<TerminalSession> {
121
- const existingSession = this.existingSessions.get(`${projectId}:${instanceId}.${terminalName}`)
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, instanceId, terminalName)
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
- await this.stateBackend.putActiveTerminalSessions(
191
- Array.from(this.managedTerminals.values()).map(t => t.session),
192
- )
215
+ // TODO
193
216
  }
194
217
 
195
218
  private createManagedTerminal(
196
- factory: InstanceTerminal,
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
- factory,
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.instanceId}.${managedTerminal.session.terminalName}`,
263
+ `${managedTerminal.session.projectId}:${managedTerminal.session.terminalId}`,
233
264
  )
234
265
 
235
- managedTerminal.session.finishedAt = new Date()
236
- this.terminalEE.emit(managedTerminal.session.id, managedTerminal.session)
237
-
238
- void this.stateBackend.putTerminalSession(
239
- managedTerminal.session.projectId,
240
- managedTerminal.session.instanceId,
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
- void this.persistHistory.call([terminalSession.id, entry])
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.instanceId}.${managedTerminal.session.terminalName}`,
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: StateBackend,
299
- runnerBackend: RunnerBackend,
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
- runnerBackend,
341
+ pubsubManager,
342
+ projectUnlockService,
306
343
  logger.child({ service: "TerminalManager" }),
307
344
  )
308
345
  }
309
346
 
310
- private persistHistory = createAsyncBatcher(async (entries: TerminalHistoryEntry[]) => {
311
- this.logger.trace({ msg: "persisting history entries", count: entries.length })
347
+ private async markFinishedSessions(projectId: string): Promise<void> {
348
+ const activeSessions = await this.stateManager
349
+ .getActiveTerminalSessionIndexRepository(projectId)
350
+ .getAllItems()
312
351
 
313
- // TODO: buffer entries and save them as lines
352
+ if (activeSessions.length === 0) {
353
+ this.logger.debug({ projectId }, "no lost terminal sessions found")
354
+ return
355
+ }
314
356
 
315
- await this.stateBackend.appendTerminalSessionHistory(entries)
316
- })
357
+ const batch = this.stateManager.batch()
317
358
 
318
- private async markFinishedSessions(): Promise<void> {
319
- const sessions = await this.stateBackend.getActiveTerminalSessions()
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
- for (const session of sessions) {
322
- await this.stateBackend.putTerminalSession(session.projectId, session.instanceId, {
323
- ...session,
324
- finishedAt: new Date(),
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 this.updateActiveTerminalSessions()
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
  }
@@ -16,18 +16,30 @@ done
16
16
 
17
17
  # Create files
18
18
  for key in "\${filesKeys[@]}"; do
19
- isBinary=$(jq -r ".files[\\"$key\\"].isBinary // false" <<<"$data")
20
- content=$(jq -r ".files[\\"$key\\"].content" <<<"$data")
21
- mode=$(jq -r ".files[\\"$key\\"].mode // 0" <<<"$data")
22
-
23
- if [ "$isBinary" = "true" ]; then
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
- if [ "$mode" -ne 0 ]; then
30
- chmod $(printf "%o" "$mode") "$key"
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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./abstractions"
2
+ export * from "./factory"
3
+ export * from "./manager"