termdock 1.4.155 → 1.4.156

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.
@@ -27,7 +27,7 @@ import { RuntimeMonitor } from './utils/runtimeMonitor.js';
27
27
  import { startOnboardingServer, stopOnboardingServer, getOnboardingServerUrl } from './onboardingServer.js';
28
28
  import { CertificateWatcher } from './certificateWatcher.js';
29
29
  import { startTermdockLogMaintenance, writeDiffTraceLog, writeErrorLog, writeJsonLog, writeTextLog, } from './utils/serverLogger.js';
30
- import { resolveRuntimeClientDist } from './utils/runtimeClient.js';
30
+ import { pinBundledRuntimeClientDist, resolveRuntimeClientDist } from './utils/runtimeClient.js';
31
31
  import { getTermdockVersion, TERMDOCK_CAPABILITIES, TERMDOCK_PROTOCOL_VERSION, } from './utils/version.js';
32
32
  import { PORT, DEFAULT_HOST } from './config.js';
33
33
  const CLIENT_STATE_COOKIE = 'termdock-client';
@@ -448,35 +448,28 @@ export function createApp(options = {}) {
448
448
  // 文件系统路由(继承 /api/terminal 上的 auth + CSRF 保护)
449
449
  app.use('/api/terminal/fs', filesystemRoutes);
450
450
  if (fs.existsSync(bundledClientIndexPath)) {
451
- let cachedClientPath = '';
452
- let cachedCompression = null;
453
- let cachedStatic = null;
454
- const activeClientHandlers = () => {
455
- const clientPath = resolveRuntimeClientDist(bundledClientDistPath);
456
- if (clientPath !== cachedClientPath || !cachedCompression || !cachedStatic) {
457
- cachedClientPath = clientPath;
458
- cachedCompression = createStaticCompressionMiddleware(clientPath);
459
- cachedStatic = express.static(clientPath, {
460
- setHeaders: (res, filePath, stat) => {
461
- void stat;
462
- const relativePath = `/${path.relative(clientPath, filePath).split(path.sep).join('/')}`;
463
- setStaticCacheHeaders({ url: relativePath, path: relativePath }, res);
464
- },
465
- });
466
- }
467
- return { clientPath, compression: cachedCompression, staticFiles: cachedStatic };
468
- };
451
+ const selectedClientPath = resolveRuntimeClientDist(bundledClientDistPath);
452
+ const clientPath = selectedClientPath === bundledClientDistPath
453
+ ? pinBundledRuntimeClientDist(bundledClientDistPath)
454
+ : selectedClientPath;
455
+ const compression = createStaticCompressionMiddleware(clientPath);
456
+ const staticFiles = express.static(clientPath, {
457
+ setHeaders: (res, filePath, stat) => {
458
+ void stat;
459
+ const relativePath = `/${path.relative(clientPath, filePath).split(path.sep).join('/')}`;
460
+ setStaticCacheHeaders({ url: relativePath, path: relativePath }, res);
461
+ },
462
+ });
469
463
  app.use((req, res, next) => {
470
- const handlers = activeClientHandlers();
471
- handlers.compression(req, res, (compressionError) => {
464
+ compression(req, res, (compressionError) => {
472
465
  if (compressionError)
473
466
  return next(compressionError);
474
- handlers.staticFiles(req, res, next);
467
+ staticFiles(req, res, next);
475
468
  });
476
469
  });
477
470
  app.get(/^(?!\/api(?:\/|$)|\/health$|\/onboarding(?:\/|$)|\/ca(?:\/|$)).*/, (req, res) => {
478
471
  setStaticCacheHeaders(req, res);
479
- res.sendFile(path.join(activeClientHandlers().clientPath, 'index.html'));
472
+ res.sendFile(path.join(clientPath, 'index.html'));
480
473
  });
481
474
  }
482
475
  return app;
@@ -1,3 +1,4 @@
1
+ import crypto from 'node:crypto';
1
2
  import fs from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -9,6 +10,78 @@ function readManifest(filePath) {
9
10
  return null;
10
11
  }
11
12
  }
13
+ function isCompleteSnapshot(clientDist, serverBundleHash) {
14
+ const marker = readManifest(path.join(clientDist, '..', '..', 'snapshot-manifest.json'));
15
+ return marker?.schemaVersion === 1
16
+ && marker.serverBundleHash === serverBundleHash
17
+ && marker.clientEntrypoint === 'dist/client/index.html'
18
+ && fs.existsSync(path.join(clientDist, 'index.html'));
19
+ }
20
+ /**
21
+ * Keep the browser bundle served by this process immutable for its lifetime.
22
+ *
23
+ * `npm install --global` replaces files underneath a still-running Termdock
24
+ * process. Without a snapshot, Express immediately starts serving the new
25
+ * index/assets while the in-memory server is still the old version. That can
26
+ * pair a newer terminal protocol client with an older WebSocket server until
27
+ * the user confirms the restart.
28
+ */
29
+ export function pinBundledRuntimeClientDist(defaultClientDist, homeDir = os.homedir()) {
30
+ try {
31
+ const packageRoot = path.resolve(defaultClientDist, '..', '..');
32
+ const manifest = readManifest(path.join(packageRoot, 'runtime-manifest.json'));
33
+ if (manifest?.schemaVersion !== 1
34
+ || typeof manifest.serverBundleHash !== 'string'
35
+ || manifest.clientEntrypoint !== 'dist/client/index.html'
36
+ || !fs.existsSync(path.join(defaultClientDist, 'index.html'))) {
37
+ return defaultClientDist;
38
+ }
39
+ const snapshotKey = crypto
40
+ .createHash('sha256')
41
+ .update(manifest.serverBundleHash)
42
+ .digest('hex')
43
+ .slice(0, 24);
44
+ const snapshotsRoot = path.join(homeDir, '.termdock', 'client-snapshots');
45
+ const snapshotRoot = path.join(snapshotsRoot, snapshotKey);
46
+ const snapshotClientDist = path.join(snapshotRoot, 'dist', 'client');
47
+ if (isCompleteSnapshot(snapshotClientDist, manifest.serverBundleHash)) {
48
+ return snapshotClientDist;
49
+ }
50
+ fs.mkdirSync(snapshotsRoot, { recursive: true, mode: 0o700 });
51
+ const temporaryRoot = fs.mkdtempSync(path.join(snapshotsRoot, `.${snapshotKey}-`));
52
+ const temporaryClientDist = path.join(temporaryRoot, 'dist', 'client');
53
+ try {
54
+ fs.cpSync(defaultClientDist, temporaryClientDist, {
55
+ recursive: true,
56
+ preserveTimestamps: true,
57
+ });
58
+ fs.writeFileSync(path.join(temporaryRoot, 'snapshot-manifest.json'), `${JSON.stringify({
59
+ schemaVersion: 1,
60
+ serverBundleHash: manifest.serverBundleHash,
61
+ clientEntrypoint: 'dist/client/index.html',
62
+ })}\n`, { mode: 0o600 });
63
+ try {
64
+ fs.renameSync(temporaryRoot, snapshotRoot);
65
+ }
66
+ catch (error) {
67
+ if (error.code !== 'EEXIST')
68
+ throw error;
69
+ }
70
+ }
71
+ finally {
72
+ if (fs.existsSync(temporaryRoot)) {
73
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
74
+ }
75
+ }
76
+ return isCompleteSnapshot(snapshotClientDist, manifest.serverBundleHash)
77
+ ? snapshotClientDist
78
+ : defaultClientDist;
79
+ }
80
+ catch {
81
+ // A read-only home or interrupted snapshot must never block the server.
82
+ return defaultClientDist;
83
+ }
84
+ }
12
85
  export function resolveRuntimeClientDist(defaultClientDist, homeDir = os.homedir()) {
13
86
  try {
14
87
  const defaultPackageRoot = path.resolve(defaultClientDist, '..', '..');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.155",
3
+ "version": "1.4.156",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "main": ".desktop-dist/main.js",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "termdock",
4
- "version": "1.4.155",
4
+ "version": "1.4.156",
5
5
  "runtimeProtocolVersion": 1,
6
6
  "minimumDesktopVersion": "1.4.46",
7
7
  "nodeMajor": 22,
8
8
  "dependencyHash": "sha256-HOcSzDZKWX251Njijj5fna89AzzVkOmrgAV2AuvVlJk=",
9
- "serverBundleHash": "sha256-0Ff/a3stFg47uLUati8tKSYtUhBxfHRcZBwpYMq9KCk=",
9
+ "serverBundleHash": "sha256-03guIBU3l8lNaGGjmY7l/fSgflPMZ++rwcORglMctvo=",
10
10
  "entrypoint": "dist/server/cli.js",
11
11
  "clientEntrypoint": "dist/client/index.html"
12
12
  }