soxogvv commited on
Commit
aaa6ec8
Β·
verified Β·
1 Parent(s): a2afe1d

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +49 -24
app.js CHANGED
@@ -30,13 +30,10 @@ function stripAnsi(str) {
30
  }
31
 
32
  // ── Pre-seed shellular config from env vars ───────────────────────────────────
33
- // If SHELLULAR_HOST_ID and SHELLULAR_KEY are set (as HF Secrets), we write
34
- // them into ~/.shellular/ so shellular skips the registration API call entirely.
35
- // This avoids rate-limit errors during container cold-starts.
36
  function seedShellularConfig() {
37
- const hostId = process.env.SHELLULAR_HOST_ID;
38
- const keyB64 = process.env.SHELLULAR_KEY; // base64-encoded 32-byte key
39
- const machineId = process.env.SHELLULAR_MACHINE_ID; // must match registration
40
 
41
  if (!hostId || !keyB64 || !machineId) return;
42
 
@@ -47,13 +44,11 @@ function seedShellularConfig() {
47
  try {
48
  fs.mkdirSync(shellularDir, { recursive: true });
49
 
50
- // Write config.json (skips registration on next shellular start)
51
  if (!fs.existsSync(configFile)) {
52
  fs.writeFileSync(configFile, JSON.stringify({ hostId, machineId }), 'utf-8');
53
  console.log(`[shellular] seeded config: hostId=${hostId}`);
54
  }
55
 
56
- // Write the E2E key file (32 bytes from base64)
57
  if (!fs.existsSync(keyFile)) {
58
  fs.writeFileSync(keyFile, Buffer.from(keyB64, 'base64'), { mode: 0o600 });
59
  console.log(`[shellular] seeded key: ${keyFile}`);
@@ -66,8 +61,6 @@ function seedShellularConfig() {
66
  seedShellularConfig();
67
 
68
  // ── Shellular machine-id helper ───────────────────────────────────────────────
69
- // node-machine-id hashes /etc/machine-id with SHA-256. We replicate that here
70
- // so the frontend can show the correct curl registration command.
71
  function getHashedMachineId() {
72
  try {
73
  const raw = fs.readFileSync('/etc/machine-id', 'utf-8').trim();
@@ -77,14 +70,11 @@ function getHashedMachineId() {
77
  }
78
  }
79
 
80
- // Returns the hashed machine-id (safe to expose β€” not a secret).
81
  app.get('/api/shellular/machine-id', (_req, res) => {
82
  const id = getHashedMachineId();
83
  id ? res.json({ machineId: id }) : res.status(500).json({ error: 'Cannot read machine-id' });
84
  });
85
 
86
- // Accepts a hostId obtained manually by the user, writes ~/.shellular/config.json,
87
- // and restarts shellular so it skips the registration API entirely.
88
  app.post('/api/shellular/seed-host', requireAuth, (req, res) => {
89
  const { hostId } = req.body || {};
90
  if (!hostId || typeof hostId !== 'string' || !hostId.trim()) {
@@ -102,7 +92,6 @@ app.post('/api/shellular/seed-host', requireAuth, (req, res) => {
102
  'utf-8'
103
  );
104
 
105
- // Restart shellular so it picks up the new config
106
  stopShellular();
107
  outputBuffer = '';
108
  broadcast({ type: 'clear' });
@@ -168,7 +157,6 @@ function startShellular() {
168
  stdio: ['ignore', 'pipe', 'pipe'],
169
  });
170
 
171
- // Accumulate stdout/stderr so we can detect the error type on exit
172
  let procOutput = '';
173
 
174
  const handleData = (chunk) => {
@@ -192,7 +180,6 @@ function startShellular() {
192
  shellularProc.on('exit', (code, signal) => {
193
  shellularProc = null;
194
 
195
- // Detect rate-limit / registration failure (exit code 1, no signal)
196
  const isRegError = code === 1 && !signal &&
197
  (procOutput.includes('invalid_union') || procOutput.includes('Too many requests') ||
198
  procOutput.includes('host registration'));
@@ -232,12 +219,52 @@ function stopShellular() {
232
  shellularProc = null;
233
  }
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  // ── SSE stream ─────────────────────────────────────────────────────────────────
236
  app.get('/api/stream', requireAuth, (req, res) => {
237
  res.setHeader('Content-Type', 'text/event-stream');
238
  res.setHeader('Cache-Control', 'no-cache');
239
  res.setHeader('Connection', 'keep-alive');
240
- res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering on HF
241
  res.flushHeaders();
242
 
243
  send(res, { type: 'status', status: shellularProc ? 'running' : 'stopped' });
@@ -275,8 +302,6 @@ app.get('/api/status', requireAuth, (_req, res) => {
275
  res.json({ running: !!shellularProc });
276
  });
277
 
278
- // Tells the frontend whether SHELLULAR_* secrets are already saved.
279
- // If not, the UI shows a first-time setup panel with values to copy into HF Secrets.
280
  app.get('/api/setup-status', requireAuth, (_req, res) => {
281
  const seeded = !!(
282
  process.env.SHELLULAR_HOST_ID &&
@@ -286,7 +311,6 @@ app.get('/api/setup-status', requireAuth, (_req, res) => {
286
  res.json({ seeded });
287
  });
288
 
289
- // Returns the registered hostId + base64 key so they can be saved as HF Secrets.
290
  app.get('/api/shellular/credentials', requireAuth, (_req, res) => {
291
  try {
292
  const shellularDir = path.join(os.homedir(), '.shellular');
@@ -300,9 +324,6 @@ app.get('/api/shellular/credentials', requireAuth, (_req, res) => {
300
  }
301
  });
302
 
303
- // Returns the QR data string ("hostId:keyBase64") for client-side QR rendering.
304
- // This is safe to expose post-auth β€” the key is shared with the scanning device
305
- // anyway (that is the point of the QR code).
306
  app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {
307
  try {
308
  const shellularDir = path.join(os.homedir(), '.shellular');
@@ -310,7 +331,6 @@ app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {
310
  const { hostId, machineId } = JSON.parse(configRaw);
311
  const keyFile = path.join(shellularDir, `shellular-${machineId}.e2ee`);
312
  const keyB64 = fs.readFileSync(keyFile).toString('base64');
313
- // Same format shellular itself encodes into the terminal QR
314
  res.json({ qrData: `${hostId}:${keyB64}` });
315
  } catch {
316
  res.status(404).json({ error: 'Config not seeded yet.' });
@@ -320,4 +340,9 @@ app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {
320
  // ── Start ──────────────────────────────────────────────────────────────────────
321
  app.listen(PORT, '0.0.0.0', () => {
322
  console.log(`Shellular Web UI β†’ http://0.0.0.0:${PORT}`);
 
323
  });
 
 
 
 
 
30
  }
31
 
32
  // ── Pre-seed shellular config from env vars ───────────────────────────────────
 
 
 
33
  function seedShellularConfig() {
34
+ const hostId = process.env.SHELLULAR_HOST_ID;
35
+ const keyB64 = process.env.SHELLULAR_KEY;
36
+ const machineId = process.env.SHELLULAR_MACHINE_ID;
37
 
38
  if (!hostId || !keyB64 || !machineId) return;
39
 
 
44
  try {
45
  fs.mkdirSync(shellularDir, { recursive: true });
46
 
 
47
  if (!fs.existsSync(configFile)) {
48
  fs.writeFileSync(configFile, JSON.stringify({ hostId, machineId }), 'utf-8');
49
  console.log(`[shellular] seeded config: hostId=${hostId}`);
50
  }
51
 
 
52
  if (!fs.existsSync(keyFile)) {
53
  fs.writeFileSync(keyFile, Buffer.from(keyB64, 'base64'), { mode: 0o600 });
54
  console.log(`[shellular] seeded key: ${keyFile}`);
 
61
  seedShellularConfig();
62
 
63
  // ── Shellular machine-id helper ───────────────────────────────────────────────
 
 
64
  function getHashedMachineId() {
65
  try {
66
  const raw = fs.readFileSync('/etc/machine-id', 'utf-8').trim();
 
70
  }
71
  }
72
 
 
73
  app.get('/api/shellular/machine-id', (_req, res) => {
74
  const id = getHashedMachineId();
75
  id ? res.json({ machineId: id }) : res.status(500).json({ error: 'Cannot read machine-id' });
76
  });
77
 
 
 
78
  app.post('/api/shellular/seed-host', requireAuth, (req, res) => {
79
  const { hostId } = req.body || {};
80
  if (!hostId || typeof hostId !== 'string' || !hostId.trim()) {
 
92
  'utf-8'
93
  );
94
 
 
95
  stopShellular();
96
  outputBuffer = '';
97
  broadcast({ type: 'clear' });
 
157
  stdio: ['ignore', 'pipe', 'pipe'],
158
  });
159
 
 
160
  let procOutput = '';
161
 
162
  const handleData = (chunk) => {
 
180
  shellularProc.on('exit', (code, signal) => {
181
  shellularProc = null;
182
 
 
183
  const isRegError = code === 1 && !signal &&
184
  (procOutput.includes('invalid_union') || procOutput.includes('Too many requests') ||
185
  procOutput.includes('host registration'));
 
219
  shellularProc = null;
220
  }
221
 
222
+ // ── Python sync.py subprocess ─────────────────────────────────────────────────
223
+ let syncProc = null;
224
+
225
+ function startSyncPy() {
226
+ if (syncProc) return;
227
+
228
+ console.log('[sync] Starting sync.py...');
229
+
230
+ syncProc = spawn('python3', [path.join(__dirname, 'sync.py')], {
231
+ env: { ...process.env },
232
+ stdio: ['ignore', 'pipe', 'pipe'],
233
+ });
234
+
235
+ syncProc.stdout.on('data', (chunk) => {
236
+ console.log('[sync]', chunk.toString().trim());
237
+ });
238
+
239
+ syncProc.stderr.on('data', (chunk) => {
240
+ console.error('[sync:err]', chunk.toString().trim());
241
+ });
242
+
243
+ syncProc.on('error', (err) => {
244
+ console.error('[sync] Spawn error:', err.message);
245
+ syncProc = null;
246
+ });
247
+
248
+ syncProc.on('exit', (code, signal) => {
249
+ console.warn(`[sync] sync.py exited β€” code=${code ?? '?'}, signal=${signal ?? 'none'}`);
250
+ syncProc = null;
251
+ // Auto-restart after 10s if it crashes
252
+ setTimeout(startSyncPy, 10_000);
253
+ });
254
+ }
255
+
256
+ function stopSyncPy() {
257
+ if (!syncProc) return;
258
+ syncProc.kill('SIGTERM');
259
+ syncProc = null;
260
+ }
261
+
262
  // ── SSE stream ─────────────────────────────────────────────────────────────────
263
  app.get('/api/stream', requireAuth, (req, res) => {
264
  res.setHeader('Content-Type', 'text/event-stream');
265
  res.setHeader('Cache-Control', 'no-cache');
266
  res.setHeader('Connection', 'keep-alive');
267
+ res.setHeader('X-Accel-Buffering', 'no');
268
  res.flushHeaders();
269
 
270
  send(res, { type: 'status', status: shellularProc ? 'running' : 'stopped' });
 
302
  res.json({ running: !!shellularProc });
303
  });
304
 
 
 
305
  app.get('/api/setup-status', requireAuth, (_req, res) => {
306
  const seeded = !!(
307
  process.env.SHELLULAR_HOST_ID &&
 
311
  res.json({ seeded });
312
  });
313
 
 
314
  app.get('/api/shellular/credentials', requireAuth, (_req, res) => {
315
  try {
316
  const shellularDir = path.join(os.homedir(), '.shellular');
 
324
  }
325
  });
326
 
 
 
 
327
  app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {
328
  try {
329
  const shellularDir = path.join(os.homedir(), '.shellular');
 
331
  const { hostId, machineId } = JSON.parse(configRaw);
332
  const keyFile = path.join(shellularDir, `shellular-${machineId}.e2ee`);
333
  const keyB64 = fs.readFileSync(keyFile).toString('base64');
 
334
  res.json({ qrData: `${hostId}:${keyB64}` });
335
  } catch {
336
  res.status(404).json({ error: 'Config not seeded yet.' });
 
340
  // ── Start ──────────────────────────────────────────────────────────────────────
341
  app.listen(PORT, '0.0.0.0', () => {
342
  console.log(`Shellular Web UI β†’ http://0.0.0.0:${PORT}`);
343
+ startSyncPy(); // 🐍 Launch sync.py on server start
344
  });
345
+
346
+ // ── Graceful shutdown ──────────────────────────────────────────────────────────
347
+ process.on('SIGTERM', () => { stopShellular(); stopSyncPy(); });
348
+ process.on('SIGINT', () => { stopShellular(); stopSyncPy(); });