peekinside 1.2.0 → 1.2.2
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/package.json +1 -1
- package/src/cli.js +3 -0
- package/src/hub/client.js +76 -1
- package/src/hub/server.js +31 -8
- package/src/transport/local.js +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peekinside",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Debug Node.js from the inside: errors with the requests, queries, logs and jobs behind them. A free local dashboard, and encrypted monitoring for production",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
package/src/cli.js
CHANGED
|
@@ -70,6 +70,9 @@ export class Cli {
|
|
|
70
70
|
* @throws {Error} When the hub did not start in time
|
|
71
71
|
*/
|
|
72
72
|
async start(openBrowser) {
|
|
73
|
+
// A hub left running by an older PeekInside is replaced by this version's (its data stays).
|
|
74
|
+
const replaced = await this.hub.replaceIfOlder();
|
|
75
|
+
if (replaced) console.log(`Restarted PeekInside Local: it was version ${replaced}, now ${VERSION}.`);
|
|
73
76
|
let running = await this.hub.status();
|
|
74
77
|
if (!running) {
|
|
75
78
|
this.hub.spawn();
|
package/src/hub/client.js
CHANGED
|
@@ -9,7 +9,7 @@ import fs from 'fs';
|
|
|
9
9
|
import http from 'http';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import { HUB_APP_ID, VERSION, settings } from '../config.js';
|
|
12
|
+
import { HUB_API_VERSION, HUB_APP_ID, VERSION, settings } from '../config.js';
|
|
13
13
|
|
|
14
14
|
const HUB_ENTRY = fileURLToPath(new URL('./entry.js', import.meta.url));
|
|
15
15
|
|
|
@@ -153,6 +153,50 @@ export class HubClient {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Replaces a running hub from an older PeekInside with this version's. Hubs outlive the apps
|
|
158
|
+
* that start them, so after an upgrade the old one would keep serving, its dashboard page
|
|
159
|
+
* included. Only a hub with the same API and data folder is replaced, so its data stays, and
|
|
160
|
+
* only the process hub.json names, so nothing else listening on the port is ever stopped.
|
|
161
|
+
*
|
|
162
|
+
* @returns {Promise<string|null>} The version that was replaced, or null when none was
|
|
163
|
+
*/
|
|
164
|
+
async replaceIfOlder() {
|
|
165
|
+
const running = await this.status().catch(() => null);
|
|
166
|
+
const dataDir = settings.dataDir();
|
|
167
|
+
if (!HubClient.shouldReplace(running, { version: VERSION, dataDir, infoPid: readInfoPid(dataDir) })) return null;
|
|
168
|
+
try {
|
|
169
|
+
process.kill(running.pid, 'SIGTERM');
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
175
|
+
if (!(await this.status().catch(() => null))) {
|
|
176
|
+
this.spawn();
|
|
177
|
+
await this.waitUntilReady(8000).catch(() => null);
|
|
178
|
+
return running.version;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Whether a running hub is an older version than this one that may be replaced: same API, same
|
|
186
|
+
* data folder, and the process its hub.json names.
|
|
187
|
+
*
|
|
188
|
+
* @param {Object|null} running - Its GET /api/status
|
|
189
|
+
* @param {{ version: string, dataDir: string, infoPid: number|null }} self - This version, its
|
|
190
|
+
* data folder, and the pid in that folder's hub.json
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
static shouldReplace(running, { version, dataDir, infoPid }) {
|
|
194
|
+
if (!running || running.apiVersion !== HUB_API_VERSION) return false;
|
|
195
|
+
if (!Number.isInteger(running.pid) || running.pid !== infoPid || running.pid === process.pid) return false;
|
|
196
|
+
if (typeof running.dataDir !== 'string' || path.resolve(running.dataDir) !== path.resolve(dataDir)) return false;
|
|
197
|
+
return compareVersions(running.version, version) < 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
156
200
|
/**
|
|
157
201
|
* Waits until the hub answers.
|
|
158
202
|
*
|
|
@@ -182,3 +226,34 @@ export class HubClient {
|
|
|
182
226
|
if (this.agent) this.agent.destroy();
|
|
183
227
|
}
|
|
184
228
|
}
|
|
229
|
+
|
|
230
|
+
// The pid in the data folder's hub.json (written by the hub that runs), or null.
|
|
231
|
+
function readInfoPid(dataDir) {
|
|
232
|
+
try {
|
|
233
|
+
const info = JSON.parse(fs.readFileSync(path.join(dataDir, 'hub.json'), 'utf8'));
|
|
234
|
+
return Number.isInteger(info.pid) ? info.pid : null;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Compares two versions by major, minor and patch ("1.10.0" is after "1.9.3"). Anything after the
|
|
242
|
+
* patch number (a prerelease) is ignored, and versions that can't be read compare as equal, so
|
|
243
|
+
* nothing is replaced on a guess.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} a
|
|
246
|
+
* @param {string} b
|
|
247
|
+
* @returns {number} Negative when a is older, positive when newer, 0 otherwise
|
|
248
|
+
*/
|
|
249
|
+
export function compareVersions(a, b) {
|
|
250
|
+
const parse = (value) => /^(\d+)\.(\d+)\.(\d+)/.exec(String(value || ''));
|
|
251
|
+
const left = parse(a);
|
|
252
|
+
const right = parse(b);
|
|
253
|
+
if (!left || !right) return 0;
|
|
254
|
+
for (let index = 1; index <= 3; index++) {
|
|
255
|
+
const difference = Number(left[index]) - Number(right[index]);
|
|
256
|
+
if (difference) return difference;
|
|
257
|
+
}
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
package/src/hub/server.js
CHANGED
|
@@ -57,6 +57,11 @@ const BASE_HEADERS = {
|
|
|
57
57
|
'Cross-Origin-Resource-Policy': 'same-origin'
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// If the interface doesn't arrive (blocked, offline, or still missing after 20 seconds), says so
|
|
61
|
+
// instead of "Loading" forever. The app replaces the loading message when it starts, so the timer
|
|
62
|
+
// finds nothing to replace then.
|
|
63
|
+
const BOOT_WATCH = '(function(){var shown=false;function show(){if(shown)return;var loading=document.querySelector(".boot-loading"),failed=document.querySelector(".boot-failed");if(!loading||!failed)return;shown=true;loading.hidden=true;failed.hidden=false}function broken(){if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",show);else show()}document.addEventListener("securitypolicyviolation",function(e){if(/^(script|style)-src/.test(e.effectiveDirective||e.violatedDirective||""))broken()});window.addEventListener("error",function(e){var t=e.target;if(t&&t.tagName==="SCRIPT"&&/\\/app\\.js$/.test(t.src||""))broken()},true);setTimeout(function(){if(document.querySelector(".boot-loading"))broken()},20000)})()';
|
|
64
|
+
|
|
60
65
|
// Applies the saved theme before first paint (same as the PeekInside site).
|
|
61
66
|
const THEME_SCRIPT = '(function(){try{var t=localStorage.getItem("peekinside:theme");if(t!=="light"&&t!=="dark"){t=matchMedia("(prefers-color-scheme: light)").matches?"light":"dark"}document.documentElement.setAttribute("data-theme",t)}catch(e){document.documentElement.setAttribute("data-theme","dark")}})()';
|
|
62
67
|
|
|
@@ -408,19 +413,20 @@ export class HubServer {
|
|
|
408
413
|
}
|
|
409
414
|
|
|
410
415
|
// The dashboard page: a shell that loads the interface from the PeekInside site. The Content
|
|
411
|
-
// Security Policy allows scripts only from that site (and the inline
|
|
412
|
-
//
|
|
416
|
+
// Security Policy allows scripts only from that site (and the inline scripts, by nonce), and
|
|
417
|
+
// lets the page fetch data only from this server.
|
|
413
418
|
_shell(req, res) {
|
|
414
419
|
const assets = this._assets;
|
|
415
420
|
const siteUrl = this.siteUrl;
|
|
421
|
+
const host = new URL(siteUrl).host;
|
|
416
422
|
const nonce = randomBytes(16).toString('base64');
|
|
417
|
-
const
|
|
423
|
+
const assetOrigins = siteOrigins(assets);
|
|
418
424
|
const csp = [
|
|
419
425
|
"default-src 'none'",
|
|
420
|
-
`script-src 'nonce-${nonce}' ${
|
|
421
|
-
`style-src ${
|
|
426
|
+
`script-src 'nonce-${nonce}' ${assetOrigins}`,
|
|
427
|
+
`style-src ${assetOrigins} 'unsafe-inline'`,
|
|
422
428
|
"img-src 'self' data:",
|
|
423
|
-
`font-src ${
|
|
429
|
+
`font-src ${assetOrigins}`,
|
|
424
430
|
"connect-src 'self'",
|
|
425
431
|
"base-uri 'none'",
|
|
426
432
|
"form-action 'none'",
|
|
@@ -436,12 +442,13 @@ export class HubServer {
|
|
|
436
442
|
<title>PeekInside Local</title>
|
|
437
443
|
<link rel="icon" href="/brand/peekinside-span-core-mark-light.svg" type="image/svg+xml" media="(prefers-color-scheme: light)">
|
|
438
444
|
<link rel="icon" href="/brand/peekinside-span-core-mark-dark.svg" type="image/svg+xml" media="(prefers-color-scheme: dark)">
|
|
445
|
+
<script nonce="${nonce}">${BOOT_WATCH}</script>
|
|
439
446
|
<link rel="stylesheet" href="${escapeHtml(assets)}/app.css">
|
|
440
447
|
<script nonce="${nonce}">${THEME_SCRIPT}</script>
|
|
441
|
-
<style>.boot{font:13px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;max-width:440px;margin:18vh auto 0;padding:0 16px;opacity:.75}.boot a{color:inherit}</style>
|
|
448
|
+
<style>.boot{font:13px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;max-width:440px;margin:18vh auto 0;padding:0 16px;opacity:.75}.boot a{color:inherit}.boot[hidden]{display:none}.boot-failed{opacity:.9}</style>
|
|
442
449
|
</head>
|
|
443
450
|
<body>
|
|
444
|
-
<div id="root"><div class="boot"><p><strong>Loading PeekInside…</strong></p><p>The dashboard interface loads from <a href="${escapeHtml(siteUrl)}">${escapeHtml(
|
|
451
|
+
<div id="root"><div class="boot boot-loading"><p><strong>Loading PeekInside…</strong></p><p>The dashboard interface loads from <a href="${escapeHtml(siteUrl)}">${escapeHtml(host)}</a>. Your requests stay on this machine.</p></div><div class="boot boot-failed" hidden><p><strong>The dashboard didn’t load</strong></p><p>Its interface comes from <a href="${escapeHtml(siteUrl)}">${escapeHtml(host)}</a>, and this browser couldn’t load it. Check that this computer can reach ${escapeHtml(host)}: a firewall, VPN, proxy or ad blocker can block it. Then <a href="">reload this page</a>.</p><p>Your app keeps recording meanwhile, so nothing is lost.</p></div></div>
|
|
445
452
|
<script src="${escapeHtml(assets)}/app.js" defer></script>
|
|
446
453
|
</body>
|
|
447
454
|
</html>
|
|
@@ -454,6 +461,22 @@ export class HubServer {
|
|
|
454
461
|
}
|
|
455
462
|
}
|
|
456
463
|
|
|
464
|
+
// The site's origin and its www twin: hosting often redirects one to the other (Vercel sends the
|
|
465
|
+
// bare domain to www by default), and a Content Security Policy also checks where a redirect
|
|
466
|
+
// lands. Local addresses (localhost, IPs) have no twin.
|
|
467
|
+
function siteOrigins(siteUrl) {
|
|
468
|
+
const url = new URL(siteUrl);
|
|
469
|
+
const host = url.hostname;
|
|
470
|
+
const origins = [url.origin];
|
|
471
|
+
const named = host.includes('.') && !host.startsWith('[') && !/^[\d.]+$/.test(host) && host !== 'localhost' && !host.endsWith('.localhost');
|
|
472
|
+
if (named) {
|
|
473
|
+
const twin = new URL(url.origin);
|
|
474
|
+
twin.hostname = host.startsWith('www.') ? host.slice(4) : `www.${host}`;
|
|
475
|
+
origins.push(twin.origin);
|
|
476
|
+
}
|
|
477
|
+
return origins.join(' ');
|
|
478
|
+
}
|
|
479
|
+
|
|
457
480
|
function isRead(req) {
|
|
458
481
|
return req.method === 'GET' || req.method === 'HEAD';
|
|
459
482
|
}
|
package/src/transport/local.js
CHANGED
|
@@ -79,6 +79,7 @@ export class LocalTransport extends BatchTransport {
|
|
|
79
79
|
this.blocked = new BlockedRequests();
|
|
80
80
|
this.blockedWarnings = new Set();
|
|
81
81
|
this.outdatedHubNoted = false;
|
|
82
|
+
this.hubVersionChecked = false;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/**
|
|
@@ -198,6 +199,12 @@ export class LocalTransport extends BatchTransport {
|
|
|
198
199
|
// Sends the batch with the blocked-request counts gathered since the last batch. Records are
|
|
199
200
|
// dropped only when the hub refuses them; when it isn't reachable, both are kept for later.
|
|
200
201
|
async _deliver(batch) {
|
|
202
|
+
// Once per process: a hub left running by an older PeekInside is replaced by this version's.
|
|
203
|
+
if (!this.hubVersionChecked) {
|
|
204
|
+
this.hubVersionChecked = true;
|
|
205
|
+
const replaced = await this.hub.replaceIfOlder().catch(() => null);
|
|
206
|
+
if (replaced) console.log(`${LOG_PREFIX} Restarted the local dashboard: it was PeekInside ${replaced}, and this app uses ${VERSION}.`);
|
|
207
|
+
}
|
|
201
208
|
const blocked = this.blocked.take();
|
|
202
209
|
const samples = this.takeSamples();
|
|
203
210
|
const { requests, exceptions, jobs, logs } = BatchTransport.split(batch);
|