webtun 1.5.2 → 1.5.3

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/README.md CHANGED
@@ -68,7 +68,7 @@ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh &&
68
68
  - **Multi-tab** — drag to reorder, side-by-side sessions
69
69
  - **Bracketed paste** — Ctrl+V works in TUI apps (vim, nano, htop)
70
70
  - **Command history** — keystroke-based capture, strips ANSI/control sequences
71
- - **tmux sessions** — persistent sessions survive page reload
71
+ - **Session persistence** — tmux or in-memory PTY; tabs survive page reload (all platforms)
72
72
  - **Keyboard Shortcuts** — reference dialog in the overflow menu (Ctrl+P/T/W/B/F, etc.)
73
73
 
74
74
  ### File Explorer
@@ -175,6 +175,10 @@ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh &&
175
175
 
176
176
  ## Changelog
177
177
 
178
+ ### v1.5.3
179
+ - Terminal session persistence without tmux (in-memory PTY on Windows + Linux)
180
+ - Session IDs always generated; tabs now survive page reload on all platforms
181
+
178
182
  ### v1.5.2
179
183
  - UI/UX audit fixes: theme-aware xterm selection color, `--fg3` across all 6 themes
180
184
  - Replaced native `confirm()` with a theme-aware, focus-trapped dialog
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webtun",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Self-hosted web terminal with Cloudflare Tunnel, file explorer, and PWA support",
5
5
  "author": {
6
6
  "name": "Gaurang Patel",
package/public/index.html CHANGED
@@ -2068,7 +2068,7 @@ function hideTermLoading(tab) {
2068
2068
 
2069
2069
  function newTab(title, sessionId, dir) {
2070
2070
  const id = ++tabCounter;
2071
- const sid = sessionId || (hasTmux ? uuid() : null);
2071
+ const sid = sessionId || uuid();
2072
2072
  const tab = { id, sessionId: sid, title: title || `Term ${nextTermNumber()}`, term: null, fitAddon: null, searchAddon: null, ws: null, el: null, wrapper: null, closed: false, reconnectDelay: 1000, dataDisposable: null, resizeDisposable: null, resizeObserver: null, cwd: dir || currentPath };
2073
2073
  tabs.push(tab);
2074
2074
  createTabButton(tab);
@@ -2348,7 +2348,7 @@ async function closeTab(e, id) {
2348
2348
  tab.term?.dispose();
2349
2349
  tab.el?.remove();
2350
2350
  tab.wrapper?.remove();
2351
- // Kill the backing tmux session so it doesn't pile up
2351
+ // Kill the backing session (tmux or in-memory) so it doesn't pile up
2352
2352
  if (tab.sessionId) api(`/api/sessions/${tab.sessionId}`, { method: 'DELETE' });
2353
2353
  tabs = tabs.filter(t => t.id !== id);
2354
2354
  saveTabState();
@@ -5387,6 +5387,9 @@ function setupDragDrop() {
5387
5387
  const traversed = await traverseDirectoryEntry(entry);
5388
5388
  filesToUpload.push(...traversed);
5389
5389
  } catch(e) { console.warn(e); }
5390
+ } else {
5391
+ const file = item.getAsFile();
5392
+ if (file) filesToUpload.push({ file, path: file.name });
5390
5393
  }
5391
5394
  }
5392
5395
  }
package/server.js CHANGED
@@ -1272,9 +1272,12 @@ app.delete('/api/history/:index', checkPin, (req, res) => {
1272
1272
  res.json({ success: true, history: cmdHistory });
1273
1273
  });
1274
1274
 
1275
- // ── Session persistence via tmux ──────────────────────────────────────
1275
+ // ── Session persistence ──────────────────────────────────────────────
1276
1276
  const TMUX = (() => { try { return execSync('command -v tmux', { stdio: ['ignore','pipe','ignore'] }).toString().trim(); } catch { return null; } })();
1277
1277
 
1278
+ // In-memory PTY session store — enables persistence without tmux (Windows + Linux)
1279
+ const ptySessions = new Map(); // sessionId -> { proc, drainCheck, createdAt }
1280
+
1278
1281
  function isValidPID(pid) {
1279
1282
  return typeof pid === 'number' && Number.isInteger(pid) && pid > 0;
1280
1283
  }
@@ -1301,22 +1304,38 @@ function tmuxSessionExists(name) {
1301
1304
  }
1302
1305
 
1303
1306
  app.get('/api/sessions', checkPin, (req, res) => {
1304
- if (!TMUX) return res.json({ tmux: false, sessions: [] });
1305
- try {
1306
- const out = execFileSync(TMUX, ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' }).trim();
1307
- const sessions = out.split('\n')
1308
- .filter(s => s.startsWith('wt-'))
1309
- .map(s => ({ id: s.replace(/^wt-/, ''), name: s }));
1310
- res.json({ tmux: true, sessions });
1311
- } catch {
1312
- res.json({ tmux: true, sessions: [] });
1307
+ if (TMUX) {
1308
+ try {
1309
+ const out = execFileSync(TMUX, ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' }).trim();
1310
+ const sessions = out.split('\n')
1311
+ .filter(s => s.startsWith('wt-'))
1312
+ .map(s => ({ id: s.replace(/^wt-/, ''), name: s }));
1313
+ return res.json({ tmux: true, sessions });
1314
+ } catch {
1315
+ return res.json({ tmux: true, sessions: [] });
1316
+ }
1317
+ }
1318
+ // In-memory sessions (no tmux)
1319
+ const sessions = [];
1320
+ for (const [id] of ptySessions) {
1321
+ sessions.push({ id, name: 'wt-' + id });
1313
1322
  }
1323
+ res.json({ tmux: false, sessions });
1314
1324
  });
1315
1325
 
1316
1326
  app.delete('/api/sessions/:id', checkPin, (req, res) => {
1317
- if (!TMUX) return res.json({ success: false });
1318
- const name = 'wt-' + req.params.id.replace(/[^a-zA-Z0-9_-]/g, '');
1319
- try { execFileSync(TMUX, ['kill-session', '-t', name], { stdio: 'ignore' }); } catch {}
1327
+ const id = req.params.id.replace(/[^a-zA-Z0-9_-]/g, '');
1328
+ if (TMUX) {
1329
+ const name = 'wt-' + id;
1330
+ try { execFileSync(TMUX, ['kill-session', '-t', name], { stdio: 'ignore' }); } catch {}
1331
+ return res.json({ success: true });
1332
+ }
1333
+ // In-memory session
1334
+ const entry = ptySessions.get(id);
1335
+ if (entry) {
1336
+ try { entry.proc.kill(); } catch {}
1337
+ ptySessions.delete(id);
1338
+ }
1320
1339
  res.json({ success: true });
1321
1340
  });
1322
1341
 
@@ -1378,8 +1397,28 @@ wss.on('connection', (ws, req) => {
1378
1397
  };
1379
1398
 
1380
1399
  let proc;
1400
+ let reattached = false;
1381
1401
  try {
1382
- if (TMUX && sessionId) {
1402
+ if (sessionId && !TMUX) {
1403
+ // ── In-memory PTY persistence (no tmux needed) ──
1404
+ const existing = ptySessions.get(sessionId);
1405
+ if (existing && existing.proc && !existing.exited) {
1406
+ // Reattach: remove old listeners, reuse the running PTY
1407
+ proc = existing.proc;
1408
+ proc.removeAllListeners('data');
1409
+ proc.removeAllListeners('exit');
1410
+ proc.resize(cols, rows);
1411
+ reattached = true;
1412
+ } else {
1413
+ // New in-memory session
1414
+ if (existing) ptySessions.delete(sessionId);
1415
+ const shellArgs = os.platform() === 'win32' ? ['-NoLogo'] : ['-l'];
1416
+ proc = pty.spawn(SHELL, shellArgs, {
1417
+ name: 'xterm-256color', cols, rows, cwd,
1418
+ env: sessionEnv
1419
+ });
1420
+ }
1421
+ } else if (TMUX && sessionId) {
1383
1422
  const tmuxName = 'wt-' + sessionId;
1384
1423
  const exists = tmuxSessionExists(tmuxName);
1385
1424
 
@@ -1413,6 +1452,12 @@ wss.on('connection', (ws, req) => {
1413
1452
  const HIGH_WATER = 4 * 1024 * 1024; // 4MB — pause PTY above this
1414
1453
  const LOW_WATER = 1 * 1024 * 1024; // 1MB — resume PTY below this
1415
1454
 
1455
+ const drainCheck = setInterval(() => {
1456
+ if (paused && ws.bufferedAmount < LOW_WATER) {
1457
+ try { proc.resume(); paused = false; } catch (_) {}
1458
+ }
1459
+ }, 50);
1460
+
1416
1461
  proc.onData(data => {
1417
1462
  if (ws.readyState !== WebSocket.OPEN) return;
1418
1463
  send(0x00, data);
@@ -1422,19 +1467,29 @@ wss.on('connection', (ws, req) => {
1422
1467
  }
1423
1468
  });
1424
1469
 
1425
- // Drain check: resume PTY when buffer drops
1426
- const drainCheck = setInterval(() => {
1427
- if (paused && ws.bufferedAmount < LOW_WATER) {
1428
- try { proc.resume(); paused = false; } catch (_) {}
1429
- }
1430
- }, 50);
1470
+ const useInMemory = sessionId && !TMUX;
1471
+ const useTmux = TMUX && sessionId;
1431
1472
 
1432
1473
  proc.onExit(() => {
1433
1474
  clearInterval(drainCheck);
1434
- if (!TMUX || !sessionId) send(0x01, Buffer.from([0]));
1475
+ if (useInMemory) ptySessions.delete(sessionId);
1476
+ if (!useTmux) send(0x01, Buffer.from([0]));
1435
1477
  ws.close();
1436
1478
  });
1437
1479
 
1480
+ // If this is a new in-memory session, register it now (after onExit is wired)
1481
+ if (useInMemory && !rehattached) {
1482
+ ptySessions.set(sessionId, { proc, exited: false, createdAt: Date.now() });
1483
+ // Track exit so stale sessions are detected on reconnect
1484
+ proc.onExit(() => {
1485
+ const entry = ptySessions.get(sessionId);
1486
+ if (entry) entry.exited = true;
1487
+ });
1488
+ } else if (useInMemory && reattached) {
1489
+ const entry = ptySessions.get(sessionId);
1490
+ if (entry) { entry.proc = proc; entry.exited = false; }
1491
+ }
1492
+
1438
1493
  ws.isAlive = true;
1439
1494
  const pingInterval = setInterval(() => {
1440
1495
  if (!ws.isAlive) { clearInterval(pingInterval); ws.terminate(); return; }
@@ -1467,6 +1522,11 @@ wss.on('connection', (ws, req) => {
1467
1522
  const cleanup = () => {
1468
1523
  clearInterval(pingInterval);
1469
1524
  clearInterval(drainCheck);
1525
+ if (useInMemory && sessionId) {
1526
+ // Keep the PTY alive for reattachment — just detach listeners
1527
+ try { proc.removeAllListeners('data'); } catch {}
1528
+ return;
1529
+ }
1470
1530
  try { proc.kill(); } catch {}
1471
1531
  };
1472
1532
  ws.on('close', cleanup);
@@ -1928,6 +1988,11 @@ function cleanup() {
1928
1988
  }
1929
1989
  } catch {}
1930
1990
  }
1991
+ // Kill all in-memory PTY sessions
1992
+ for (const [id, entry] of ptySessions) {
1993
+ try { entry.proc.kill(); } catch {}
1994
+ }
1995
+ ptySessions.clear();
1931
1996
  }
1932
1997
 
1933
1998
  function startServer(opts = {}) {