shubhjn commited on
Commit
a01d7d6
·
1 Parent(s): 61e1ccb
Files changed (4) hide show
  1. dist/lib/docker/manager.js +81 -119
  2. dist/server.js +30 -9
  3. lib/docker/manager.ts +83 -126
  4. server.ts +32 -9
dist/lib/docker/manager.js CHANGED
@@ -44,6 +44,34 @@ exports.stopWorkspaceContainer = stopWorkspaceContainer;
44
  const dockerode_1 = __importDefault(require("dockerode"));
45
  const path_1 = __importDefault(require("path"));
46
  const child_process_1 = require("child_process");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  // Connect to the local Docker daemon
48
  const docker = new dockerode_1.default({ socketPath: process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock' });
49
  // Native process registry to manage non-docker workspaces (HF Fallback)
@@ -83,10 +111,8 @@ async function startNativeWorkspace(config) {
83
  return { success: true, containerId: `native-${id}`, port: String(existing.port) };
84
  }
85
  onLog("[SYSTEM] Docker not detected. Entering Native Isolation Mode...");
86
- // HF standard: use /app/workspaces or the resolved mount
87
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
88
  const dataPath = path_1.default.resolve(process.cwd(), 'workspaces', userId, safeName);
89
- // Simple port allocation (multi-workspace on HF isn't common but we handle it)
90
  const port = 8080 + nativeProcesses.size;
91
  onLog(`[NATIVE] Booting code-server for ${projectName} on port ${port}...`);
92
  const child = (0, child_process_1.spawn)('code-server', [
@@ -102,8 +128,15 @@ async function startNativeWorkspace(config) {
102
  child.stdout.on('data', (data) => onLog(`[NATIVE-STDOUT] ${data}`));
103
  child.stderr.on('data', (data) => onLog(`[NATIVE-STDERR] ${data}`));
104
  nativeProcesses.set(id, { process: child, port });
105
- // Give it a moment to bind
106
- await new Promise(resolve => setTimeout(resolve, 2000));
 
 
 
 
 
 
 
107
  return {
108
  success: true,
109
  containerId: `native-${id}`,
@@ -118,7 +151,7 @@ async function startNativeWorkspace(config) {
118
  * Optionally spins up a sidecar Android emulator container.
119
  */
120
  async function startWorkspaceContainer(config) {
121
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
122
  const { id, userId, projectName, withAndroidEmulator = false, onLog = console.log } = config;
123
  // Check availability first
124
  if (!await isDockerAvailable()) {
@@ -146,12 +179,9 @@ async function startWorkspaceContainer(config) {
146
  if (error.statusCode !== 404) {
147
  throw new Error(`Failed to inspect container: ${error.message}`);
148
  }
149
- // Map the local host path to the workspace
150
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
151
  const dataPath = process.env.DATA_PATH || path_1.default.resolve(process.cwd(), 'workspaces', userId, safeName);
152
- // --- WORKSPACE CONFIG LOGIC AND IMAGE BUILDING ---
153
  const { buildWorkspaceImage } = await Promise.resolve().then(() => __importStar(require('./builder')));
154
- // Let the builder handle parsing and creating the image
155
  const { imageName, config: codeverseConfig } = await buildWorkspaceImage(id, dataPath, onLog);
156
  let workspaceSpecificEnv = [];
157
  if (codeverseConfig.env) {
@@ -160,113 +190,66 @@ async function startWorkspaceContainer(config) {
160
  if ((_c = codeverseConfig.ios) === null || _c === void 0 ? void 0 : _c.appetizeUrl) {
161
  appetizeUrl = codeverseConfig.ios.appetizeUrl;
162
  }
 
163
  const container = await docker.createContainer({
164
  Image: imageName,
165
  name: containerName,
166
  Env: [
167
- 'AUTH=none',
168
- 'PASSWORD=codeverse',
169
- 'SUDO_PASSWORD=codeverse',
170
- 'TZ=UTC',
171
  ...workspaceSpecificEnv
172
  ],
173
- Cmd: ['--auth', 'none'],
174
  HostConfig: {
175
- Binds: [
176
- `${dataPath}:/config/workspace`
177
- ],
178
  PortBindings: {
179
- '8080/tcp': [{ HostPort: '' }]
180
  },
181
- RestartPolicy: {
182
- Name: 'unless-stopped'
183
- }
184
- },
185
- ExposedPorts: {
186
- '8080/tcp': {}
187
  }
188
  });
189
  await container.start();
190
- const info = await container.inspect();
191
- mainContainerId = container.id;
192
- mainPort = (_e = (_d = info.NetworkSettings.Ports['8080/tcp']) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.HostPort;
193
- if (!mainPort) {
194
- throw new Error("Failed to map port 8080 for Code-Server");
195
- }
196
  }
197
- // --- 2. Optional Android sidecar container ---
198
  if (withAndroidEmulator) {
199
- const androidImage = 'budtmo/docker-android-x86-11.0';
200
  try {
201
- // 1. Check if it already exists
202
  const existing = docker.getContainer(androidContainerName);
203
  const info = await existing.inspect();
204
  if (!info.State.Running) {
205
  await existing.start();
206
  }
207
  androidContainerId = info.Id;
208
- androidPort = (_g = (_f = info.NetworkSettings.Ports['6080/tcp']) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.HostPort;
209
  }
210
  catch (e) {
211
  const error = e;
212
- if (error.statusCode !== 404) {
213
- throw new Error(`Failed to inspect Android container: ${error.message}`);
214
- }
215
- // Ensure android image exists
216
- try {
217
- await docker.getImage(androidImage).inspect();
218
- }
219
- catch (_l) {
220
- console.log(`Pulling ${androidImage}... Note: this is a huge image.`);
221
- await new Promise((resolve, reject) => {
222
- docker.pull(androidImage, (err, stream) => {
223
- if (err)
224
- return reject(err);
225
- docker.modem.followProgress(stream, (err, res) => err ? reject(err) : resolve(res));
226
- });
227
  });
 
 
 
 
228
  }
229
- // Start Android container
230
- const androidContainer = await docker.createContainer({
231
- Image: androidImage,
232
- name: androidContainerName,
233
- Env: ['EMULATOR_DEVICE=Samsung Galaxy S10', 'WEB_VNC=true'],
234
- HostConfig: {
235
- Privileged: true, // Required for KVM usually, though some configs might work without
236
- PortBindings: {
237
- '6080/tcp': [{ HostPort: '' }] // Map noVNC port to dynamic host port
238
- },
239
- RestartPolicy: {
240
- Name: 'unless-stopped'
241
- }
242
- },
243
- ExposedPorts: {
244
- '6080/tcp': {}
245
- }
246
- });
247
- await androidContainer.start();
248
- const info = await androidContainer.inspect();
249
- androidContainerId = androidContainer.id;
250
- androidPort = (_j = (_h = info.NetworkSettings.Ports['6080/tcp']) === null || _h === void 0 ? void 0 : _h[0]) === null || _j === void 0 ? void 0 : _j.HostPort;
251
  }
252
  }
253
- // However, if the container was ALREADY running (we short-circuited at the top), we need
254
- // to read appetizeUrl manually as well.
255
- if (!appetizeUrl) {
256
- try {
257
- const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
258
- const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
259
- const dataPath = process.env.DATA_PATH || path_1.default.resolve(process.cwd(), 'workspaces', userId, safeName);
260
- const configPath = path_1.default.join(dataPath, 'codeverse.json');
261
- const configContent = await fs.readFile(configPath, 'utf8');
262
- const customConfig = JSON.parse(configContent);
263
- if ((_k = customConfig.ios) === null || _k === void 0 ? void 0 : _k.appetizeUrl) {
264
- appetizeUrl = customConfig.ios.appetizeUrl;
265
- }
266
- }
267
- catch (_m) {
268
- // ignore if missing on running container
269
- }
270
  }
271
  return {
272
  success: true,
@@ -277,47 +260,26 @@ async function startWorkspaceContainer(config) {
277
  appetizeUrl
278
  };
279
  }
280
- /**
281
- * Stops and optionally removes a workspace container and its sidecars.
282
- */
283
- async function stopWorkspaceContainer(id, remove = false) {
284
- // Check Native Mode first
285
  if (nativeProcesses.has(id)) {
286
- const { process: child } = nativeProcesses.get(id);
287
- child.kill();
288
  nativeProcesses.delete(id);
289
  return { success: true };
290
  }
291
- const containerName = `codeverse-workspace-${id}`;
292
- const androidContainerName = `codeverse-android-${id}`;
293
- let errorMsg = "";
294
  try {
 
295
  const container = docker.getContainer(containerName);
296
  await container.stop();
297
- if (remove) {
298
- await container.remove();
299
- }
300
- }
301
- catch (e) {
302
- const error = e;
303
- // Ignore 404s
304
- if (error.statusCode !== 404)
305
- errorMsg += `Workspace stop error: ${error.message}. `;
306
- }
307
- try {
308
- const androidContainer = docker.getContainer(androidContainerName);
309
- await androidContainer.stop();
310
- if (remove) {
311
- await androidContainer.remove();
312
  }
 
 
313
  }
314
  catch (e) {
315
- const error = e;
316
- if (error.statusCode !== 404)
317
- errorMsg += `Android stop error: ${error.message}. `;
318
- }
319
- if (errorMsg) {
320
- return { success: false, error: errorMsg };
321
  }
322
- return { success: true };
323
  }
 
44
  const dockerode_1 = __importDefault(require("dockerode"));
45
  const path_1 = __importDefault(require("path"));
46
  const child_process_1 = require("child_process");
47
+ const net_1 = __importDefault(require("net"));
48
+ /**
49
+ * Helper to wait for an internal port to become available
50
+ */
51
+ async function waitForPort(port, timeoutMs = 30000) {
52
+ const start = Date.now();
53
+ while (Date.now() - start < timeoutMs) {
54
+ try {
55
+ await new Promise((resolve, reject) => {
56
+ const socket = net_1.default.createConnection(port, '127.0.0.1');
57
+ socket.on('connect', () => {
58
+ socket.end();
59
+ resolve();
60
+ });
61
+ socket.on('error', reject);
62
+ setTimeout(() => {
63
+ socket.destroy();
64
+ reject(new Error('timeout'));
65
+ }, 500);
66
+ });
67
+ return true;
68
+ }
69
+ catch (_a) {
70
+ await new Promise(resolve => setTimeout(resolve, 500));
71
+ }
72
+ }
73
+ return false;
74
+ }
75
  // Connect to the local Docker daemon
76
  const docker = new dockerode_1.default({ socketPath: process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock' });
77
  // Native process registry to manage non-docker workspaces (HF Fallback)
 
111
  return { success: true, containerId: `native-${id}`, port: String(existing.port) };
112
  }
113
  onLog("[SYSTEM] Docker not detected. Entering Native Isolation Mode...");
 
114
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
115
  const dataPath = path_1.default.resolve(process.cwd(), 'workspaces', userId, safeName);
 
116
  const port = 8080 + nativeProcesses.size;
117
  onLog(`[NATIVE] Booting code-server for ${projectName} on port ${port}...`);
118
  const child = (0, child_process_1.spawn)('code-server', [
 
128
  child.stdout.on('data', (data) => onLog(`[NATIVE-STDOUT] ${data}`));
129
  child.stderr.on('data', (data) => onLog(`[NATIVE-STDERR] ${data}`));
130
  nativeProcesses.set(id, { process: child, port });
131
+ // Wait for code-server to be ready
132
+ onLog(`[NATIVE] Waiting for code-server to bind to 127.0.0.1:${port}...`);
133
+ const ready = await waitForPort(port);
134
+ if (!ready) {
135
+ onLog(`[FATAL] code-server failed to bind within timeout.`);
136
+ }
137
+ else {
138
+ onLog(`[READY] code-server is now listening on port ${port}.`);
139
+ }
140
  return {
141
  success: true,
142
  containerId: `native-${id}`,
 
151
  * Optionally spins up a sidecar Android emulator container.
152
  */
153
  async function startWorkspaceContainer(config) {
154
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
155
  const { id, userId, projectName, withAndroidEmulator = false, onLog = console.log } = config;
156
  // Check availability first
157
  if (!await isDockerAvailable()) {
 
179
  if (error.statusCode !== 404) {
180
  throw new Error(`Failed to inspect container: ${error.message}`);
181
  }
 
182
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
183
  const dataPath = process.env.DATA_PATH || path_1.default.resolve(process.cwd(), 'workspaces', userId, safeName);
 
184
  const { buildWorkspaceImage } = await Promise.resolve().then(() => __importStar(require('./builder')));
 
185
  const { imageName, config: codeverseConfig } = await buildWorkspaceImage(id, dataPath, onLog);
186
  let workspaceSpecificEnv = [];
187
  if (codeverseConfig.env) {
 
190
  if ((_c = codeverseConfig.ios) === null || _c === void 0 ? void 0 : _c.appetizeUrl) {
191
  appetizeUrl = codeverseConfig.ios.appetizeUrl;
192
  }
193
+ onLog(`[DOCKER] Spawning container ${containerName} using image ${imageName}...`);
194
  const container = await docker.createContainer({
195
  Image: imageName,
196
  name: containerName,
197
  Env: [
198
+ `PUID=${((_d = process.getuid) === null || _d === void 0 ? void 0 : _d.call(process)) || 1000}`,
199
+ `PGID=${((_e = process.getgid) === null || _e === void 0 ? void 0 : _e.call(process)) || 1000}`,
200
+ `TZ=Etc/UTC`,
 
201
  ...workspaceSpecificEnv
202
  ],
 
203
  HostConfig: {
204
+ Binds: [`${dataPath}:/home/coder/project`],
 
 
205
  PortBindings: {
206
+ '8080/tcp': [{ HostPort: '0' }]
207
  },
208
+ RestartPolicy: { Name: 'unless-stopped' }
 
 
 
 
 
209
  }
210
  });
211
  await container.start();
212
+ const inspect = await container.inspect();
213
+ mainContainerId = inspect.Id;
214
+ mainPort = (_g = (_f = inspect.NetworkSettings.Ports['8080/tcp']) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.HostPort;
 
 
 
215
  }
216
+ // --- 2. Android Sidecar Container (Optional) ---
217
  if (withAndroidEmulator) {
 
218
  try {
 
219
  const existing = docker.getContainer(androidContainerName);
220
  const info = await existing.inspect();
221
  if (!info.State.Running) {
222
  await existing.start();
223
  }
224
  androidContainerId = info.Id;
225
+ androidPort = ((_j = (_h = info.NetworkSettings.Ports['6080/tcp']) === null || _h === void 0 ? void 0 : _h[0]) === null || _j === void 0 ? void 0 : _j.HostPort) || '6080';
226
  }
227
  catch (e) {
228
  const error = e;
229
+ if (error.statusCode === 404) {
230
+ onLog(`[DOCKER] Spawning Android sidecar ${androidContainerName}...`);
231
+ const container = await docker.createContainer({
232
+ Image: 'shubhjn/codeverse-android:latest',
233
+ name: androidContainerName,
234
+ HostConfig: {
235
+ PortBindings: {
236
+ '6080/tcp': [{ HostPort: '0' }]
237
+ },
238
+ RestartPolicy: { Name: 'unless-stopped' },
239
+ Privileged: true
240
+ }
 
 
 
241
  });
242
+ await container.start();
243
+ const inspect = await container.inspect();
244
+ androidContainerId = inspect.Id;
245
+ androidPort = (_l = (_k = inspect.NetworkSettings.Ports['6080/tcp']) === null || _k === void 0 ? void 0 : _k[0]) === null || _l === void 0 ? void 0 : _l.HostPort;
246
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
  }
249
+ // Polling for readiness
250
+ if (mainPort) {
251
+ onLog(`[DOCKER] Waiting for code-server to be ready at port ${mainPort}...`);
252
+ await waitForPort(parseInt(mainPort));
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
254
  return {
255
  success: true,
 
260
  appetizeUrl
261
  };
262
  }
263
+ async function stopWorkspaceContainer(id) {
 
 
 
 
264
  if (nativeProcesses.has(id)) {
265
+ const { process } = nativeProcesses.get(id);
266
+ process.kill();
267
  nativeProcesses.delete(id);
268
  return { success: true };
269
  }
 
 
 
270
  try {
271
+ const containerName = `codeverse-workspace-${id}`;
272
  const container = docker.getContainer(containerName);
273
  await container.stop();
274
+ try {
275
+ const androidContainerName = `codeverse-android-${id}`;
276
+ const androidContainer = docker.getContainer(androidContainerName);
277
+ await androidContainer.stop();
 
 
 
 
 
 
 
 
 
 
 
278
  }
279
+ catch (_a) { }
280
+ return { success: true };
281
  }
282
  catch (e) {
283
+ return { success: false, error: e.message };
 
 
 
 
 
284
  }
 
285
  }
dist/server.js CHANGED
@@ -74,6 +74,19 @@ proxy.on("error", (err, req, res) => {
74
  res.end("Workspace Proxy Error");
75
  }
76
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  console.log(`[BOOT] NODE_ENV: ${process.env.NODE_ENV}, DEV: ${dev}`);
78
  console.log("[BOOT] Initializing Next.js app.prepare()...");
79
  app.prepare()
@@ -89,24 +102,32 @@ app.prepare()
89
  // 1. Workspace IDE Proxy (/workspace/:id/)
90
  if (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/workspace/")) {
91
  const parts = pathname.split("/");
92
- const port = (0, manager_1.getNativeWorkspacePort)(parts[2]) || 8080; // Fallback to 8080 for Docker
93
- // Strip the /workspace/:id prefix when forwarding to code-server
 
 
94
  req.url = "/" + parts.slice(3).join("/");
95
- return proxy.web(req, res, { target: `http://localhost:${port}` });
96
  }
97
  // 2. Android NoVNC Proxy (/android/:id/)
98
  if (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/android/")) {
99
  const parts = pathname.split("/");
100
- const port = (0, manager_1.getAndroidPort)(parts[2]) || 6080;
 
 
 
101
  req.url = "/" + parts.slice(3).join("/");
102
- return proxy.web(req, res, { target: `http://localhost:${port}` });
103
  }
104
  // 3. User Web Preview Proxy (/preview/:id/)
105
  if (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/preview/")) {
106
  const parts = pathname.split("/");
107
- const port = 3000; // Default user dev server port
 
 
 
108
  req.url = "/" + parts.slice(3).join("/");
109
- return proxy.web(req, res, { target: `http://localhost:${port}` });
110
  }
111
  handle(req, res, parsedUrl);
112
  });
@@ -128,13 +149,13 @@ app.prepare()
128
  const parts = pathname.split("/");
129
  const port = (0, manager_1.getNativeWorkspacePort)(parts[2]) || 8080;
130
  req.url = "/" + parts.slice(3).join("/");
131
- return proxy.ws(req, socket, head, { target: `http://localhost:${port}` });
132
  }
133
  if (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/android/")) {
134
  const parts = pathname.split("/");
135
  const port = (0, manager_1.getAndroidPort)(parts[2]) || 6080;
136
  req.url = "/" + parts.slice(3).join("/");
137
- return proxy.ws(req, socket, head, { target: `http://localhost:${port}` });
138
  }
139
  });
140
  yjsWss.on("connection", (conn, request) => {
 
74
  res.end("Workspace Proxy Error");
75
  }
76
  });
77
+ proxy.on("proxyRes", (proxyRes, req, res) => {
78
+ // 1. Rewrite Location redirects to include the path prefix (Fixes redirect loops)
79
+ const id = req.headers['x-codeverse-id'];
80
+ const type = req.headers['x-codeverse-type'];
81
+ if (id && type && proxyRes.headers.location) {
82
+ const originalLocation = proxyRes.headers.location;
83
+ // Ensure we don't double-prefix if it's already an absolute URL to another domain
84
+ if (originalLocation.startsWith('/') && !originalLocation.startsWith(`/${type}/${id}`)) {
85
+ proxyRes.headers.location = `/${type}/${id}${originalLocation}`;
86
+ console.log(`[PROXY-REWRITE] Redirect ${originalLocation} -> ${proxyRes.headers.location}`);
87
+ }
88
+ }
89
+ });
90
  console.log(`[BOOT] NODE_ENV: ${process.env.NODE_ENV}, DEV: ${dev}`);
91
  console.log("[BOOT] Initializing Next.js app.prepare()...");
92
  app.prepare()
 
102
  // 1. Workspace IDE Proxy (/workspace/:id/)
103
  if (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/workspace/")) {
104
  const parts = pathname.split("/");
105
+ const id = parts[2];
106
+ const port = (0, manager_1.getNativeWorkspacePort)(id) || 8080;
107
+ req.headers['x-codeverse-id'] = id;
108
+ req.headers['x-codeverse-type'] = 'workspace';
109
  req.url = "/" + parts.slice(3).join("/");
110
+ return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
111
  }
112
  // 2. Android NoVNC Proxy (/android/:id/)
113
  if (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/android/")) {
114
  const parts = pathname.split("/");
115
+ const id = parts[2];
116
+ const port = (0, manager_1.getAndroidPort)(id) || 6080;
117
+ req.headers['x-codeverse-id'] = id;
118
+ req.headers['x-codeverse-type'] = 'android';
119
  req.url = "/" + parts.slice(3).join("/");
120
+ return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
121
  }
122
  // 3. User Web Preview Proxy (/preview/:id/)
123
  if (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/preview/")) {
124
  const parts = pathname.split("/");
125
+ const id = parts[2];
126
+ const port = 3000;
127
+ req.headers['x-codeverse-id'] = id;
128
+ req.headers['x-codeverse-type'] = 'preview';
129
  req.url = "/" + parts.slice(3).join("/");
130
+ return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
131
  }
132
  handle(req, res, parsedUrl);
133
  });
 
149
  const parts = pathname.split("/");
150
  const port = (0, manager_1.getNativeWorkspacePort)(parts[2]) || 8080;
151
  req.url = "/" + parts.slice(3).join("/");
152
+ return proxy.ws(req, socket, head, { target: `http://127.0.0.1:${port}` });
153
  }
154
  if (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith("/android/")) {
155
  const parts = pathname.split("/");
156
  const port = (0, manager_1.getAndroidPort)(parts[2]) || 6080;
157
  req.url = "/" + parts.slice(3).join("/");
158
+ return proxy.ws(req, socket, head, { target: `http://127.0.0.1:${port}` });
159
  }
160
  });
161
  yjsWss.on("connection", (conn, request) => {
lib/docker/manager.ts CHANGED
@@ -1,7 +1,34 @@
1
  import Docker from 'dockerode';
2
  import path from 'path';
3
  import { spawn, ChildProcess } from 'child_process';
4
- import { existsSync } from 'fs';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  // Connect to the local Docker daemon
7
  const docker = new Docker({ socketPath: process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock' });
@@ -57,11 +84,9 @@ async function startNativeWorkspace(config: WorkspaceConfig) {
57
 
58
  onLog("[SYSTEM] Docker not detected. Entering Native Isolation Mode...");
59
 
60
- // HF standard: use /app/workspaces or the resolved mount
61
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
62
  const dataPath = path.resolve(process.cwd(), 'workspaces', userId, safeName);
63
 
64
- // Simple port allocation (multi-workspace on HF isn't common but we handle it)
65
  const port = 8080 + nativeProcesses.size;
66
 
67
  onLog(`[NATIVE] Booting code-server for ${projectName} on port ${port}...`);
@@ -82,8 +107,14 @@ async function startNativeWorkspace(config: WorkspaceConfig) {
82
 
83
  nativeProcesses.set(id, { process: child, port });
84
 
85
- // Give it a moment to bind
86
- await new Promise(resolve => setTimeout(resolve, 2000));
 
 
 
 
 
 
87
 
88
  return {
89
  success: true,
@@ -132,14 +163,10 @@ export async function startWorkspaceContainer(config: WorkspaceConfig) {
132
  throw new Error(`Failed to inspect container: ${error.message}`);
133
  }
134
 
135
- // Map the local host path to the workspace
136
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
137
  const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'workspaces', userId, safeName);
138
 
139
- // --- WORKSPACE CONFIG LOGIC AND IMAGE BUILDING ---
140
  const { buildWorkspaceImage } = await import('./builder');
141
-
142
- // Let the builder handle parsing and creating the image
143
  const { imageName, config: codeverseConfig } = await buildWorkspaceImage(id, dataPath, onLog);
144
 
145
  let workspaceSpecificEnv: string[] = [];
@@ -151,117 +178,69 @@ export async function startWorkspaceContainer(config: WorkspaceConfig) {
151
  appetizeUrl = codeverseConfig.ios.appetizeUrl;
152
  }
153
 
 
154
  const container = await docker.createContainer({
155
  Image: imageName,
156
  name: containerName,
157
  Env: [
158
- 'AUTH=none',
159
- 'PASSWORD=codeverse',
160
- 'SUDO_PASSWORD=codeverse',
161
- 'TZ=UTC',
162
  ...workspaceSpecificEnv
163
  ],
164
- Cmd: ['--auth', 'none'],
165
  HostConfig: {
166
- Binds: [
167
- `${dataPath}:/config/workspace`
168
- ],
169
  PortBindings: {
170
- '8080/tcp': [{ HostPort: '' }]
171
  },
172
- RestartPolicy: {
173
- Name: 'unless-stopped'
174
- }
175
- },
176
- ExposedPorts: {
177
- '8080/tcp': {}
178
  }
179
  });
180
 
181
  await container.start();
182
-
183
- const info = await container.inspect();
184
- mainContainerId = container.id;
185
- mainPort = info.NetworkSettings.Ports['8080/tcp']?.[0]?.HostPort;
186
-
187
- if (!mainPort) {
188
- throw new Error("Failed to map port 8080 for Code-Server");
189
- }
190
  }
191
 
192
- // --- 2. Optional Android sidecar container ---
193
  if (withAndroidEmulator) {
194
- const androidImage = 'budtmo/docker-android-x86-11.0';
195
  try {
196
- // 1. Check if it already exists
197
  const existing = docker.getContainer(androidContainerName);
198
  const info = await existing.inspect();
199
  if (!info.State.Running) {
200
  await existing.start();
201
  }
202
  androidContainerId = info.Id;
203
- androidPort = info.NetworkSettings.Ports['6080/tcp']?.[0]?.HostPort;
204
  } catch (e: unknown) {
205
  const error = e as Error & { statusCode?: number };
206
- if (error.statusCode !== 404) {
207
- throw new Error(`Failed to inspect Android container: ${error.message}`);
208
- }
209
-
210
- // Ensure android image exists
211
- try {
212
- await docker.getImage(androidImage).inspect();
213
- } catch {
214
- console.log(`Pulling ${androidImage}... Note: this is a huge image.`);
215
- await new Promise((resolve, reject) => {
216
- docker.pull(androidImage, (err: unknown, stream: NodeJS.ReadableStream) => {
217
- if (err) return reject(err);
218
- docker.modem.followProgress(stream, (err: unknown, res: unknown[]) => err ? reject(err) : resolve(res));
219
- });
220
- });
221
- }
222
-
223
- // Start Android container
224
- const androidContainer = await docker.createContainer({
225
- Image: androidImage,
226
- name: androidContainerName,
227
- Env: ['EMULATOR_DEVICE=Samsung Galaxy S10', 'WEB_VNC=true'],
228
- HostConfig: {
229
- Privileged: true, // Required for KVM usually, though some configs might work without
230
- PortBindings: {
231
- '6080/tcp': [{ HostPort: '' }] // Map noVNC port to dynamic host port
232
- },
233
- RestartPolicy: {
234
- Name: 'unless-stopped'
235
  }
236
- },
237
- ExposedPorts: {
238
- '6080/tcp': {}
239
- }
240
- });
241
 
242
- await androidContainer.start();
243
- const info = await androidContainer.inspect();
244
- androidContainerId = androidContainer.id;
245
- androidPort = info.NetworkSettings.Ports['6080/tcp']?.[0]?.HostPort;
 
246
  }
247
  }
248
 
249
- // However, if the container was ALREADY running (we short-circuited at the top), we need
250
- // to read appetizeUrl manually as well.
251
- if (!appetizeUrl) {
252
- try {
253
- const fs = await import('fs/promises');
254
- const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
255
- const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'workspaces', userId, safeName);
256
- const configPath = path.join(dataPath, 'codeverse.json');
257
- const configContent = await fs.readFile(configPath, 'utf8');
258
- const customConfig = JSON.parse(configContent);
259
- if (customConfig.ios?.appetizeUrl) {
260
- appetizeUrl = customConfig.ios.appetizeUrl;
261
- }
262
- } catch {
263
- // ignore if missing on running container
264
- }
265
  }
266
 
267
  return {
@@ -274,49 +253,27 @@ export async function startWorkspaceContainer(config: WorkspaceConfig) {
274
  };
275
  }
276
 
277
- /**
278
- * Stops and optionally removes a workspace container and its sidecars.
279
- */
280
- export async function stopWorkspaceContainer(id: string, remove = false) {
281
- // Check Native Mode first
282
  if (nativeProcesses.has(id)) {
283
- const { process: child } = nativeProcesses.get(id)!;
284
- child.kill();
285
  nativeProcesses.delete(id);
286
  return { success: true };
287
  }
288
 
289
- const containerName = `codeverse-workspace-${id}`;
290
- const androidContainerName = `codeverse-android-${id}`;
291
-
292
- let errorMsg = "";
293
-
294
  try {
 
295
  const container = docker.getContainer(containerName);
296
  await container.stop();
297
- if (remove) {
298
- await container.remove();
299
- }
300
- } catch (e: unknown) {
301
- const error = e as Error & { statusCode?: number };
302
- // Ignore 404s
303
- if (error.statusCode !== 404) errorMsg += `Workspace stop error: ${error.message}. `;
304
- }
305
-
306
- try {
307
- const androidContainer = docker.getContainer(androidContainerName);
308
- await androidContainer.stop();
309
- if (remove) {
310
- await androidContainer.remove();
311
- }
312
- } catch (e: unknown) {
313
- const error = e as Error & { statusCode?: number };
314
- if (error.statusCode !== 404) errorMsg += `Android stop error: ${error.message}. `;
315
- }
316
-
317
- if (errorMsg) {
318
- return { success: false, error: errorMsg };
319
  }
320
-
321
- return { success: true };
322
  }
 
1
  import Docker from 'dockerode';
2
  import path from 'path';
3
  import { spawn, ChildProcess } from 'child_process';
4
+ import net from 'net';
5
+
6
+ /**
7
+ * Helper to wait for an internal port to become available
8
+ */
9
+ async function waitForPort(port: number, timeoutMs = 30000): Promise<boolean> {
10
+ const start = Date.now();
11
+ while (Date.now() - start < timeoutMs) {
12
+ try {
13
+ await new Promise<void>((resolve, reject) => {
14
+ const socket = net.createConnection(port, '127.0.0.1');
15
+ socket.on('connect', () => {
16
+ socket.end();
17
+ resolve();
18
+ });
19
+ socket.on('error', reject);
20
+ setTimeout(() => {
21
+ socket.destroy();
22
+ reject(new Error('timeout'));
23
+ }, 500);
24
+ });
25
+ return true;
26
+ } catch {
27
+ await new Promise(resolve => setTimeout(resolve, 500));
28
+ }
29
+ }
30
+ return false;
31
+ }
32
 
33
  // Connect to the local Docker daemon
34
  const docker = new Docker({ socketPath: process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock' });
 
84
 
85
  onLog("[SYSTEM] Docker not detected. Entering Native Isolation Mode...");
86
 
 
87
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
88
  const dataPath = path.resolve(process.cwd(), 'workspaces', userId, safeName);
89
 
 
90
  const port = 8080 + nativeProcesses.size;
91
 
92
  onLog(`[NATIVE] Booting code-server for ${projectName} on port ${port}...`);
 
107
 
108
  nativeProcesses.set(id, { process: child, port });
109
 
110
+ // Wait for code-server to be ready
111
+ onLog(`[NATIVE] Waiting for code-server to bind to 127.0.0.1:${port}...`);
112
+ const ready = await waitForPort(port);
113
+ if (!ready) {
114
+ onLog(`[FATAL] code-server failed to bind within timeout.`);
115
+ } else {
116
+ onLog(`[READY] code-server is now listening on port ${port}.`);
117
+ }
118
 
119
  return {
120
  success: true,
 
163
  throw new Error(`Failed to inspect container: ${error.message}`);
164
  }
165
 
 
166
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
167
  const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'workspaces', userId, safeName);
168
 
 
169
  const { buildWorkspaceImage } = await import('./builder');
 
 
170
  const { imageName, config: codeverseConfig } = await buildWorkspaceImage(id, dataPath, onLog);
171
 
172
  let workspaceSpecificEnv: string[] = [];
 
178
  appetizeUrl = codeverseConfig.ios.appetizeUrl;
179
  }
180
 
181
+ onLog(`[DOCKER] Spawning container ${containerName} using image ${imageName}...`);
182
  const container = await docker.createContainer({
183
  Image: imageName,
184
  name: containerName,
185
  Env: [
186
+ `PUID=${process.getuid?.() || 1000}`,
187
+ `PGID=${process.getgid?.() || 1000}`,
188
+ `TZ=Etc/UTC`,
 
189
  ...workspaceSpecificEnv
190
  ],
 
191
  HostConfig: {
192
+ Binds: [`${dataPath}:/home/coder/project`],
 
 
193
  PortBindings: {
194
+ '8080/tcp': [{ HostPort: '0' }]
195
  },
196
+ RestartPolicy: { Name: 'unless-stopped' }
 
 
 
 
 
197
  }
198
  });
199
 
200
  await container.start();
201
+ const inspect = await container.inspect();
202
+ mainContainerId = inspect.Id;
203
+ mainPort = inspect.NetworkSettings.Ports['8080/tcp']?.[0]?.HostPort;
 
 
 
 
 
204
  }
205
 
206
+ // --- 2. Android Sidecar Container (Optional) ---
207
  if (withAndroidEmulator) {
 
208
  try {
 
209
  const existing = docker.getContainer(androidContainerName);
210
  const info = await existing.inspect();
211
  if (!info.State.Running) {
212
  await existing.start();
213
  }
214
  androidContainerId = info.Id;
215
+ androidPort = info.NetworkSettings.Ports['6080/tcp']?.[0]?.HostPort || '6080';
216
  } catch (e: unknown) {
217
  const error = e as Error & { statusCode?: number };
218
+ if (error.statusCode === 404) {
219
+ onLog(`[DOCKER] Spawning Android sidecar ${androidContainerName}...`);
220
+ const container = await docker.createContainer({
221
+ Image: 'shubhjn/codeverse-android:latest',
222
+ name: androidContainerName,
223
+ HostConfig: {
224
+ PortBindings: {
225
+ '6080/tcp': [{ HostPort: '0' }]
226
+ },
227
+ RestartPolicy: { Name: 'unless-stopped' },
228
+ Privileged: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  }
230
+ });
 
 
 
 
231
 
232
+ await container.start();
233
+ const inspect = await container.inspect();
234
+ androidContainerId = inspect.Id;
235
+ androidPort = inspect.NetworkSettings.Ports['6080/tcp']?.[0]?.HostPort;
236
+ }
237
  }
238
  }
239
 
240
+ // Polling for readiness
241
+ if (mainPort) {
242
+ onLog(`[DOCKER] Waiting for code-server to be ready at port ${mainPort}...`);
243
+ await waitForPort(parseInt(mainPort));
 
 
 
 
 
 
 
 
 
 
 
 
244
  }
245
 
246
  return {
 
253
  };
254
  }
255
 
256
+ export async function stopWorkspaceContainer(id: string) {
 
 
 
 
257
  if (nativeProcesses.has(id)) {
258
+ const { process } = nativeProcesses.get(id)!;
259
+ process.kill();
260
  nativeProcesses.delete(id);
261
  return { success: true };
262
  }
263
 
 
 
 
 
 
264
  try {
265
+ const containerName = `codeverse-workspace-${id}`;
266
  const container = docker.getContainer(containerName);
267
  await container.stop();
268
+
269
+ try {
270
+ const androidContainerName = `codeverse-android-${id}`;
271
+ const androidContainer = docker.getContainer(androidContainerName);
272
+ await androidContainer.stop();
273
+ } catch {}
274
+
275
+ return { success: true };
276
+ } catch (e) {
277
+ return { success: false, error: (e as Error).message };
 
 
 
 
 
 
 
 
 
 
 
 
278
  }
 
 
279
  }
server.ts CHANGED
@@ -40,6 +40,21 @@ proxy.on("error", (err: Error, req: IncomingMessage, res: ServerResponse | Duple
40
  }
41
  });
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  console.log(`[BOOT] NODE_ENV: ${process.env.NODE_ENV}, DEV: ${dev}`);
44
  console.log("[BOOT] Initializing Next.js app.prepare()...");
45
 
@@ -60,29 +75,37 @@ app.prepare()
60
  // 1. Workspace IDE Proxy (/workspace/:id/)
61
  if (pathname?.startsWith("/workspace/")) {
62
  const parts = pathname.split("/");
63
- const port = getNativeWorkspacePort(parts[2]) || 8080; // Fallback to 8080 for Docker
 
64
 
65
- // Strip the /workspace/:id prefix when forwarding to code-server
 
66
  req.url = "/" + parts.slice(3).join("/");
67
- return proxy.web(req, res, { target: `http://localhost:${port}` });
68
  }
69
 
70
  // 2. Android NoVNC Proxy (/android/:id/)
71
  if (pathname?.startsWith("/android/")) {
72
  const parts = pathname.split("/");
73
- const port = getAndroidPort(parts[2]) || 6080;
 
74
 
 
 
75
  req.url = "/" + parts.slice(3).join("/");
76
- return proxy.web(req, res, { target: `http://localhost:${port}` });
77
  }
78
 
79
  // 3. User Web Preview Proxy (/preview/:id/)
80
  if (pathname?.startsWith("/preview/")) {
81
  const parts = pathname.split("/");
82
- const port = 3000; // Default user dev server port
 
83
 
 
 
84
  req.url = "/" + parts.slice(3).join("/");
85
- return proxy.web(req, res, { target: `http://localhost:${port}` });
86
  }
87
 
88
  handle(req, res, parsedUrl);
@@ -110,14 +133,14 @@ app.prepare()
110
  const parts = pathname.split("/");
111
  const port = getNativeWorkspacePort(parts[2]) || 8080;
112
  req.url = "/" + parts.slice(3).join("/");
113
- return proxy.ws(req, socket, head, { target: `http://localhost:${port}` });
114
  }
115
 
116
  if (pathname?.startsWith("/android/")) {
117
  const parts = pathname.split("/");
118
  const port = getAndroidPort(parts[2]) || 6080;
119
  req.url = "/" + parts.slice(3).join("/");
120
- return proxy.ws(req, socket, head, { target: `http://localhost:${port}` });
121
  }
122
  });
123
 
 
40
  }
41
  });
42
 
43
+ proxy.on("proxyRes", (proxyRes, req, res) => {
44
+ // 1. Rewrite Location redirects to include the path prefix (Fixes redirect loops)
45
+ const id = req.headers['x-codeverse-id'] as string;
46
+ const type = req.headers['x-codeverse-type'] as string;
47
+
48
+ if (id && type && proxyRes.headers.location) {
49
+ const originalLocation = proxyRes.headers.location;
50
+ // Ensure we don't double-prefix if it's already an absolute URL to another domain
51
+ if (originalLocation.startsWith('/') && !originalLocation.startsWith(`/${type}/${id}`)) {
52
+ proxyRes.headers.location = `/${type}/${id}${originalLocation}`;
53
+ console.log(`[PROXY-REWRITE] Redirect ${originalLocation} -> ${proxyRes.headers.location}`);
54
+ }
55
+ }
56
+ });
57
+
58
  console.log(`[BOOT] NODE_ENV: ${process.env.NODE_ENV}, DEV: ${dev}`);
59
  console.log("[BOOT] Initializing Next.js app.prepare()...");
60
 
 
75
  // 1. Workspace IDE Proxy (/workspace/:id/)
76
  if (pathname?.startsWith("/workspace/")) {
77
  const parts = pathname.split("/");
78
+ const id = parts[2];
79
+ const port = getNativeWorkspacePort(id) || 8080;
80
 
81
+ req.headers['x-codeverse-id'] = id;
82
+ req.headers['x-codeverse-type'] = 'workspace';
83
  req.url = "/" + parts.slice(3).join("/");
84
+ return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
85
  }
86
 
87
  // 2. Android NoVNC Proxy (/android/:id/)
88
  if (pathname?.startsWith("/android/")) {
89
  const parts = pathname.split("/");
90
+ const id = parts[2];
91
+ const port = getAndroidPort(id) || 6080;
92
 
93
+ req.headers['x-codeverse-id'] = id;
94
+ req.headers['x-codeverse-type'] = 'android';
95
  req.url = "/" + parts.slice(3).join("/");
96
+ return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
97
  }
98
 
99
  // 3. User Web Preview Proxy (/preview/:id/)
100
  if (pathname?.startsWith("/preview/")) {
101
  const parts = pathname.split("/");
102
+ const id = parts[2];
103
+ const port = 3000;
104
 
105
+ req.headers['x-codeverse-id'] = id;
106
+ req.headers['x-codeverse-type'] = 'preview';
107
  req.url = "/" + parts.slice(3).join("/");
108
+ return proxy.web(req, res, { target: `http://127.0.0.1:${port}`, changeOrigin: true });
109
  }
110
 
111
  handle(req, res, parsedUrl);
 
133
  const parts = pathname.split("/");
134
  const port = getNativeWorkspacePort(parts[2]) || 8080;
135
  req.url = "/" + parts.slice(3).join("/");
136
+ return proxy.ws(req, socket, head, { target: `http://127.0.0.1:${port}` });
137
  }
138
 
139
  if (pathname?.startsWith("/android/")) {
140
  const parts = pathname.split("/");
141
  const port = getAndroidPort(parts[2]) || 6080;
142
  req.url = "/" + parts.slice(3).join("/");
143
+ return proxy.ws(req, socket, head, { target: `http://127.0.0.1:${port}` });
144
  }
145
  });
146