add real system initialiasation
Browse files- .agent/memory/session.json +2 -2
- Dockerfile +23 -0
- app/dashboard/booting/BootSequenceClient.tsx +49 -36
- lib/docker/manager.ts +114 -46
.agent/memory/session.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"version": "1.0.0",
|
| 3 |
-
"session_id": "
|
| 4 |
-
"started_at": "2026-04-
|
| 5 |
"workspace": "D:\\Code\\codeverse",
|
| 6 |
"active_task_id": null,
|
| 7 |
"active_agent": null,
|
|
|
|
| 1 |
{
|
| 2 |
"version": "1.0.0",
|
| 3 |
+
"session_id": "56fef4b2",
|
| 4 |
+
"started_at": "2026-04-06T16:00:00.261811+05:30",
|
| 5 |
"workspace": "D:\\Code\\codeverse",
|
| 6 |
"active_task_id": null,
|
| 7 |
"active_agent": null,
|
Dockerfile
CHANGED
|
@@ -4,6 +4,7 @@ FROM node:20-bookworm-slim AS base
|
|
| 4 |
|
| 5 |
# Install build tools and compatibility layers for native modules (node-pty)
|
| 6 |
# Also add code-server for Native Isolation Mode (when Docker is missing)
|
|
|
|
| 7 |
RUN apt-get update && apt-get install -y \
|
| 8 |
libc6 \
|
| 9 |
libstdc++6 \
|
|
@@ -14,12 +15,34 @@ RUN apt-get update && apt-get install -y \
|
|
| 14 |
curl \
|
| 15 |
ca-certificates \
|
| 16 |
tar \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
&& rm -rf /var/lib/apt/lists/* \
|
| 18 |
&& curl -fL https://github.com/coder/code-server/releases/download/v4.96.2/code-server-4.96.2-linux-amd64.tar.gz \
|
| 19 |
| tar -C /usr/local/lib -xz \
|
| 20 |
&& mv /usr/local/lib/code-server-4.96.2-linux-amd64 /usr/local/lib/code-server \
|
| 21 |
&& ln -s /usr/local/lib/code-server/bin/code-server /usr/local/bin/code-server
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# Step 1. Rebuild the source code only when needed
|
| 24 |
FROM base AS builder
|
| 25 |
WORKDIR /app
|
|
|
|
| 4 |
|
| 5 |
# Install build tools and compatibility layers for native modules (node-pty)
|
| 6 |
# Also add code-server for Native Isolation Mode (when Docker is missing)
|
| 7 |
+
# And the real implementation of Docker, Android, X11, and Desktop bridge
|
| 8 |
RUN apt-get update && apt-get install -y \
|
| 9 |
libc6 \
|
| 10 |
libstdc++6 \
|
|
|
|
| 15 |
curl \
|
| 16 |
ca-certificates \
|
| 17 |
tar \
|
| 18 |
+
unzip \
|
| 19 |
+
openjdk-17-jdk \
|
| 20 |
+
xvfb \
|
| 21 |
+
fluxbox \
|
| 22 |
+
novnc \
|
| 23 |
+
websockify \
|
| 24 |
+
libnss3 \
|
| 25 |
+
libatk-bridge2.0-0 \
|
| 26 |
+
libcups2 \
|
| 27 |
+
libgtk-3-0 \
|
| 28 |
+
# Docker Client and Daemon (for build-time environment availability)
|
| 29 |
+
docker.io \
|
| 30 |
&& rm -rf /var/lib/apt/lists/* \
|
| 31 |
&& curl -fL https://github.com/coder/code-server/releases/download/v4.96.2/code-server-4.96.2-linux-amd64.tar.gz \
|
| 32 |
| tar -C /usr/local/lib -xz \
|
| 33 |
&& mv /usr/local/lib/code-server-4.96.2-linux-amd64 /usr/local/lib/code-server \
|
| 34 |
&& ln -s /usr/local/lib/code-server/bin/code-server /usr/local/bin/code-server
|
| 35 |
|
| 36 |
+
# Install Android SDK Command Line Tools
|
| 37 |
+
ENV ANDROID_SDK_ROOT /app/android-sdk
|
| 38 |
+
RUN mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools \
|
| 39 |
+
&& curl -fL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o cmdline-tools.zip \
|
| 40 |
+
&& unzip cmdline-tools.zip -d ${ANDROID_SDK_ROOT}/cmdline-tools \
|
| 41 |
+
&& mv ${ANDROID_SDK_ROOT}/cmdline-tools/cmdline-tools ${ANDROID_SDK_ROOT}/cmdline-tools/latest \
|
| 42 |
+
&& rm cmdline-tools.zip
|
| 43 |
+
|
| 44 |
+
ENV PATH ${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools
|
| 45 |
+
|
| 46 |
# Step 1. Rebuild the source code only when needed
|
| 47 |
FROM base AS builder
|
| 48 |
WORKDIR /app
|
app/dashboard/booting/BootSequenceClient.tsx
CHANGED
|
@@ -13,68 +13,81 @@ export default function BootSequenceClient() {
|
|
| 13 |
const [logs, setLogs] = useState<string[]>([]);
|
| 14 |
const [status, setStatus] = useState<"booting" | "ready" | "error">("booting");
|
| 15 |
const endRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
const boot = useCallback(() => {
|
| 18 |
if (!id) {
|
| 19 |
-
|
| 20 |
setStatus("error");
|
| 21 |
return;
|
| 22 |
}
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
setStatus("booting");
|
| 25 |
setLogs([]);
|
| 26 |
|
| 27 |
-
// Initialize Real-Time SSE Stream for workspace initialization
|
| 28 |
const eventSource = new EventSource(`/api/workspace/stream?id=${id}&withAndroid=${withAndroid}`);
|
|
|
|
| 29 |
|
| 30 |
eventSource.addEventListener("log", (event) => {
|
| 31 |
-
|
| 32 |
-
setLogs(prev => [...prev, msg]);
|
| 33 |
});
|
| 34 |
|
| 35 |
eventSource.addEventListener("ready", (event) => {
|
| 36 |
try {
|
| 37 |
-
JSON.parse(event.data);
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
| 43 |
router.push(`/?workspace=${encodeURIComponent(id)}`);
|
| 44 |
-
}
|
| 45 |
} catch (e) {
|
| 46 |
console.error("Failed to parse ready event:", e);
|
| 47 |
setStatus("error");
|
| 48 |
} finally {
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
});
|
| 52 |
|
| 53 |
eventSource.addEventListener("error", (event) => {
|
| 54 |
try {
|
| 55 |
-
const
|
| 56 |
-
|
|
|
|
| 57 |
} catch {
|
| 58 |
-
|
| 59 |
}
|
| 60 |
-
setStatus("error");
|
| 61 |
-
eventSource.close();
|
| 62 |
});
|
| 63 |
|
| 64 |
-
// Basic error handler for connection issues
|
| 65 |
-
eventSource.onerror = () => {
|
| 66 |
-
setLogs(prev => {
|
| 67 |
-
if (prev.length > 0 && prev[prev.length - 1].startsWith("[FATAL]")) return prev;
|
| 68 |
-
return [...prev, "[FATAL] Lost connection to the provisioning engine."];
|
| 69 |
-
});
|
| 70 |
-
setStatus("error");
|
| 71 |
-
eventSource.close();
|
| 72 |
-
};
|
| 73 |
-
|
| 74 |
return () => {
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
| 76 |
};
|
| 77 |
-
}, [id, withAndroid, router]);
|
| 78 |
|
| 79 |
useEffect(() => {
|
| 80 |
const cleanup = boot();
|
|
@@ -90,14 +103,14 @@ export default function BootSequenceClient() {
|
|
| 90 |
return (
|
| 91 |
<div className="flex flex-col gap-5">
|
| 92 |
{/* Terminal Log */}
|
| 93 |
-
<div className="bg-[#0a0a0a] rounded-xl p-4 min-h-[300px] border border-[#222] shadow-[inset_0_0_30px_rgba(0,0,0,0.6)] overflow-y-auto font-mono text-sm h-[
|
| 94 |
{logs.map((log, i) => (
|
| 95 |
<div key={i} className="mb-1 leading-relaxed">
|
| 96 |
-
{log.startsWith("[ERROR]") || log.startsWith("[FATAL]") ? (
|
| 97 |
<span className="text-red-400">{log}</span>
|
| 98 |
) : log.includes("Ready") || log.includes("Online") || log.includes("success") || log.includes("complete") ? (
|
| 99 |
<span className="text-green-400">{log}</span>
|
| 100 |
-
) : log.startsWith("[SYSTEM]") || log.startsWith("[MANAGER]") ? (
|
| 101 |
<span className="text-blue-400">{log}</span>
|
| 102 |
) : (
|
| 103 |
<span className="text-zinc-500">{log}</span>
|
|
@@ -108,19 +121,19 @@ export default function BootSequenceClient() {
|
|
| 108 |
{status === "booting" && (
|
| 109 |
<div className="flex items-center gap-2 mt-4 text-green-400">
|
| 110 |
<Loader2 size={14} className="animate-spin" />
|
| 111 |
-
<span className="text-sm font-semibold animate-pulse">
|
| 112 |
</div>
|
| 113 |
)}
|
| 114 |
{status === "ready" && (
|
| 115 |
<div className="flex items-center gap-2 mt-4 text-green-400">
|
| 116 |
<CheckCircle2 size={14} />
|
| 117 |
-
<span className="text-sm font-semibold">
|
| 118 |
</div>
|
| 119 |
)}
|
| 120 |
{status === "error" && (
|
| 121 |
<div className="flex items-center gap-2 mt-4 text-red-500">
|
| 122 |
<XCircle size={14} />
|
| 123 |
-
<span className="text-sm font-semibold">
|
| 124 |
</div>
|
| 125 |
)}
|
| 126 |
<div ref={endRef} />
|
|
@@ -134,7 +147,7 @@ export default function BootSequenceClient() {
|
|
| 134 |
className="flex items-center gap-2 px-4 py-2 bg-[#161b22] hover:bg-[#21262d] text-zinc-300 hover:text-white rounded-lg text-sm transition-all border border-[#30363d] hover:border-[#444]"
|
| 135 |
>
|
| 136 |
<ArrowLeft size={14} />
|
| 137 |
-
|
| 138 |
</button>
|
| 139 |
<button
|
| 140 |
onClick={() => boot()}
|
|
|
|
| 13 |
const [logs, setLogs] = useState<string[]>([]);
|
| 14 |
const [status, setStatus] = useState<"booting" | "ready" | "error">("booting");
|
| 15 |
const endRef = useRef<HTMLDivElement>(null);
|
| 16 |
+
const eventSourceRef = useRef<EventSource | null>(null);
|
| 17 |
+
|
| 18 |
+
// Stable log handler to avoid triggering synchronous re-renders in boot()
|
| 19 |
+
const addLog = useCallback((msg: string) => {
|
| 20 |
+
setLogs(prev => {
|
| 21 |
+
if (prev.includes(msg)) return prev;
|
| 22 |
+
return [...prev, msg];
|
| 23 |
+
});
|
| 24 |
+
}, []);
|
| 25 |
|
| 26 |
const boot = useCallback(() => {
|
| 27 |
if (!id) {
|
| 28 |
+
addLog("ERROR: No workspace ID provided.");
|
| 29 |
setStatus("error");
|
| 30 |
return;
|
| 31 |
}
|
| 32 |
|
| 33 |
+
// Close any existing connection before starting a new one
|
| 34 |
+
if (eventSourceRef.current) {
|
| 35 |
+
eventSourceRef.current.close();
|
| 36 |
+
eventSourceRef.current = null;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// We use a functional approach to initializing state to avoid synchronous cascade warnings
|
| 40 |
setStatus("booting");
|
| 41 |
setLogs([]);
|
| 42 |
|
|
|
|
| 43 |
const eventSource = new EventSource(`/api/workspace/stream?id=${id}&withAndroid=${withAndroid}`);
|
| 44 |
+
eventSourceRef.current = eventSource;
|
| 45 |
|
| 46 |
eventSource.addEventListener("log", (event) => {
|
| 47 |
+
addLog(event.data);
|
|
|
|
| 48 |
});
|
| 49 |
|
| 50 |
eventSource.addEventListener("ready", (event) => {
|
| 51 |
try {
|
| 52 |
+
const data = JSON.parse(event.data);
|
| 53 |
+
if (data.success === false) {
|
| 54 |
+
addLog(`[FATAL] ${data.error || "Provisioning failed."}`);
|
| 55 |
+
setStatus("error");
|
| 56 |
+
} else {
|
| 57 |
+
addLog("[SYSTEM] Workspace Online. Handshake complete.");
|
| 58 |
+
setStatus("ready");
|
| 59 |
+
|
| 60 |
+
// Redirect immediately for performance
|
| 61 |
router.push(`/?workspace=${encodeURIComponent(id)}`);
|
| 62 |
+
}
|
| 63 |
} catch (e) {
|
| 64 |
console.error("Failed to parse ready event:", e);
|
| 65 |
setStatus("error");
|
| 66 |
} finally {
|
| 67 |
+
if (eventSourceRef.current) {
|
| 68 |
+
eventSourceRef.current.close();
|
| 69 |
+
eventSourceRef.current = null;
|
| 70 |
+
}
|
| 71 |
}
|
| 72 |
});
|
| 73 |
|
| 74 |
eventSource.addEventListener("error", (event) => {
|
| 75 |
try {
|
| 76 |
+
const data = (event as MessageEvent).data;
|
| 77 |
+
const errData = data ? JSON.parse(data) : {};
|
| 78 |
+
addLog(`[FATAL] ${errData.message || "The deployment engine encountered an unexpected interruption."}`);
|
| 79 |
} catch {
|
| 80 |
+
console.warn("Retrying SSE connection via EventSource auto-reconnect...");
|
| 81 |
}
|
|
|
|
|
|
|
| 82 |
});
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
return () => {
|
| 85 |
+
if (eventSourceRef.current) {
|
| 86 |
+
eventSourceRef.current.close();
|
| 87 |
+
eventSourceRef.current = null;
|
| 88 |
+
}
|
| 89 |
};
|
| 90 |
+
}, [id, withAndroid, router, addLog]);
|
| 91 |
|
| 92 |
useEffect(() => {
|
| 93 |
const cleanup = boot();
|
|
|
|
| 103 |
return (
|
| 104 |
<div className="flex flex-col gap-5">
|
| 105 |
{/* Terminal Log */}
|
| 106 |
+
<div className="bg-[#0a0a0a] rounded-xl p-4 min-h-[300px] border border-[#222] shadow-[inset_0_0_30px_rgba(0,0,0,0.6)] overflow-y-auto font-mono text-sm h-[450px]">
|
| 107 |
{logs.map((log, i) => (
|
| 108 |
<div key={i} className="mb-1 leading-relaxed">
|
| 109 |
+
{log.startsWith("[ERROR]") || log.startsWith("[FATAL]") || log.startsWith("[STDERR]") ? (
|
| 110 |
<span className="text-red-400">{log}</span>
|
| 111 |
) : log.includes("Ready") || log.includes("Online") || log.includes("success") || log.includes("complete") ? (
|
| 112 |
<span className="text-green-400">{log}</span>
|
| 113 |
+
) : log.startsWith("[SYSTEM]") || log.startsWith("[MANAGER]") || log.startsWith("[IDE-MANAGER]") || log.startsWith("[UP]") ? (
|
| 114 |
<span className="text-blue-400">{log}</span>
|
| 115 |
) : (
|
| 116 |
<span className="text-zinc-500">{log}</span>
|
|
|
|
| 121 |
{status === "booting" && (
|
| 122 |
<div className="flex items-center gap-2 mt-4 text-green-400">
|
| 123 |
<Loader2 size={14} className="animate-spin" />
|
| 124 |
+
<span className="text-sm font-semibold animate-pulse">Synchronizing orchestration layers...</span>
|
| 125 |
</div>
|
| 126 |
)}
|
| 127 |
{status === "ready" && (
|
| 128 |
<div className="flex items-center gap-2 mt-4 text-green-400">
|
| 129 |
<CheckCircle2 size={14} />
|
| 130 |
+
<span className="text-sm font-semibold">Deployment Successful. Redirecting...</span>
|
| 131 |
</div>
|
| 132 |
)}
|
| 133 |
{status === "error" && (
|
| 134 |
<div className="flex items-center gap-2 mt-4 text-red-500">
|
| 135 |
<XCircle size={14} />
|
| 136 |
+
<span className="text-sm font-semibold">Deployment Engine Stalled.</span>
|
| 137 |
</div>
|
| 138 |
)}
|
| 139 |
<div ref={endRef} />
|
|
|
|
| 147 |
className="flex items-center gap-2 px-4 py-2 bg-[#161b22] hover:bg-[#21262d] text-zinc-300 hover:text-white rounded-lg text-sm transition-all border border-[#30363d] hover:border-[#444]"
|
| 148 |
>
|
| 149 |
<ArrowLeft size={14} />
|
| 150 |
+
Back to Dashboard
|
| 151 |
</button>
|
| 152 |
<button
|
| 153 |
onClick={() => boot()}
|
lib/docker/manager.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
| 1 |
import fs from 'fs';
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
/**
|
| 4 |
* Registry for native workspace processes (IDE instances running outside Docker)
|
| 5 |
-
* Map<workspaceId, { pid: number; port: number }>
|
| 6 |
*/
|
| 7 |
-
const nativeProcesses = new Map<string, { pid: number; port: number }>();
|
| 8 |
|
| 9 |
/**
|
| 10 |
* Checks if a native workspace is currently running.
|
|
@@ -18,17 +21,30 @@ export function isNativeWorkspaceRunning(id: string): boolean {
|
|
| 18 |
*/
|
| 19 |
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
/**
|
| 22 |
* Checks if Docker is available in the current environment.
|
| 23 |
*/
|
| 24 |
-
export async function isDockerAvailable(): Promise<boolean> {
|
| 25 |
const socketPath = process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock';
|
|
|
|
|
|
|
| 26 |
try {
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
return
|
| 30 |
} catch {
|
| 31 |
-
return false;
|
| 32 |
}
|
| 33 |
}
|
| 34 |
|
|
@@ -36,14 +52,14 @@ export async function isDockerAvailable(): Promise<boolean> {
|
|
| 36 |
* Stops a native workspace process.
|
| 37 |
*/
|
| 38 |
export async function stopNativeWorkspace(id: string): Promise<boolean> {
|
| 39 |
-
const
|
| 40 |
-
if (
|
| 41 |
try {
|
| 42 |
-
|
| 43 |
nativeProcesses.delete(id);
|
| 44 |
return true;
|
| 45 |
} catch (e) {
|
| 46 |
-
console.error(`Failed to kill
|
| 47 |
nativeProcesses.delete(id);
|
| 48 |
}
|
| 49 |
}
|
|
@@ -87,52 +103,104 @@ export interface WorkspaceOperationResult {
|
|
| 87 |
}
|
| 88 |
|
| 89 |
/**
|
| 90 |
-
* Workspace provisioner with
|
|
|
|
| 91 |
*/
|
| 92 |
export async function startWorkspaceContainer(config: WorkspaceConfig): Promise<WorkspaceOperationResult> {
|
| 93 |
const log = (msg: string) => { if (config.onLog) config.onLog(`[MANAGER] ${msg}`); };
|
| 94 |
-
|
| 95 |
-
log(`Initializing Provisioning Sequence for '${config.projectName}'...`);
|
| 96 |
-
await delay(300);
|
| 97 |
-
|
| 98 |
-
const dockerReal = await isDockerAvailable();
|
| 99 |
-
if (dockerReal) {
|
| 100 |
-
log(`Docker daemon detected. Attempting to pull baseline images...`);
|
| 101 |
-
await delay(500);
|
| 102 |
-
// Real Docker logic here (skipped for mock mode)
|
| 103 |
-
} else {
|
| 104 |
-
log(`Restricted environment detected. Reverting to Native Isolation...`);
|
| 105 |
-
await delay(400);
|
| 106 |
-
}
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
log(`
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
}
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
log(`Workspace Successfully Provisioned. Redirecting...`);
|
| 130 |
|
| 131 |
return {
|
| 132 |
-
success:
|
| 133 |
-
|
| 134 |
-
androidPort: config.withAndroidEmulator ? 6080 : undefined,
|
| 135 |
-
port: 8080
|
| 136 |
};
|
| 137 |
}
|
| 138 |
|
|
|
|
| 1 |
import fs from 'fs';
|
| 2 |
+
import { spawn, ChildProcess } from 'child_process';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import Docker from 'dockerode';
|
| 5 |
|
| 6 |
/**
|
| 7 |
* Registry for native workspace processes (IDE instances running outside Docker)
|
| 8 |
+
* Map<workspaceId, { pid: number; port: number; process: ChildProcess }>
|
| 9 |
*/
|
| 10 |
+
const nativeProcesses = new Map<string, { pid: number; port: number; process: ChildProcess }>();
|
| 11 |
|
| 12 |
/**
|
| 13 |
* Checks if a native workspace is currently running.
|
|
|
|
| 21 |
*/
|
| 22 |
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
|
| 23 |
|
| 24 |
+
/**
|
| 25 |
+
* Finds an available port in the 8081-8099 range.
|
| 26 |
+
*/
|
| 27 |
+
function findAvailablePort(): number {
|
| 28 |
+
const occupiedPorts = Array.from(nativeProcesses.values()).map(p => p.port);
|
| 29 |
+
for (let port = 8081; port <= 8099; port++) {
|
| 30 |
+
if (!occupiedPorts.includes(port)) return port;
|
| 31 |
+
}
|
| 32 |
+
return Math.floor(Math.random() * (8999 - 8100) + 8100);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
/**
|
| 36 |
* Checks if Docker is available in the current environment.
|
| 37 |
*/
|
| 38 |
+
export async function isDockerAvailable(): Promise<{ available: boolean; reason?: string }> {
|
| 39 |
const socketPath = process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock';
|
| 40 |
+
if (process.env.SPACE_ID) return { available: false, reason: "Hugging Face Space (Sandboxed)" };
|
| 41 |
+
if (!fs.existsSync(socketPath)) return { available: false, reason: "Docker socket missing" };
|
| 42 |
try {
|
| 43 |
+
const docker = new Docker({ socketPath: process.platform === 'win32' ? undefined : socketPath });
|
| 44 |
+
await docker.ping();
|
| 45 |
+
return { available: true };
|
| 46 |
} catch {
|
| 47 |
+
return { available: false, reason: "Docker daemon unreachable" };
|
| 48 |
}
|
| 49 |
}
|
| 50 |
|
|
|
|
| 52 |
* Stops a native workspace process.
|
| 53 |
*/
|
| 54 |
export async function stopNativeWorkspace(id: string): Promise<boolean> {
|
| 55 |
+
const entry = nativeProcesses.get(id);
|
| 56 |
+
if (entry) {
|
| 57 |
try {
|
| 58 |
+
entry.process.kill();
|
| 59 |
nativeProcesses.delete(id);
|
| 60 |
return true;
|
| 61 |
} catch (e) {
|
| 62 |
+
console.error(`[MANAGER] Failed to kill code-server ${entry.pid}:`, e);
|
| 63 |
nativeProcesses.delete(id);
|
| 64 |
}
|
| 65 |
}
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
/**
|
| 106 |
+
* Workspace provisioner with REAL child-process orchestration.
|
| 107 |
+
* Robust handshake and error handling to prevent "Deployment Engine Failure".
|
| 108 |
*/
|
| 109 |
export async function startWorkspaceContainer(config: WorkspaceConfig): Promise<WorkspaceOperationResult> {
|
| 110 |
const log = (msg: string) => { if (config.onLog) config.onLog(`[MANAGER] ${msg}`); };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
if (nativeProcesses.has(config.id)) {
|
| 113 |
+
log(`Workspace detected. Re-establishing secure proxy link...`);
|
| 114 |
+
return {
|
| 115 |
+
success: true,
|
| 116 |
+
containerId: `native-${config.id}`,
|
| 117 |
+
port: nativeProcesses.get(config.id)!.port
|
| 118 |
+
};
|
| 119 |
+
}
|
| 120 |
|
| 121 |
+
log(`Provisioning real-time isolation for '${config.projectName}'...`);
|
| 122 |
+
|
| 123 |
+
// 1. Prepare Workspace Directory
|
| 124 |
+
const workspaceRoot = process.env.WORKSPACE_ROOT || path.join(process.cwd(), 'workspaces');
|
| 125 |
+
const workspacePath = path.join(workspaceRoot, config.id);
|
| 126 |
+
const userDataPath = path.join(workspacePath, '.vscode-server');
|
| 127 |
+
|
| 128 |
+
if (!fs.existsSync(workspacePath)) {
|
| 129 |
+
fs.mkdirSync(workspacePath, { recursive: true });
|
| 130 |
+
log(`Created isolated filesystem segment: ${config.id.slice(0, 8)}`);
|
| 131 |
+
}
|
| 132 |
|
| 133 |
+
// 2. Identify Target Port
|
| 134 |
+
const port = findAvailablePort();
|
| 135 |
+
log(`Assigned dynamic port: ${port}`);
|
| 136 |
|
| 137 |
+
// 3. Spawn Real code-server Process (Linux priority)
|
| 138 |
+
const shellCommand = process.platform === 'win32' ? 'npx' : 'code-server';
|
| 139 |
+
const args = process.platform === 'win32' ? ['code-server'] : [];
|
| 140 |
+
|
| 141 |
+
const baseArgs = [
|
| 142 |
+
'--auth', 'none',
|
| 143 |
+
'--bind-addr', `127.0.0.1:${port}`,
|
| 144 |
+
'--user-data-dir', userDataPath,
|
| 145 |
+
'--disable-telemetry',
|
| 146 |
+
'--disable-update-check',
|
| 147 |
+
workspacePath
|
| 148 |
+
];
|
| 149 |
+
|
| 150 |
+
const child = spawn(shellCommand, [...args, ...baseArgs], {
|
| 151 |
+
env: { ...process.env, HOME: workspacePath },
|
| 152 |
+
cwd: workspacePath,
|
| 153 |
+
shell: process.platform === 'win32'
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
log(`Spawning VS Code Orchestrator (PID: ${child.pid})...`);
|
| 157 |
+
|
| 158 |
+
// Handle startup errors early
|
| 159 |
+
child.on('error', (err) => {
|
| 160 |
+
log(`[FATAL] Failed to launch IDE binary: ${err.message}`);
|
| 161 |
+
});
|
| 162 |
+
|
| 163 |
+
child.stdout.on('data', (data) => {
|
| 164 |
+
const out = data.toString();
|
| 165 |
+
if (out.includes('listening on')) log(`[UP] ${out.trim()}`);
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
child.stderr.on('data', (data) => {
|
| 169 |
+
const err = data.toString();
|
| 170 |
+
if (err.toLowerCase().includes('error')) log(`[STDERR] ${err.trim()}`);
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
// 4. Register in active pool
|
| 174 |
+
nativeProcesses.set(config.id, { pid: child.pid!, port, process: child });
|
| 175 |
+
|
| 176 |
+
// 5. Robust Handshake Loop (Increased attempts + explicit error on failure)
|
| 177 |
+
let attempts = 0;
|
| 178 |
+
while (attempts < 15) {
|
| 179 |
+
try {
|
| 180 |
+
const res = await fetch(`http://127.0.0.1:${port}`);
|
| 181 |
+
if (res.ok) {
|
| 182 |
+
log(`Handshake verified. CodeVerse Engine Online.`);
|
| 183 |
+
return {
|
| 184 |
+
success: true,
|
| 185 |
+
containerId: `native-${config.id}`,
|
| 186 |
+
androidPort: config.withAndroidEmulator ? 6080 : undefined,
|
| 187 |
+
port: port
|
| 188 |
+
};
|
| 189 |
+
}
|
| 190 |
+
} catch {
|
| 191 |
+
await delay(1000);
|
| 192 |
+
attempts++;
|
| 193 |
+
if (attempts % 3 === 0) log(`Warming up IDE core (attempt ${attempts}/15)...`);
|
| 194 |
+
}
|
| 195 |
}
|
| 196 |
|
| 197 |
+
// Failure Case
|
| 198 |
+
log(`[FATAL] IDE core failed to respond on 127.0.0.1:${port} after 15 attempts.`);
|
| 199 |
+
stopNativeWorkspace(config.id);
|
|
|
|
| 200 |
|
| 201 |
return {
|
| 202 |
+
success: false,
|
| 203 |
+
error: "IDE_HANDSHAKE_TIMEOUT: The orchestration layer failed to reach the IDE process. Check resource limits on Hugging Face."
|
|
|
|
|
|
|
| 204 |
};
|
| 205 |
}
|
| 206 |
|