querysub 0.490.0 → 0.492.0
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/.claude/settings.local.json +2 -1
- package/package.json +1 -1
- package/src/-e-certs/certAuthority.ts +13 -3
- package/src/2-proxy/PathValueProxyWatcher.ts +43 -21
- package/src/3-path-functions/PathFunctionRunner.ts +21 -1
- package/src/3-path-functions/functionCapture.ts +213 -0
- package/src/3-path-functions/functionCaptureFormat.ts +103 -0
- package/src/3-path-functions/functionReplay.ts +272 -0
- package/src/4-querysub/Querysub.ts +5 -2
- package/src/diagnostics/managementPages.tsx +11 -0
- package/src/diagnostics/misc-pages/FunctionCaptureAnalyzePage.tsx +220 -0
- package/src/diagnostics/misc-pages/FunctionCapturePage.tsx +192 -0
- package/tsconfig.json +14 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
module.allowclient = true;
|
|
2
|
+
|
|
3
|
+
import { qreact } from "../../4-dom/qreact";
|
|
4
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
5
|
+
import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
6
|
+
import { css } from "typesafecss";
|
|
7
|
+
import { Querysub, t } from "../../4-querysub/Querysub";
|
|
8
|
+
import { Button } from "../../library-components/Button";
|
|
9
|
+
import { ATag } from "../../library-components/ATag";
|
|
10
|
+
import { formatDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
11
|
+
import { green, red } from "socket-function/src/formatting/logColors";
|
|
12
|
+
import { assertIsManagementUser, managementPageURL } from "../managementPages";
|
|
13
|
+
import { getControllerNodeIdList } from "../../-g-core-values/NodeCapabilities";
|
|
14
|
+
import { errorToUndefined } from "../../errors";
|
|
15
|
+
import { CaptureStatus, FunctionCaptureController } from "../../3-path-functions/functionCapture";
|
|
16
|
+
|
|
17
|
+
interface RunnerStatus {
|
|
18
|
+
nodeId: string;
|
|
19
|
+
entryPoint: string;
|
|
20
|
+
status?: CaptureStatus;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const REFRESH_INTERVAL = 1000;
|
|
24
|
+
|
|
25
|
+
export class FunctionCapturePage extends qreact.Component {
|
|
26
|
+
state = t.state({
|
|
27
|
+
statuses: t.atomic<RunnerStatus[]>(),
|
|
28
|
+
busy: t.lookup(t.boolean),
|
|
29
|
+
error: t.string(""),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
private intervalId: ReturnType<typeof setInterval> | undefined;
|
|
33
|
+
|
|
34
|
+
componentDidMount() {
|
|
35
|
+
void this.refresh();
|
|
36
|
+
this.intervalId = setInterval(() => void this.refresh(), REFRESH_INTERVAL);
|
|
37
|
+
}
|
|
38
|
+
componentWillUnmount() {
|
|
39
|
+
if (this.intervalId) clearInterval(this.intervalId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async refresh() {
|
|
43
|
+
try {
|
|
44
|
+
let statuses = await FunctionCaptureProxyController.nodes[getBrowserUrlNode()].getStatuses();
|
|
45
|
+
Querysub.localCommit(() => {
|
|
46
|
+
this.state.statuses = statuses;
|
|
47
|
+
this.state.error = "";
|
|
48
|
+
});
|
|
49
|
+
} catch (e: any) {
|
|
50
|
+
Querysub.localCommit(() => {
|
|
51
|
+
this.state.error = String(e?.stack || e);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render() {
|
|
57
|
+
let statuses = this.state.statuses;
|
|
58
|
+
return <div className={css.pad2(10).vbox(14).fillWidth}>
|
|
59
|
+
<h1>Function Capture</h1>
|
|
60
|
+
<div>Turn on capturing to record fully-resolved function runs (reads, writes, timings) on a runner. Stop to download a capture file, then analyze it in the <ATag values={[{ param: managementPageURL, value: "FunctionCaptureAnalyzePage" }]}>Capture Analyzer</ATag>.</div>
|
|
61
|
+
{this.state.error && <div className={css.colorhsl(0, 70, 60)}>{this.state.error}</div>}
|
|
62
|
+
{!statuses && <div>Loading runners…</div>}
|
|
63
|
+
{statuses && statuses.length === 0 && <div>No function runners found.</div>}
|
|
64
|
+
{statuses && statuses.length > 0 &&
|
|
65
|
+
<div className={css.vbox(1).fillWidth}>
|
|
66
|
+
<div className={css.hbox(20).fillWidth.borderBottom("1px solid hsla(0,0%,0%,0.3)").pad2(6, 4).fontWeight("bold")}>
|
|
67
|
+
<div className={css.width(280)}>Runner</div>
|
|
68
|
+
<div className={css.width(90)}>Capturing</div>
|
|
69
|
+
<div className={css.width(90)}>Calls</div>
|
|
70
|
+
<div className={css.width(120)}>Size</div>
|
|
71
|
+
<div className={css.width(140)}>Started</div>
|
|
72
|
+
<div>Actions</div>
|
|
73
|
+
</div>
|
|
74
|
+
{statuses.map(runner => this.renderRunner(runner))}
|
|
75
|
+
</div>
|
|
76
|
+
}
|
|
77
|
+
</div>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private renderRunner(runner: RunnerStatus) {
|
|
81
|
+
let status = runner.status;
|
|
82
|
+
let busy = runner.nodeId in this.state.busy;
|
|
83
|
+
return <div className={css.hbox(20).fillWidth.borderBottom("1px solid hsla(0,0%,0%,0.15)").pad2(6, 4)}>
|
|
84
|
+
<div className={css.width(280).vbox(2)}>
|
|
85
|
+
<div>{runner.entryPoint}</div>
|
|
86
|
+
<div className={css.fontSize(11).colorhsl(0, 0, 45).ellipsis.maxWidth(280)}>{runner.nodeId}</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div className={css.width(90)}>{!status ? "?" : status.capturing ? "● live" : "off"}</div>
|
|
89
|
+
<div className={css.width(90)}>{status ? formatNumber(status.callCount) : "-"}{status && status.pendingCount > 0 && ` (+${status.pendingCount})`}</div>
|
|
90
|
+
<div className={css.width(120)}>{status ? `${formatNumber(status.byteCount)} B` : "-"}</div>
|
|
91
|
+
<div className={css.width(140)}>{status && status.startedAt ? formatTime(Date.now() - status.startedAt) + " ago" : "-"}</div>
|
|
92
|
+
<div className={css.hbox(8)}>
|
|
93
|
+
{status && !status.capturing &&
|
|
94
|
+
<Button disabled={busy} onClick={() => this.runAction(runner, "start")}>Start</Button>
|
|
95
|
+
}
|
|
96
|
+
{status && status.capturing &&
|
|
97
|
+
<Button disabled={busy} onClick={() => this.runAction(runner, "stop")}>Stop & Download</Button>
|
|
98
|
+
}
|
|
99
|
+
{status && !status.capturing && (status.callCount > 0) &&
|
|
100
|
+
<Button disabled={busy} onClick={() => this.downloadCapture(runner)}>Download</Button>
|
|
101
|
+
}
|
|
102
|
+
{status && !status.capturing && (status.callCount > 0) &&
|
|
103
|
+
<Button disabled={busy} onClick={() => this.runAction(runner, "clear")}>Clear</Button>
|
|
104
|
+
}
|
|
105
|
+
</div>
|
|
106
|
+
</div>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async runAction(runner: RunnerStatus, action: "start" | "stop" | "clear") {
|
|
110
|
+
if (Querysub.fastRead(() => runner.nodeId in this.state.busy)) return;
|
|
111
|
+
Querysub.localCommit(() => { this.state.busy[runner.nodeId] = true; });
|
|
112
|
+
try {
|
|
113
|
+
let proxy = FunctionCaptureProxyController.nodes[getBrowserUrlNode()];
|
|
114
|
+
if (action === "start") {
|
|
115
|
+
await proxy.startCapture({ nodeId: runner.nodeId });
|
|
116
|
+
} else if (action === "clear") {
|
|
117
|
+
await proxy.clearCapture({ nodeId: runner.nodeId });
|
|
118
|
+
} else if (action === "stop") {
|
|
119
|
+
let buffer = await proxy.stopCapture({ nodeId: runner.nodeId });
|
|
120
|
+
downloadBuffer(buffer, runner);
|
|
121
|
+
}
|
|
122
|
+
console.log(green(`${action} capture on ${runner.entryPoint}`));
|
|
123
|
+
} catch (e: any) {
|
|
124
|
+
console.error(red(`Failed to ${action} capture on ${runner.nodeId}: ${e.stack}`));
|
|
125
|
+
} finally {
|
|
126
|
+
Querysub.localCommit(() => { delete this.state.busy[runner.nodeId]; });
|
|
127
|
+
void this.refresh();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async downloadCapture(runner: RunnerStatus) {
|
|
132
|
+
if (Querysub.fastRead(() => runner.nodeId in this.state.busy)) return;
|
|
133
|
+
Querysub.localCommit(() => { this.state.busy[runner.nodeId] = true; });
|
|
134
|
+
try {
|
|
135
|
+
let buffer = await FunctionCaptureProxyController.nodes[getBrowserUrlNode()].stopCapture({ nodeId: runner.nodeId });
|
|
136
|
+
downloadBuffer(buffer, runner);
|
|
137
|
+
} catch (e: any) {
|
|
138
|
+
console.error(red(`Failed to download capture on ${runner.nodeId}: ${e.stack}`));
|
|
139
|
+
} finally {
|
|
140
|
+
Querysub.localCommit(() => { delete this.state.busy[runner.nodeId]; });
|
|
141
|
+
void this.refresh();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function downloadBuffer(buffer: Buffer, runner: RunnerStatus) {
|
|
147
|
+
let blob = new Blob([buffer]);
|
|
148
|
+
let url = URL.createObjectURL(blob);
|
|
149
|
+
let anchor = document.createElement("a");
|
|
150
|
+
anchor.href = url;
|
|
151
|
+
let label = runner.entryPoint.replace(/[^A-Za-z0-9_.-]+/g, "_");
|
|
152
|
+
anchor.download = `function-capture-${label}-${formatDateTime(Date.now())}.qsfncap`;
|
|
153
|
+
document.body.appendChild(anchor);
|
|
154
|
+
anchor.click();
|
|
155
|
+
document.body.removeChild(anchor);
|
|
156
|
+
URL.revokeObjectURL(url);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Management-side proxy: pages can't talk to runner processes directly, so this endpoint (running
|
|
160
|
+
// where management pages are hosted) fans out to the runner nodes over trusted server-to-server calls.
|
|
161
|
+
class FunctionCaptureProxyControllerBase {
|
|
162
|
+
public async getStatuses(): Promise<RunnerStatus[]> {
|
|
163
|
+
let runners = await getControllerNodeIdList(FunctionCaptureController);
|
|
164
|
+
return await Promise.all(runners.map(async runner => {
|
|
165
|
+
let status = await errorToUndefined(FunctionCaptureController.nodes[runner.nodeId].getStatus());
|
|
166
|
+
return { nodeId: runner.nodeId, entryPoint: runner.entryPoint, status };
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
public async startCapture(config: { nodeId: string }): Promise<CaptureStatus> {
|
|
170
|
+
return await FunctionCaptureController.nodes[config.nodeId].startCapture();
|
|
171
|
+
}
|
|
172
|
+
public async clearCapture(config: { nodeId: string }): Promise<CaptureStatus> {
|
|
173
|
+
return await FunctionCaptureController.nodes[config.nodeId].clearCapture();
|
|
174
|
+
}
|
|
175
|
+
public async stopCapture(config: { nodeId: string }): Promise<Buffer> {
|
|
176
|
+
return await FunctionCaptureController.nodes[config.nodeId].stopCapture();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const FunctionCaptureProxyController = SocketFunction.register(
|
|
181
|
+
"FunctionCaptureProxyController-9c4e2b71-5a83-4d19-8e0f-3b6a7c2d1f42",
|
|
182
|
+
new FunctionCaptureProxyControllerBase(),
|
|
183
|
+
() => ({
|
|
184
|
+
getStatuses: {},
|
|
185
|
+
startCapture: {},
|
|
186
|
+
clearCapture: {},
|
|
187
|
+
stopCapture: {},
|
|
188
|
+
}),
|
|
189
|
+
() => ({
|
|
190
|
+
hooks: [assertIsManagementUser],
|
|
191
|
+
}),
|
|
192
|
+
);
|
package/tsconfig.json
CHANGED
|
@@ -18,10 +18,23 @@
|
|
|
18
18
|
"types": [
|
|
19
19
|
"node",
|
|
20
20
|
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"socket-function": [
|
|
23
|
+
"./node_modules/socket-function"
|
|
24
|
+
],
|
|
25
|
+
"socket-function/*": [
|
|
26
|
+
"./node_modules/socket-function/*"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
21
29
|
"experimentalDecorators": true,
|
|
22
30
|
"emitDecoratorMetadata": false,
|
|
23
31
|
"skipLibCheck": true,
|
|
24
32
|
"inlineSourceMap": true,
|
|
25
33
|
"inlineSources": true,
|
|
26
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"exclude": [
|
|
36
|
+
"node_modules",
|
|
37
|
+
"database-storage",
|
|
38
|
+
"sliftutils"
|
|
39
|
+
]
|
|
27
40
|
}
|