cloud-ide-cide 2.0.35 → 2.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deployer/node/upload-api.js +182 -8
- package/package.json +1 -1
|
@@ -8,11 +8,20 @@
|
|
|
8
8
|
* Stores zips in: ../.cide/releases/{appCode}/{version}.zip (for rollback)
|
|
9
9
|
* Logs to: ../.cide/logs/deploy.log
|
|
10
10
|
*
|
|
11
|
+
* After upload/rollback the listener automatically:
|
|
12
|
+
* 1. Runs npm install in the app directory
|
|
13
|
+
* 2. Stops the previous app process (if any)
|
|
14
|
+
* 3. Starts the app with `node server.js`
|
|
15
|
+
* 4. Tracks the app PID for future stop/restart
|
|
16
|
+
*
|
|
11
17
|
* Endpoints (all except /health require Authorization: Bearer <token>):
|
|
12
18
|
*
|
|
13
|
-
* POST /upload (multipart/form-data) — upload &
|
|
19
|
+
* POST /upload (multipart/form-data) — upload, deploy & start app
|
|
14
20
|
* GET /history?appCode=xxx — deployment history
|
|
15
|
-
* POST /rollback { appCode, version } —
|
|
21
|
+
* POST /rollback { appCode, version } — rollback & restart app
|
|
22
|
+
* POST /app/stop { appCode } — stop a running app
|
|
23
|
+
* POST /app/start { appCode } — start a stopped app
|
|
24
|
+
* GET /app/status?appCode=xxx — check app process status
|
|
16
25
|
* GET /health — uptime check (no auth)
|
|
17
26
|
*/
|
|
18
27
|
|
|
@@ -21,6 +30,7 @@ const multer = require('multer');
|
|
|
21
30
|
const path = require('path');
|
|
22
31
|
const fs = require('fs');
|
|
23
32
|
const AdmZip = require('adm-zip');
|
|
33
|
+
const { execSync, spawn } = require('child_process');
|
|
24
34
|
|
|
25
35
|
const app = express();
|
|
26
36
|
app.use(express.json());
|
|
@@ -37,6 +47,7 @@ const PORT = process.env.CIDE_UPLOAD_PORT || process.env.PORT || 4500;
|
|
|
37
47
|
const RELEASES = config.paths?.releases || path.join(CIDE_DIR, 'releases');
|
|
38
48
|
const LOGS = config.paths?.logs || path.join(CIDE_DIR, 'logs');
|
|
39
49
|
const DEPLOYMENTS = path.join(CIDE_DIR, 'deployments.json');
|
|
50
|
+
const SERVER_ROOT = config.serverRoot || path.dirname(CIDE_DIR);
|
|
40
51
|
|
|
41
52
|
const upload = multer({ dest: path.join(CIDE_DIR, 'tmp') });
|
|
42
53
|
|
|
@@ -56,6 +67,119 @@ function appendLog(message) {
|
|
|
56
67
|
try { fs.appendFileSync(logFile, line); } catch { /* logs dir may not exist yet */ }
|
|
57
68
|
}
|
|
58
69
|
|
|
70
|
+
// ── App process management ───────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function getAppPidFile(appCode) {
|
|
73
|
+
return path.join(CIDE_DIR, 'logs', `${appCode}.pid`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getAppLogFile(appCode) {
|
|
77
|
+
return path.join(LOGS, `${appCode}.log`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readAppPid(appCode) {
|
|
81
|
+
try {
|
|
82
|
+
const pid = parseInt(fs.readFileSync(getAppPidFile(appCode), 'utf8').trim(), 10);
|
|
83
|
+
return isNaN(pid) ? null : pid;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isProcessRunning(pid) {
|
|
90
|
+
try {
|
|
91
|
+
process.kill(pid, 0);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stopApp(appCode) {
|
|
99
|
+
const pid = readAppPid(appCode);
|
|
100
|
+
if (!pid) return { stopped: false, reason: 'no pid file' };
|
|
101
|
+
|
|
102
|
+
if (!isProcessRunning(pid)) {
|
|
103
|
+
try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
|
|
104
|
+
return { stopped: false, reason: 'not running (stale pid cleaned)' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
process.kill(pid, 'SIGTERM');
|
|
109
|
+
// Give it a moment to die, then force kill if needed
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
if (isProcessRunning(pid)) {
|
|
112
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
113
|
+
}
|
|
114
|
+
}, 3000);
|
|
115
|
+
try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
|
|
116
|
+
appendLog(`STOP app=${appCode} pid=${pid}`);
|
|
117
|
+
return { stopped: true, pid };
|
|
118
|
+
} catch (e) {
|
|
119
|
+
return { stopped: false, reason: e.message };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function startApp(appCode) {
|
|
124
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
125
|
+
const entryFile = path.join(appDir, 'server.js');
|
|
126
|
+
|
|
127
|
+
if (!fs.existsSync(entryFile)) {
|
|
128
|
+
return { started: false, reason: `Entry file not found: ${entryFile}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Stop existing process first
|
|
132
|
+
stopApp(appCode);
|
|
133
|
+
|
|
134
|
+
const logFile = getAppLogFile(appCode);
|
|
135
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
136
|
+
const out = fs.openSync(logFile, 'a');
|
|
137
|
+
const err = fs.openSync(logFile, 'a');
|
|
138
|
+
|
|
139
|
+
const child = spawn('node', [entryFile], {
|
|
140
|
+
cwd: appDir,
|
|
141
|
+
detached: true,
|
|
142
|
+
stdio: ['ignore', out, err],
|
|
143
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
child.unref();
|
|
147
|
+
|
|
148
|
+
// Save PID
|
|
149
|
+
fs.writeFileSync(getAppPidFile(appCode), String(child.pid));
|
|
150
|
+
appendLog(`START app=${appCode} pid=${child.pid} entry=${entryFile}`);
|
|
151
|
+
|
|
152
|
+
return { started: true, pid: child.pid, logFile };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function installAndStart(appCode) {
|
|
156
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
157
|
+
const steps = [];
|
|
158
|
+
|
|
159
|
+
// npm install if package.json exists
|
|
160
|
+
const pkgPath = path.join(appDir, 'package.json');
|
|
161
|
+
if (fs.existsSync(pkgPath)) {
|
|
162
|
+
try {
|
|
163
|
+
execSync('npm install --production', { cwd: appDir, stdio: 'pipe', shell: true, timeout: 120000 });
|
|
164
|
+
steps.push('npm install: ok');
|
|
165
|
+
appendLog(`INSTALL app=${appCode} npm install ok`);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
steps.push(`npm install: failed (${e.message})`);
|
|
168
|
+
appendLog(`INSTALL app=${appCode} npm install FAILED: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Start the app
|
|
173
|
+
const result = startApp(appCode);
|
|
174
|
+
if (result.started) {
|
|
175
|
+
steps.push(`started: pid ${result.pid}`);
|
|
176
|
+
} else {
|
|
177
|
+
steps.push(`start failed: ${result.reason}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { steps, ...result };
|
|
181
|
+
}
|
|
182
|
+
|
|
59
183
|
// ── Auth middleware ───────────────────────────────────────────────────────────
|
|
60
184
|
function authGuard(req, res, next) {
|
|
61
185
|
if (!TOKEN) {
|
|
@@ -69,9 +193,6 @@ function authGuard(req, res, next) {
|
|
|
69
193
|
next();
|
|
70
194
|
}
|
|
71
195
|
|
|
72
|
-
// ── Server root (parent of .cide/) ───────────────────────────────────────────
|
|
73
|
-
const SERVER_ROOT = config.serverRoot || path.dirname(CIDE_DIR);
|
|
74
|
-
|
|
75
196
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
76
197
|
// ── Upload ────────────────────────────────────────────────────────────────────
|
|
77
198
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -107,16 +228,19 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
|
|
|
107
228
|
zip.extractAllTo(appDir, true);
|
|
108
229
|
fs.unlinkSync(req.file.path);
|
|
109
230
|
|
|
231
|
+
// ── Install dependencies & start the app ─────────────────────────────
|
|
232
|
+
const appResult = installAndStart(appCode);
|
|
233
|
+
|
|
110
234
|
// ── Track state ──────────────────────────────────────────────────────
|
|
111
235
|
const deployments = loadDeployments();
|
|
112
236
|
const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
|
|
113
237
|
|
|
114
|
-
// ── Record ───────────────────────────────────────────────────────────
|
|
115
238
|
const record = {
|
|
116
239
|
version,
|
|
117
240
|
environment,
|
|
118
241
|
appDir,
|
|
119
242
|
releaseZip: releaseZipPath,
|
|
243
|
+
appPid: appResult.pid || null,
|
|
120
244
|
deployedAt: new Date().toISOString(),
|
|
121
245
|
action: 'upload',
|
|
122
246
|
message: deployMsg,
|
|
@@ -134,13 +258,14 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
|
|
|
134
258
|
|
|
135
259
|
res.json({
|
|
136
260
|
status: 'SUCCESS',
|
|
137
|
-
message: '
|
|
261
|
+
message: 'Uploaded, installed & started.',
|
|
138
262
|
appCode,
|
|
139
263
|
environment,
|
|
140
264
|
releaseVersion: version,
|
|
141
265
|
previousVersion,
|
|
142
266
|
appDir,
|
|
143
267
|
releaseZip: releaseZipPath,
|
|
268
|
+
appProcess: appResult,
|
|
144
269
|
completedAt: new Date().toISOString(),
|
|
145
270
|
});
|
|
146
271
|
} catch (err) {
|
|
@@ -196,11 +321,15 @@ app.post('/rollback', authGuard, (req, res) => {
|
|
|
196
321
|
const zip = new AdmZip(releaseZipPath);
|
|
197
322
|
zip.extractAllTo(appDir, true);
|
|
198
323
|
|
|
324
|
+
// Install dependencies & restart the app
|
|
325
|
+
const appResult = installAndStart(appCode);
|
|
326
|
+
|
|
199
327
|
const record = {
|
|
200
328
|
version,
|
|
201
329
|
environment: deployments.apps?.[appCode]?.current?.environment || 'production',
|
|
202
330
|
appDir,
|
|
203
331
|
releaseZip: releaseZipPath,
|
|
332
|
+
appPid: appResult.pid || null,
|
|
204
333
|
deployedAt: new Date().toISOString(),
|
|
205
334
|
action: 'rollback',
|
|
206
335
|
previousVersion,
|
|
@@ -217,10 +346,11 @@ app.post('/rollback', authGuard, (req, res) => {
|
|
|
217
346
|
|
|
218
347
|
res.json({
|
|
219
348
|
status: 'SUCCESS',
|
|
220
|
-
message: `Rolled back to
|
|
349
|
+
message: `Rolled back to ${version} & restarted.`,
|
|
221
350
|
appCode,
|
|
222
351
|
rolledBackTo: version,
|
|
223
352
|
previousVersion,
|
|
353
|
+
appProcess: appResult,
|
|
224
354
|
completedAt: new Date().toISOString(),
|
|
225
355
|
});
|
|
226
356
|
} catch (err) {
|
|
@@ -228,6 +358,49 @@ app.post('/rollback', authGuard, (req, res) => {
|
|
|
228
358
|
}
|
|
229
359
|
});
|
|
230
360
|
|
|
361
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
362
|
+
// ── App management ────────────────────────────────────────────────────────────
|
|
363
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
364
|
+
|
|
365
|
+
app.post('/app/stop', authGuard, (req, res) => {
|
|
366
|
+
const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
367
|
+
if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
|
|
368
|
+
|
|
369
|
+
const result = stopApp(appCode);
|
|
370
|
+
res.json({ status: 'SUCCESS', appCode, ...result });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
app.post('/app/start', authGuard, (req, res) => {
|
|
374
|
+
const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
375
|
+
if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
|
|
376
|
+
|
|
377
|
+
const result = startApp(appCode);
|
|
378
|
+
res.json({ status: result.started ? 'SUCCESS' : 'FAILED', appCode, ...result });
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
app.get('/app/status', authGuard, (req, res) => {
|
|
382
|
+
const appCode = (req.query.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
383
|
+
if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
|
|
384
|
+
|
|
385
|
+
const pid = readAppPid(appCode);
|
|
386
|
+
const running = pid ? isProcessRunning(pid) : false;
|
|
387
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
388
|
+
const logFile = getAppLogFile(appCode);
|
|
389
|
+
|
|
390
|
+
if (pid && !running) {
|
|
391
|
+
try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
res.json({
|
|
395
|
+
status: 'SUCCESS',
|
|
396
|
+
appCode,
|
|
397
|
+
running,
|
|
398
|
+
pid: running ? pid : null,
|
|
399
|
+
appDir,
|
|
400
|
+
logFile,
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
231
404
|
// ── Health check ──────────────────────────────────────────────────────────────
|
|
232
405
|
app.get('/health', (_req, res) => {
|
|
233
406
|
res.json({ status: 'ok', uptime: process.uptime() });
|
|
@@ -237,4 +410,5 @@ app.get('/health', (_req, res) => {
|
|
|
237
410
|
app.listen(PORT, () => {
|
|
238
411
|
console.log(`Cloud IDE upload-api listening on port ${PORT}`);
|
|
239
412
|
console.log(`.cide dir: ${CIDE_DIR}`);
|
|
413
|
+
console.log(`Server root: ${SERVER_ROOT}`);
|
|
240
414
|
});
|