| import fs from 'fs'; |
| import { spawn, ChildProcess, execFileSync, execSync } from 'child_process'; |
| import path from 'path'; |
| import Docker from 'dockerode'; |
| import { EventEmitter } from 'events'; |
| import { IdxEngine } from '../idx/idx-engine'; |
| import { buildWorkspaceImage, loadWorkspaceConfig, type CodeverseConfig } from './builder'; |
| import { createDockerClient, probeDockerAvailability, resolveDockerClient } from './client'; |
| import { HFStorage } from '../hf/storage'; |
| import { resolveSafeProjectPath } from '../fs/isolation'; |
| import { ENV_CONFIG } from '../env-config'; |
|
|
| |
| |
| |
| |
| type WorkspaceProcess = Pick<ChildProcess, 'kill' | 'pid'> & { |
| on?: ChildProcess['on']; |
| stdout?: ChildProcess['stdout']; |
| stderr?: ChildProcess['stderr']; |
| }; |
|
|
| type WorkspaceRuntimeKind = 'native' | 'docker'; |
|
|
| interface NativeWorkspaceRuntimeEntry { |
| kind: 'native'; |
| pid: number; |
| port: number; |
| process: WorkspaceProcess; |
| } |
|
|
| interface DockerWorkspaceRuntimeEntry { |
| kind: 'docker'; |
| containerId: string; |
| containerName: string; |
| imageName: string; |
| port: number; |
| socketPath: string; |
| } |
|
|
| type WorkspaceRuntimeEntry = NativeWorkspaceRuntimeEntry | DockerWorkspaceRuntimeEntry; |
|
|
| |
| |
| |
| |
| export const nativeProcesses = new Map<string, NativeWorkspaceRuntimeEntry>(); |
| export const dockerWorkspaces = new Map<string, DockerWorkspaceRuntimeEntry>(); |
|
|
| |
| |
| |
| class ProvisioningBus extends EventEmitter {} |
| export const provisioningBus = new ProvisioningBus(); |
|
|
| |
| |
| |
| export const pendingProvisioning = new Map<string, Promise<WorkspaceOperationResult>>(); |
|
|
| interface WorkspaceRuntimePaths { |
| fullWorkspaceId: string; |
| shortWorkspaceId: string; |
| projectPath: string; |
| runtimeRootPath: string; |
| runtimeWorkspacePath: string; |
| userDataPath: string; |
| metadataPath: string; |
| npmCachePath: string; |
| } |
|
|
| interface CodeServerLaunch { |
| command: string; |
| args: string[]; |
| label: string; |
| usesNpx: boolean; |
| useShell: boolean; |
| } |
|
|
| const SHORT_WORKSPACE_ID_LENGTH = 8; |
| const RUNTIME_ROOT_DIR_NAME = '.codeverse-runtime'; |
| const DOCKER_CODE_SERVER_PORT = 8080; |
| const WORKSPACE_CONTAINER_NAME_PREFIX = 'codeverse-workspace-'; |
| const WORKSPACE_RUNTIME_LABEL = 'codeverse.runtime'; |
| const WORKSPACE_RUNTIME_LABEL_VALUE = 'workspace'; |
| const WORKSPACE_ID_LABEL = 'codeverse.workspace.id'; |
|
|
| function getWorkspaceRootPath(): string { |
| return ENV_CONFIG.WORKSPACE_ROOT; |
| } |
|
|
| function getRuntimeRootPath(): string { |
| return path.join( getWorkspaceRootPath(), RUNTIME_ROOT_DIR_NAME); |
| } |
|
|
| function getShortWorkspaceId(id: string): string { |
| return id.slice(0, SHORT_WORKSPACE_ID_LENGTH); |
| } |
|
|
| function isPathWithinParent(parentPath: string, targetPath: string): boolean { |
| const normalizedParent = path.resolve(parentPath); |
| const normalizedTarget = path.resolve(targetPath); |
|
|
| return normalizedTarget === normalizedParent || normalizedTarget.startsWith(`${normalizedParent}${path.sep}`); |
| } |
|
|
| function getNativeWorkspaceEntry(id: string): NativeWorkspaceRuntimeEntry | undefined { |
| const directEntry = nativeProcesses.get(id); |
| if (directEntry) { |
| return directEntry; |
| } |
|
|
| const prefixedKey = Array.from(nativeProcesses.keys()).find((key) => id.startsWith(key)); |
| return prefixedKey ? nativeProcesses.get(prefixedKey) : undefined; |
| } |
|
|
| function getDockerWorkspaceEntry(id: string): DockerWorkspaceRuntimeEntry | undefined { |
| return dockerWorkspaces.get(id); |
| } |
|
|
| function getWorkspaceRuntimeEntry(id: string): WorkspaceRuntimeEntry | undefined { |
| return getNativeWorkspaceEntry(id) |
| ?? getDockerWorkspaceEntry(id); |
| } |
|
|
| function getWorkspaceContainerName(id: string): string { |
| return `${WORKSPACE_CONTAINER_NAME_PREFIX}${id}`; |
| } |
|
|
| function getWorkspaceContainerLabels(config: Pick<WorkspaceConfig, 'id' | 'projectName' | 'userId'>): Record<string, string> { |
| return { |
| [WORKSPACE_RUNTIME_LABEL]: WORKSPACE_RUNTIME_LABEL_VALUE, |
| [WORKSPACE_ID_LABEL]: config.id, |
| 'codeverse.workspace.project': config.projectName, |
| 'codeverse.workspace.user': config.userId, |
| }; |
| } |
|
|
| function getDockerBindSourcePath(hostPath: string): string { |
| return path.resolve(hostPath); |
| } |
|
|
| function hasCustomDockerPackages(config: CodeverseConfig): boolean { |
| return Boolean( |
| config.packages?.apt?.length |
| || config.packages?.npm?.length |
| ); |
| } |
|
|
| function getWorkspaceEnvironment(config: CodeverseConfig): string[] { |
| return Object.entries(config.env ?? {}).map(([key, value]) => `${key}=${value}`); |
| } |
|
|
| async function ensureDockerImageAvailable(docker: Docker, imageName: string, log: (msg: string) => void): Promise<void> { |
| try { |
| await docker.getImage(imageName).inspect(); |
| return; |
| } catch { |
| log(`Pulling Docker image '${imageName}'...`); |
| } |
|
|
| await new Promise<void>((resolve, reject) => { |
| docker.pull(imageName, (error: Error | null, stream?: NodeJS.ReadableStream) => { |
| if (error || !stream) { |
| reject(error ?? new Error(`Failed to pull Docker image '${imageName}'`)); |
| return; |
| } |
|
|
| docker.modem.followProgress( |
| stream, |
| (progressError: Error | null) => { |
| if (progressError) { |
| reject(progressError); |
| return; |
| } |
|
|
| resolve(); |
| }, |
| (event: { status?: string; id?: string }) => { |
| if (event.status) { |
| const progressLabel = event.id ? `${event.id}: ${event.status}` : event.status; |
| log(`[DOCKER:PULL] ${progressLabel}`); |
| } |
| } |
| ); |
| }); |
| }); |
| } |
|
|
| function getPublishedPortForContainerInfo(info: Docker.ContainerInspectInfo): number | undefined { |
| const bindings = info.NetworkSettings?.Ports?.[`${DOCKER_CODE_SERVER_PORT}/tcp`]; |
| const hostPort = bindings?.[0]?.HostPort; |
| if (!hostPort) { |
| return undefined; |
| } |
|
|
| const parsedPort = Number.parseInt(hostPort, 10); |
| return Number.isFinite(parsedPort) ? parsedPort : undefined; |
| } |
|
|
| function getPublishedPortForListEntry(containerInfo: Docker.ContainerInfo): number | undefined { |
| const publishedPort = containerInfo.Ports.find((portInfo) => portInfo.PrivatePort === DOCKER_CODE_SERVER_PORT && portInfo.PublicPort)?.PublicPort; |
| return publishedPort && Number.isFinite(publishedPort) ? publishedPort : undefined; |
| } |
|
|
| async function resolveWorkspaceRuntimePaths(config: Pick<WorkspaceConfig, 'id' | 'userId' | 'projectName'>): Promise<WorkspaceRuntimePaths> { |
| const shortWorkspaceId = getShortWorkspaceId(config.id); |
| const runtimeRootPath = getRuntimeRootPath(); |
| const projectPath = await resolveSafeProjectPath(config.userId, config.projectName); |
|
|
| return { |
| fullWorkspaceId: config.id, |
| shortWorkspaceId, |
| projectPath, |
| runtimeRootPath, |
| runtimeWorkspacePath: path.join( runtimeRootPath, shortWorkspaceId), |
| userDataPath: path.join( runtimeRootPath, `${shortWorkspaceId}-userdata`), |
| metadataPath: path.join( runtimeRootPath, `${shortWorkspaceId}.id`), |
| npmCachePath: path.join( runtimeRootPath, 'npm-cache'), |
| }; |
| } |
|
|
| function ensureRuntimeWorkspacePath(paths: WorkspaceRuntimePaths, log?: (msg: string) => void): string { |
| if (!fs.existsSync(paths.projectPath)) { |
| fs.mkdirSync(paths.projectPath, { recursive: true }); |
| } |
|
|
| fs.mkdirSync(paths.runtimeRootPath, { recursive: true }); |
| fs.mkdirSync(paths.userDataPath, { recursive: true }); |
| fs.mkdirSync(paths.npmCachePath, { recursive: true }); |
|
|
| if (fs.existsSync(paths.runtimeWorkspacePath)) { |
| try { |
| const existingTargetPath = fs.realpathSync(paths.runtimeWorkspacePath); |
| if (path.resolve(existingTargetPath) === path.resolve(paths.projectPath)) { |
| fs.writeFileSync(paths.metadataPath, paths.fullWorkspaceId); |
| return paths.runtimeWorkspacePath; |
| } |
| } catch { |
| |
| } |
|
|
| if (!isPathWithinParent(paths.runtimeRootPath, paths.runtimeWorkspacePath)) { |
| throw new Error(`Unsafe runtime workspace path: ${paths.runtimeWorkspacePath}`); |
| } |
|
|
| fs.rmSync(paths.runtimeWorkspacePath, { recursive: true, force: true }); |
| } |
|
|
| try { |
| fs.symlinkSync(paths.projectPath, paths.runtimeWorkspacePath, process.platform === 'win32' ? 'junction' : 'dir'); |
| fs.writeFileSync(paths.metadataPath, paths.fullWorkspaceId); |
| log?.(`Bound runtime alias ${paths.shortWorkspaceId} -> ${paths.projectPath}`); |
| return paths.runtimeWorkspacePath; |
| } catch (error) { |
| const errorMessage = error instanceof Error ? error.message : String(error); |
| fs.writeFileSync(paths.metadataPath, paths.fullWorkspaceId); |
| log?.(`[WARN] Runtime alias creation failed. Falling back to direct project path: ${errorMessage}`); |
| return paths.projectPath; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function isNativeWorkspaceRunning(id: string): boolean { |
| if (pendingProvisioning.has(id)) return false; |
| return getNativeWorkspaceEntry(id) !== undefined; |
| } |
|
|
| export function isWorkspaceRunning(id: string): boolean { |
| if (pendingProvisioning.has(id)) return false; |
| return getWorkspaceRuntimeEntry(id) !== undefined; |
| } |
|
|
| |
| |
| |
| export function getWorkspaceStatus(id: string): "ready" | "provisioning" | "offline" { |
| if (pendingProvisioning.has(id)) return "provisioning"; |
| if (getWorkspaceRuntimeEntry(id)) return "ready"; |
| return "offline"; |
| } |
|
|
| |
| |
| |
| const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); |
|
|
| |
| |
| |
| function findAvailablePort(): number { |
| const occupiedPorts = [ |
| ...Array.from(nativeProcesses.values()).map((entry) => entry.port), |
| ...Array.from(dockerWorkspaces.values()).map((entry) => entry.port), |
| ]; |
| let port = Math.floor(Math.random() * (9000 - 8100) + 8100); |
| while (occupiedPorts.includes(port)) { |
| port = Math.floor(Math.random() * (9000 - 8100) + 8100); |
| } |
| return port; |
| } |
|
|
| function resolveExecutableOnPath(candidates: string[]): string | null { |
| const locatorCommand = process.platform === 'win32' ? 'where' : 'which'; |
|
|
| for (const candidate of candidates) { |
| try { |
| const output = execFileSync(locatorCommand, [candidate], { |
| encoding: 'utf8', |
| stdio: ['ignore', 'pipe', 'ignore'], |
| }); |
| const resolvedPath = output |
| .split(/\r?\n/) |
| .map((line) => line.trim()) |
| .find(Boolean); |
| if (resolvedPath) { |
| return resolvedPath; |
| } |
| } catch { |
| |
| } |
| } |
|
|
| return null; |
| } |
|
|
| function resolveLocalToolNodeBinary(): string | null { |
| if (ENV_CONFIG.CODE_SERVER_NODE_BIN && fs.existsSync(ENV_CONFIG.CODE_SERVER_NODE_BIN)) { |
| return ENV_CONFIG.CODE_SERVER_NODE_BIN; |
| } |
|
|
| const localToolsRoot = path.join(process.cwd(), '.codeverse-tools'); |
| if (!fs.existsSync(localToolsRoot)) { |
| return null; |
| } |
|
|
| const directNodePath = path.join(localToolsRoot, 'node', 'node.exe'); |
| if (fs.existsSync(directNodePath)) { |
| return directNodePath; |
| } |
|
|
| const nodeDirs = fs.readdirSync(localToolsRoot, { withFileTypes: true }) |
| .filter((entry) => entry.isDirectory() && /^node-v22\..*-win-x64$/i.test(entry.name)) |
| .map((entry) => path.join(localToolsRoot, entry.name, 'node.exe')); |
|
|
| return nodeDirs.find((candidate) => fs.existsSync(candidate)) ?? null; |
| } |
|
|
| function resolveLocalCodeServerEntry(): string | null { |
| if (ENV_CONFIG.CODE_SERVER_ENTRY && fs.existsSync(ENV_CONFIG.CODE_SERVER_ENTRY)) { |
| return ENV_CONFIG.CODE_SERVER_ENTRY; |
| } |
|
|
| const localEntryPath = path.join(process.cwd(), '.codeverse-tools', 'code-server', 'node_modules', 'code-server', 'out', 'node', 'entry.js'); |
| return fs.existsSync(localEntryPath) ? localEntryPath : null; |
| } |
|
|
| function getCurrentNodeMajorVersion(): number { |
| const [majorVersion] = process.versions.node.split('.'); |
| return Number.parseInt(majorVersion ?? '0', 10); |
| } |
|
|
| function resolveCodeServerLaunch(): CodeServerLaunch { |
| const overrideBinary = process.env.CODE_SERVER_BIN; |
| if (overrideBinary) { |
| const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(overrideBinary); |
| return { command: overrideBinary, args: [], label: overrideBinary, usesNpx: false, useShell }; |
| } |
|
|
| if (process.platform === 'win32') { |
| const localToolNodeBinary = resolveLocalToolNodeBinary(); |
| const localCodeServerEntry = resolveLocalCodeServerEntry(); |
| if (localToolNodeBinary && localCodeServerEntry) { |
| return { |
| command: localToolNodeBinary, |
| args: [localCodeServerEntry], |
| label: 'local code-server toolchain', |
| usesNpx: false, |
| useShell: false |
| }; |
| } |
|
|
| const codeServerBinary = resolveExecutableOnPath(['code-server.exe', 'code-server']); |
| if (codeServerBinary) { |
| return { command: codeServerBinary, args: [], label: 'code-server', usesNpx: false, useShell: false }; |
| } |
|
|
| if (getCurrentNodeMajorVersion() !== 22) { |
| throw new Error('CODE_SERVER_WINDOWS_REQUIRES_NODE_22_OR_LOCAL_TOOLCHAIN'); |
| } |
|
|
| const npxCliPath = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js'); |
| if (fs.existsSync(npxCliPath)) { |
| return { command: process.execPath, args: [npxCliPath, '--yes', 'code-server'], label: 'node npx-cli.js code-server', usesNpx: true, useShell: false }; |
| } |
|
|
| throw new Error('CODE_SERVER_BIN_NOT_FOUND'); |
| } |
|
|
| const codeServerBinary = resolveExecutableOnPath(['code-server']); |
| if (codeServerBinary) { |
| return { command: codeServerBinary, args: [], label: 'code-server', usesNpx: false, useShell: false }; |
| } |
|
|
| const npxBinary = resolveExecutableOnPath(['npx']); |
| if (npxBinary) { |
| return { command: npxBinary, args: ['--yes', 'code-server'], label: 'npx code-server', usesNpx: true, useShell: false }; |
| } |
|
|
| throw new Error('CODE_SERVER_BIN_NOT_FOUND'); |
| } |
|
|
| |
| |
| |
| export async function isDockerAvailable(): Promise<{ available: boolean; reason?: string }> { |
| if (process.env.SIMULATE_HF === 'true') { |
| return { available: false, reason: "Hugging Face Simulation Mode (Artificial Sandbox)" }; |
| } |
| |
| if (process.env.SPACE_ID) { |
| return { available: false, reason: "Hugging Face Space (Native Sandboxed)" }; |
| } |
|
|
| const availability = await probeDockerAvailability(ENV_CONFIG.DOCKER_PROBE_TIMEOUT_MS); |
| return availability.available |
| ? { available: true } |
| : { available: false, reason: availability.reason ?? "Docker daemon unreachable" }; |
| } |
|
|
| |
| |
| |
| export async function stopNativeWorkspace(id: string): Promise<boolean> { |
| const entry = nativeProcesses.get(id); |
| if (entry) { |
| try { |
| entry.process.kill(); |
| nativeProcesses.delete(id); |
| return true; |
| } catch (e) { |
| console.error(`[MANAGER] Failed to kill code-server ${id}:`, e); |
| nativeProcesses.delete(id); |
| } |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| |
| export function getNativeWorkspacePort(id: string): number | undefined { |
| if (pendingProvisioning.has(id)) { |
| return undefined; |
| } |
|
|
| return getNativeWorkspaceEntry(id)?.port; |
| } |
|
|
| export function getWorkspacePort(id: string): number | undefined { |
| if (pendingProvisioning.has(id)) { |
| return undefined; |
| } |
|
|
| return getWorkspaceRuntimeEntry(id)?.port; |
| } |
|
|
| |
| |
| |
| export function getAndroidPort(): number | undefined { |
| return 6080; |
| } |
|
|
| export interface WorkspaceConfig { |
| id: string; |
| userId: string; |
| projectName: string; |
| image?: string; |
| withAndroidEmulator?: boolean; |
| onLog?: (msg: string) => void; |
| } |
|
|
| |
| |
| |
| export interface WorkspaceOperationResult { |
| success: boolean; |
| containerId?: string; |
| androidContainerId?: string; |
| androidPort?: number; |
| port?: string | number; |
| appetizeUrl?: string; |
| error?: string; |
| runtime?: WorkspaceRuntimeKind; |
| status?: 'hydrating' | 'ready'; |
| } |
|
|
| |
| |
| |
| export async function prewarmWorkspace(config: WorkspaceConfig): Promise<void> { |
| const runtimePaths = await resolveWorkspaceRuntimePaths(config); |
| const workspacePath = ensureRuntimeWorkspacePath(runtimePaths); |
|
|
| const idxConfig = IdxEngine.getIdxConfig(workspacePath); |
| if (idxConfig) { |
| |
| IdxEngine.syncNixEnvironment(workspacePath, idxConfig, (msg) => { |
| provisioningBus.emit(`log:${config.id}`, `[HYDRATE] ${msg}`); |
| }); |
| } |
| } |
|
|
| interface WorkspaceHeartbeatOptions { |
| port: number; |
| log: (msg: string) => void; |
| getFailureReason?: () => Promise<string | null>; |
| } |
|
|
| async function waitForWorkspaceHeartbeat({ port, log, getFailureReason }: WorkspaceHeartbeatOptions): Promise<string | null> { |
| let attempts = 0; |
|
|
| while (attempts < 60) { |
| const failureReason = await getFailureReason?.(); |
| if (failureReason) { |
| log(`[FATAL] IDE bootstrap aborted before handshake: ${failureReason}`); |
| return failureReason; |
| } |
|
|
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), 2000); |
|
|
| try { |
| const response = await fetch(`http://127.0.0.1:${port}`, { signal: controller.signal }); |
| if (response.ok) { |
| log(`Handshake verified. Studio Engine Online.`); |
| return null; |
| } |
| } catch { |
| if (attempts % 5 === 0) log(`[INFO] Scanning for IDE heartbeat... (Attempt ${attempts}/60)`); |
| if (attempts === 15) { |
| const bootstrapHint = ENV_CONFIG.IDX_NIX_SYNC_ENABLED |
| ? 'Nix evaluation in progress. Cold boot detected.' |
| : 'IDE bootstrap still in progress.'; |
| log(`[INFO] ${bootstrapHint}`); |
| } |
| if (attempts === 45) log(`[WARN] Handshake threshold approaching. IDE core high load.`); |
| } finally { |
| clearTimeout(timeoutId); |
| } |
|
|
| await delay(1000); |
| attempts++; |
| } |
|
|
| log(`[FATAL] Handshake timeout on 127.0.0.1:${port}.`); |
| return "IDE_HANDSHAKE_TIMEOUT"; |
| } |
|
|
| async function resolveExistingWorkspaceContainer(docker: Docker, workspaceId: string): Promise<Docker.ContainerInfo | null> { |
| const containerName = getWorkspaceContainerName(workspaceId); |
| const containers = await docker.listContainers({ |
| all: true, |
| filters: { |
| name: [containerName], |
| }, |
| }); |
|
|
| const exactMatch = containers.find((container) => container.Names.some((name) => name === `/${containerName}`)); |
| return exactMatch ?? containers[0] ?? null; |
| } |
|
|
| function registerDockerWorkspaceRuntime( |
| workspaceId: string, |
| containerInfo: Docker.ContainerInspectInfo, |
| socketPath: string |
| ): DockerWorkspaceRuntimeEntry | null { |
| const publishedPort = getPublishedPortForContainerInfo(containerInfo); |
| if (!publishedPort) { |
| return null; |
| } |
|
|
| const containerEntry: DockerWorkspaceRuntimeEntry = { |
| kind: 'docker', |
| containerId: containerInfo.Id, |
| containerName: containerInfo.Name.replace(/^\//, ''), |
| imageName: containerInfo.Config.Image, |
| port: publishedPort, |
| socketPath, |
| }; |
|
|
| dockerWorkspaces.set(workspaceId, containerEntry); |
| return containerEntry; |
| } |
|
|
| async function startDockerWorkspace( |
| config: WorkspaceConfig, |
| runtimePaths: WorkspaceRuntimePaths, |
| workspacePath: string, |
| log: (msg: string) => void |
| ): Promise<WorkspaceOperationResult | null> { |
| const dockerResolution = await resolveDockerClient(ENV_CONFIG.DOCKER_PROBE_TIMEOUT_MS); |
| if (!dockerResolution) { |
| return null; |
| } |
|
|
| const { docker, socketPath } = dockerResolution; |
| const workspaceConfig = await loadWorkspaceConfig(workspacePath); |
| const containerName = getWorkspaceContainerName(config.id); |
| const existingContainerInfo = await resolveExistingWorkspaceContainer(docker, config.id); |
|
|
| if (existingContainerInfo) { |
| const existingContainer = docker.getContainer(existingContainerInfo.Id); |
| const existingInspect = await existingContainer.inspect(); |
|
|
| if (!existingInspect.State.Running) { |
| log(`Restarting existing Docker workspace container '${containerName}'...`); |
| await existingContainer.start(); |
| } else { |
| log(`Reusing active Docker workspace container '${containerName}'.`); |
| } |
|
|
| const runningInspect = await existingContainer.inspect(); |
| const runningEntry = registerDockerWorkspaceRuntime(config.id, runningInspect, socketPath); |
| if (!runningEntry) { |
| return { success: false, error: 'DOCKER_PORT_BINDING_MISSING' }; |
| } |
|
|
| const heartbeatError = await waitForWorkspaceHeartbeat({ |
| port: runningEntry.port, |
| log, |
| getFailureReason: async () => { |
| const info = await existingContainer.inspect(); |
| return info.State.Running ? null : `DOCKER_CONTAINER_EXITED_${info.State.ExitCode}`; |
| }, |
| }); |
|
|
| if (heartbeatError) { |
| dockerWorkspaces.delete(config.id); |
| return { success: false, error: heartbeatError }; |
| } |
|
|
| return { |
| success: true, |
| containerId: runningEntry.containerId, |
| port: runningEntry.port, |
| runtime: 'docker', |
| }; |
| } |
|
|
| let imageName = ENV_CONFIG.DOCKER_WORKSPACE_BASE_IMAGE; |
| if (hasCustomDockerPackages(workspaceConfig)) { |
| const buildResult = await buildWorkspaceImage(config.id, workspacePath, log, docker); |
| imageName = buildResult.imageName; |
| } |
|
|
| await ensureDockerImageAvailable(docker, imageName, log); |
|
|
| const port = findAvailablePort(); |
| const workspaceContainer = await docker.createContainer({ |
| name: containerName, |
| Image: imageName, |
| WorkingDir: '/home/coder/project', |
| Env: [ |
| 'HOME=/home/coder', |
| 'SHELL=/bin/bash', |
| ...getWorkspaceEnvironment(workspaceConfig), |
| ], |
| Labels: getWorkspaceContainerLabels(config), |
| ExposedPorts: { |
| [`${DOCKER_CODE_SERVER_PORT}/tcp`]: {}, |
| }, |
| HostConfig: { |
| Binds: [ |
| `${getDockerBindSourcePath(workspacePath)}:/home/coder/project`, |
| `${getDockerBindSourcePath(runtimePaths.userDataPath)}:/home/coder/.local/share/code-server`, |
| ], |
| PortBindings: { |
| [`${DOCKER_CODE_SERVER_PORT}/tcp`]: [{ HostIp: '127.0.0.1', HostPort: `${port}` }], |
| }, |
| }, |
| Cmd: [ |
| '--auth', 'none', |
| '--bind-addr', `0.0.0.0:${DOCKER_CODE_SERVER_PORT}`, |
| '--disable-telemetry', |
| '--disable-update-check', |
| '--user-data-dir', '/home/coder/.local/share/code-server', |
| '/home/coder/project', |
| ], |
| }); |
|
|
| await workspaceContainer.start(); |
| log(`Docker workspace container '${containerName}' started from image '${imageName}'.`); |
|
|
| const createdInspect = await workspaceContainer.inspect(); |
| const createdEntry = registerDockerWorkspaceRuntime(config.id, createdInspect, socketPath); |
| if (!createdEntry) { |
| return { success: false, error: 'DOCKER_PORT_BINDING_MISSING' }; |
| } |
|
|
| const heartbeatError = await waitForWorkspaceHeartbeat({ |
| port: createdEntry.port, |
| log, |
| getFailureReason: async () => { |
| const info = await workspaceContainer.inspect(); |
| return info.State.Running ? null : `DOCKER_CONTAINER_EXITED_${info.State.ExitCode}`; |
| }, |
| }); |
|
|
| if (heartbeatError) { |
| try { |
| await workspaceContainer.remove({ force: true }); |
| } catch { |
| |
| } |
| dockerWorkspaces.delete(config.id); |
| return { success: false, error: heartbeatError }; |
| } |
|
|
| return { |
| success: true, |
| containerId: createdEntry.containerId, |
| port: createdEntry.port, |
| runtime: 'docker', |
| }; |
| } |
|
|
| |
| |
| |
| async function performProvisioning(config: WorkspaceConfig): Promise<WorkspaceOperationResult> { |
| const log = (msg: string) => { |
| if (config.onLog) config.onLog(`[IDX:ENGINE] ${msg}`); |
| provisioningBus.emit(`log:${config.id}`, msg); |
| }; |
|
|
| try { |
| log(`Provisioning hermetic environment for '${config.projectName}'...`); |
| |
| |
| try { |
| await HFStorage.syncFromDataset((msg) => log(msg)); |
| } catch (e) { |
| log(`[WARN] Persistent profile restoration failed: ${e instanceof Error ? e.message : String(e)}. Proceeding with clean environment.`); |
| } |
|
|
| |
| const runtimePaths = await resolveWorkspaceRuntimePaths(config); |
| const workspacePath = ensureRuntimeWorkspacePath(runtimePaths, log); |
| const userDataPath = runtimePaths.userDataPath; |
|
|
| |
| const idxConfig = IdxEngine.getIdxConfig(workspacePath); |
| log(`Declarative config detected (Packages: ${idxConfig.packages.length}). Initializing synchronization...`); |
|
|
| await IdxEngine.syncNixEnvironment(workspacePath, idxConfig, (msg) => log(msg)); |
|
|
| const flagPath = path.join( workspacePath, '.idx-created'); |
| if (!fs.existsSync(flagPath)) { |
| if (idxConfig.onCreate) { |
| log(`Executing onCreate lifecycle hook...`); |
| await IdxEngine.runHook(workspacePath, 'onCreate', idxConfig.onCreate, (msg) => log(msg)); |
| } |
| fs.writeFileSync(flagPath, new Date().toISOString()); |
| } |
|
|
| const shouldPreferDockerRuntime = ENV_CONFIG.WORKSPACE_RUNTIME_PREFERENCE === 'docker' |
| || (ENV_CONFIG.WORKSPACE_RUNTIME_PREFERENCE === 'auto' && process.platform === 'win32'); |
| if (shouldPreferDockerRuntime) { |
| log(`Evaluating Docker runtime for workspace boot...`); |
| const dockerResult = await startDockerWorkspace(config, runtimePaths, workspacePath, log); |
| if (dockerResult) { |
| if (dockerResult.success && idxConfig.onStart) { |
| log(`Executing background onStart lifecycle hooks...`); |
| IdxEngine.runHook(workspacePath, 'onStart', idxConfig.onStart, (msg) => log(msg), true); |
| } |
|
|
| const finalDockerResult: WorkspaceOperationResult = { |
| ...dockerResult, |
| androidPort: config.withAndroidEmulator ? 6080 : undefined, |
| }; |
|
|
| if (dockerResult.success) { |
| provisioningBus.emit(`ready:${config.id}`, finalDockerResult); |
| } else { |
| provisioningBus.emit(`error:${config.id}`, finalDockerResult); |
| } |
|
|
| return finalDockerResult; |
| } |
|
|
| if (ENV_CONFIG.WORKSPACE_RUNTIME_PREFERENCE === 'docker') { |
| const dockerUnavailableResult: WorkspaceOperationResult = { |
| success: false, |
| error: 'DOCKER_DAEMON_UNREACHABLE', |
| runtime: 'docker', |
| }; |
| provisioningBus.emit(`error:${config.id}`, dockerUnavailableResult); |
| return dockerUnavailableResult; |
| } |
|
|
| log(`[INFO] Docker runtime unavailable. Falling back to native IDE process.`); |
| } |
|
|
| |
| const codeServerLaunch = resolveCodeServerLaunch(); |
| const port = findAvailablePort(); |
| const shellCommand = codeServerLaunch.command; |
| const args = codeServerLaunch.args; |
| const spawnCwd = (() => { |
| try { |
| return fs.realpathSync(workspacePath); |
| } catch { |
| return workspacePath; |
| } |
| })(); |
|
|
| const baseArgs = [ |
| '--auth', 'none', |
| '--bind-addr', `127.0.0.1:${port}`, |
| '--user-data-dir', userDataPath, |
| '--disable-telemetry', |
| '--disable-update-check', |
| workspacePath, |
| ]; |
|
|
| const spawnEnv: NodeJS.ProcessEnv = { |
| ...process.env, |
| HOME: workspacePath, |
| npm_config_cache: runtimePaths.npmCachePath, |
| npm_config_update_notifier: 'false', |
| }; |
| delete spawnEnv.PORT; |
| delete spawnEnv.SERVER_PORT; |
|
|
| const launchArgs = [...args, ...baseArgs]; |
|
|
| log(`IDE launch prepared. Binary: ${shellCommand} | cwd: ${spawnCwd} | target: ${workspacePath}`); |
|
|
| let child: ChildProcess; |
| try { |
| child = spawn(shellCommand, launchArgs, { |
| env: spawnEnv, |
| cwd: spawnCwd, |
| shell: false, |
| }); |
| } catch (error) { |
| const errorMessage = error instanceof Error ? error.message : String(error); |
| throw new Error(`IDE_SPAWN_CONFIGURATION_REJECTED: ${errorMessage}`); |
| } |
|
|
| log(`Spawning VS Code Orchestrator via ${codeServerLaunch.label} (PID: ${child.pid})...`); |
| let childExited = false; |
| let childFailureReason: string | null = null; |
|
|
| child.on('error', (err) => { |
| childFailureReason = `IDE_BINARY_FAILURE: ${err.message}`; |
| log(`[FATAL] IDE binary failure: ${err.message}`); |
| }); |
| child.stdout?.on('data', (data) => { |
| const out = data.toString().trim(); |
| if (out.includes('listening on')) log(`[IDX:UP] ${out}`); |
| else if (out.length > 0) log(`[IDE:CORE] ${out}`); |
| }); |
|
|
| child.stderr?.on('data', (data) => { |
| const err = data.toString().trim(); |
| if (err.length > 0) log(`[IDE:ERR] ${err}`); |
| }); |
|
|
| child.on('close', (code, signal) => { |
| childExited = true; |
| if (code !== 0 || signal) { |
| childFailureReason = childFailureReason ?? `IDE_PROCESS_EXIT_${code ?? 'unknown'}${signal ? `_${signal}` : ''}`; |
| } |
| log(`[IDE:EXIT] IDE process died with code ${code} (Signal: ${signal})`); |
| nativeProcesses.delete(config.id); |
| }); |
|
|
| nativeProcesses.set(config.id, { kind: 'native', pid: child.pid!, port, process: child }); |
|
|
| const handshakeError = await waitForWorkspaceHeartbeat({ |
| port, |
| log, |
| getFailureReason: async () => { |
| if (!childExited) { |
| return null; |
| } |
|
|
| const rawFailureMessage = childFailureReason ?? 'IDE_PROCESS_EXITED_BEFORE_HANDSHAKE'; |
| return codeServerLaunch.usesNpx |
| ? `${rawFailureMessage}. code-server could not be bootstrapped via npx. Install code-server globally or set CODE_SERVER_BIN.` |
| : rawFailureMessage; |
| }, |
| }); |
|
|
| if (handshakeError) { |
| const entry = nativeProcesses.get(config.id); |
| if (entry) { |
| entry.process.kill(); |
| nativeProcesses.delete(config.id); |
| } |
| const errResult = { success: false, error: handshakeError }; |
| provisioningBus.emit(`error:${config.id}`, errResult); |
| return errResult; |
| } |
|
|
| if (idxConfig.onStart) { |
| log(`Executing background onStart lifecycle hooks...`); |
| IdxEngine.runHook(workspacePath, 'onStart', idxConfig.onStart, (msg) => log(msg), true); |
| } |
|
|
| const finalResult: WorkspaceOperationResult = { |
| success: true, |
| containerId: `native-${config.id}`, |
| androidPort: config.withAndroidEmulator ? 6080 : undefined, |
| port, |
| runtime: 'native', |
| }; |
| provisioningBus.emit(`ready:${config.id}`, finalResult); |
| return finalResult; |
| } catch (e) { |
| const error = e instanceof Error ? e.message : String(e); |
| log(`[FATAL] Provisioning pipeline collapsed: ${error}`); |
| nativeProcesses.delete(config.id); |
| dockerWorkspaces.delete(config.id); |
| const errResult = { success: false, error: `PROVISIONING_FAILED: ${error}` }; |
| provisioningBus.emit(`error:${config.id}`, errResult); |
| return errResult; |
| } |
| } |
|
|
| |
| |
| |
| export async function startWorkspaceContainer(config: WorkspaceConfig): Promise<WorkspaceOperationResult> { |
| const pendingWorkspace = pendingProvisioning.get(config.id); |
| if (pendingWorkspace) { |
| return await pendingWorkspace; |
| } |
|
|
| const existingRuntime = getWorkspaceRuntimeEntry(config.id); |
| if (existingRuntime) { |
| return { |
| success: true, |
| containerId: existingRuntime.kind === 'docker' ? existingRuntime.containerId : `native-${config.id}`, |
| port: existingRuntime.port, |
| runtime: existingRuntime.kind, |
| }; |
| } |
|
|
| let pending = pendingProvisioning.get(config.id); |
| if (!pending) { |
| pending = performProvisioning(config).finally(() => { |
| pendingProvisioning.delete(config.id); |
| }); |
| pendingProvisioning.set(config.id, pending); |
| } |
|
|
| return await pending; |
| } |
|
|
| |
| |
| |
| |
| export async function reconnectRunningWorkspaces() { |
| const workspaceRoot = getWorkspaceRootPath(); |
| const runtimeRootPath = getRuntimeRootPath(); |
| |
| console.log(`[BOOT] Probing filesystem segment: ${workspaceRoot} for existing sessions...`); |
| try { |
| const dockerResolution = await resolveDockerClient(ENV_CONFIG.DOCKER_PROBE_TIMEOUT_MS); |
| if (dockerResolution) { |
| const runningContainers = await dockerResolution.docker.listContainers({ |
| filters: { |
| label: [`${WORKSPACE_RUNTIME_LABEL}=${WORKSPACE_RUNTIME_LABEL_VALUE}`], |
| }, |
| }); |
|
|
| for (const containerInfo of runningContainers) { |
| const workspaceId = containerInfo.Labels?.[WORKSPACE_ID_LABEL]; |
| const port = getPublishedPortForListEntry(containerInfo); |
| if (!workspaceId || !port) { |
| continue; |
| } |
|
|
| dockerWorkspaces.set(workspaceId, { |
| kind: 'docker', |
| containerId: containerInfo.Id, |
| containerName: containerInfo.Names[0]?.replace(/^\//, '') ?? getWorkspaceContainerName(workspaceId), |
| imageName: containerInfo.Image, |
| port, |
| socketPath: dockerResolution.socketPath, |
| }); |
| console.log(`[RECONNECT] Restored Docker workspace ${workspaceId} on port ${port}.`); |
| } |
| } |
| } catch (error) { |
| console.warn(`[RECONNECT:WARN] Docker workspace restore failed: ${error instanceof Error ? error.message : String(error)}`); |
| } |
|
|
| try { |
| |
| |
| const psCmd = process.platform === 'win32' ? 'tasklist' : "ps aux | grep code-server | grep -v grep"; |
| const output = execSync(psCmd).toString(); |
| const lines = output.split('\n'); |
| |
| for (const line of lines) { |
| |
| const bindMatch = line.match(/--bind-addr 127\.0\.0\.1:(\d+)/); |
| const userDataMatch = line.match(/\.codeverse-runtime[\/\\]([a-zA-Z0-9]{8})-userdata/); |
| const runtimePathMatch = line.match(/\.codeverse-runtime[\/\\]([a-zA-Z0-9]{8})(?:\s|$)/); |
| const legacyPathMatch = line.match(/[ /](?:w|workspaces)[\/\\]([a-zA-Z0-9]{8})/); |
| |
| if (bindMatch) { |
| const shortId = userDataMatch?.[1] ?? runtimePathMatch?.[1] ?? legacyPathMatch?.[1]; |
| if (!shortId) { |
| continue; |
| } |
|
|
| const port = parseInt(bindMatch[1], 10); |
| const metadataPath = path.join(runtimeRootPath, `${shortId}.id`); |
| const legacyIdFile = path.join(workspaceRoot, shortId, '.codeverse-id'); |
| |
| let foundFullId = ""; |
| if (fs.existsSync(metadataPath)) { |
| foundFullId = fs.readFileSync(metadataPath, 'utf-8').trim(); |
| } else if (fs.existsSync(legacyIdFile)) { |
| foundFullId = fs.readFileSync(legacyIdFile, 'utf-8').trim(); |
| } else { |
| |
| foundFullId = shortId; |
| console.warn(`[RECONNECT:WARN] No .codeverse-id for session ${shortId}. Using prefix mapping.`); |
| } |
| |
| if (foundFullId && !nativeProcesses.has(foundFullId)) { |
| |
| const psParts = line.trim().split(/\s+/); |
| const pid = parseInt(psParts[1]); |
| |
| console.log(`[RECONNECT] Identified active IDE ${foundFullId} (PID: ${pid}) on port ${port}. Restoration complete.`); |
| nativeProcesses.set(foundFullId, { |
| kind: 'native', |
| pid, |
| port, |
| process: { |
| pid, |
| kill: () => { |
| try { |
| process.kill(pid, 'SIGKILL'); |
| return true; |
| } catch { |
| try { execSync(`fuser -k ${port}/tcp`); } catch {} |
| return true; |
| } |
| } |
| } as WorkspaceProcess |
| }); |
| } |
| } |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| function startEngineWatchdog() { |
| setInterval(async () => { |
| for (const [id, entry] of nativeProcesses.entries()) { |
| try { |
| |
| try { |
| process.kill(entry.pid, 0); |
| } catch { |
| console.log(`[WATCHDOG] Process ${entry.pid} for ${id} is missing. Pruning.`); |
| nativeProcesses.delete(id); |
| continue; |
| } |
|
|
| |
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), 2000); |
| |
| try { |
| const res = await fetch(`http://127.0.0.1:${entry.port}`, { signal: controller.signal }); |
| if (!res.ok) throw new Error('Unhealthy'); |
| } catch { |
| console.warn(`[WATCHDOG] IDE ${id} (Port ${entry.port}) is non-responsive.`); |
| |
| } finally { |
| clearTimeout(timeoutId); |
| } |
| } catch (e) { |
| console.error(`[WATCHDOG:ERR] ${e}`); |
| } |
| } |
| }, 60000); |
| } |
|
|
| startEngineWatchdog(); |
|
|
| async function stopDockerWorkspace(id: string): Promise<boolean> { |
| const dockerEntry = dockerWorkspaces.get(id); |
| const docker = dockerEntry |
| ? createDockerClient(dockerEntry.socketPath) |
| : (await resolveDockerClient(ENV_CONFIG.DOCKER_PROBE_TIMEOUT_MS))?.docker; |
|
|
| if (!docker) { |
| dockerWorkspaces.delete(id); |
| return false; |
| } |
|
|
| const existingContainerInfo = await resolveExistingWorkspaceContainer(docker, id); |
| if (!existingContainerInfo) { |
| dockerWorkspaces.delete(id); |
| return false; |
| } |
|
|
| const container = docker.getContainer(existingContainerInfo.Id); |
| try { |
| const inspectInfo = await container.inspect(); |
| if (inspectInfo.State.Running) { |
| await container.stop({ t: 10 }); |
| } |
| } catch { |
| |
| } |
|
|
| try { |
| await container.remove({ force: true }); |
| } catch { |
| |
| } |
|
|
| dockerWorkspaces.delete(id); |
| return true; |
| } |
|
|
| |
| |
| |
| export async function stopWorkspaceContainer(id: string): Promise<{ success: boolean }> { |
| const nativeStopped = await stopNativeWorkspace(id); |
| if (nativeStopped) { |
| return { success: true }; |
| } |
|
|
| const dockerStopped = await stopDockerWorkspace(id); |
| return { success: dockerStopped }; |
| } |
|
|
| |
| |
| |
| export class DockerManager { |
| async getContainerStatus(id: string): Promise<"running" | "stopped" | "not_found"> { |
| if (isWorkspaceRunning(id)) return "running"; |
| return "stopped"; |
| } |
|
|
| async stopContainer(id: string): Promise<boolean> { |
| return (await stopWorkspaceContainer(id)).success; |
| } |
|
|
| async startWorkspace(config: WorkspaceConfig): Promise<boolean> { |
| const result = await startWorkspaceContainer(config); |
| return result.success; |
| } |
| } |
|
|
| export const dockerManager = new DockerManager(); |
|
|