@tsplc/debug 0.2.17 → 0.2.19
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/dist/format.js +36 -18
- package/dist/format.js.map +4 -4
- package/dist/index.js +768 -516
- package/dist/index.js.map +20 -17
- package/dist/manifest.js +6 -2
- package/dist/manifest.js.map +3 -3
- package/dist/paths.js +60 -28
- package/dist/paths.js.map +3 -3
- package/package.json +47 -46
- package/src/adapter.ts +43 -36
- package/src/api.ts +126 -123
- package/src/debugger.ts +584 -467
- package/src/errors.ts +3 -0
- package/src/hardware.ts +127 -96
- package/src/http.ts +147 -116
- package/src/index.ts +34 -4
- package/src/jobs.ts +102 -77
- package/src/login-value.ts +165 -0
- package/src/login.ts +233 -253
- package/src/manifest.ts +200 -194
- package/src/opcua.ts +55 -55
- package/src/paths.ts +230 -157
- package/src/requests.ts +86 -0
- package/src/telemetry.ts +104 -90
- package/src/timeline.ts +96 -61
- package/src/trace.ts +41 -31
- package/src/ui/bits.ts +45 -29
- package/src/ui/client.ts +33 -26
- package/src/ui/dom.ts +189 -108
- package/src/ui/format.ts +127 -72
- package/src/ui/hardware.ts +416 -343
- package/src/ui/header.ts +186 -164
- package/src/ui/index.ts +16 -9
- package/src/ui/jobs.ts +263 -214
- package/src/ui/main.ts +52 -48
- package/src/ui/overview.ts +152 -108
- package/src/ui/recent.ts +329 -276
- package/src/ui/requests.ts +69 -73
- package/src/ui/store.ts +236 -198
- package/src/ui/tree.ts +162 -130
- package/src/ui/variables.ts +557 -431
- package/src/watch.ts +22 -17
- package/src/ui/dev.ts +0 -579
- package/src/ui/fixture.ts +0 -548
package/src/paths.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isArrayIndexSegment, symbolPathSegments } from "@tsplc/opcua/contract";
|
|
1
2
|
import type { DebugManifest, DebugType, SourceRange } from "./manifest.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -7,179 +8,251 @@ import type { DebugManifest, DebugType, SourceRange } from "./manifest.ts";
|
|
|
7
8
|
|
|
8
9
|
/** A path that reads a target, or a constant the compiler folded. */
|
|
9
10
|
export type ResolvedPath =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
| {
|
|
12
|
+
kind: "target";
|
|
13
|
+
path: string;
|
|
14
|
+
target: string;
|
|
15
|
+
type: string;
|
|
16
|
+
readable: boolean;
|
|
17
|
+
generated?: true;
|
|
18
|
+
declaration?: SourceRange;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
kind: "constant";
|
|
22
|
+
path: string;
|
|
23
|
+
type: string;
|
|
24
|
+
value: unknown;
|
|
25
|
+
declaration?: SourceRange;
|
|
26
|
+
};
|
|
20
27
|
|
|
21
28
|
/** Roots or constants that share a prefix, such as `program` above `program.diverters`. */
|
|
22
29
|
interface GroupPath {
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
kind: "group";
|
|
31
|
+
path: string;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/** A child in the instance tree. */
|
|
28
35
|
export type PathNode = { name: string } & (ResolvedPath | GroupPath);
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
/** In a pattern, every index of an array. */
|
|
38
|
+
const anyIndex = "[*]";
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
/** Names and indexes as the controller's symbol paths split them, and `[*]` in patterns. */
|
|
41
|
+
export const pathSegments = (path: string) =>
|
|
42
|
+
path
|
|
43
|
+
.split(anyIndex)
|
|
44
|
+
.flatMap((part, index) => [
|
|
45
|
+
...(index > 0 ? [anyIndex] : []),
|
|
46
|
+
...symbolPathSegments(part),
|
|
47
|
+
]);
|
|
33
48
|
|
|
34
49
|
export const joinPath = (prefix: string, segment: string) =>
|
|
35
|
-
|
|
50
|
+
segment.startsWith("[") || !prefix
|
|
51
|
+
? `${prefix}${segment}`
|
|
52
|
+
: `${prefix}.${segment}`;
|
|
36
53
|
|
|
37
54
|
/** Scalar type ids are their IEC names, and may have no entry in `types`. */
|
|
38
55
|
export const typeOf = (manifest: DebugManifest, id: string): DebugType =>
|
|
39
|
-
|
|
56
|
+
manifest.types[id] ?? { kind: "scalar", iec: id };
|
|
40
57
|
|
|
41
58
|
export const createPaths = (manifest: DebugManifest) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
59
|
+
const typeOfId = (id: string) => typeOf(manifest, id);
|
|
60
|
+
// I/O bindings are readable symbols with TypeScript paths of their own.
|
|
61
|
+
const bases = new Map<string, Extract<ResolvedPath, { kind: "target" }>>([
|
|
62
|
+
...manifest.roots.map(
|
|
63
|
+
({ path, ...root }) =>
|
|
64
|
+
[path, { kind: "target" as const, path, ...root }] as const,
|
|
65
|
+
),
|
|
66
|
+
...manifest.io.map(
|
|
67
|
+
({ path, target, type }) =>
|
|
68
|
+
[
|
|
69
|
+
path,
|
|
70
|
+
{ kind: "target" as const, path, target, type, readable: true },
|
|
71
|
+
] as const,
|
|
72
|
+
),
|
|
73
|
+
]);
|
|
74
|
+
const constants = new Map(
|
|
75
|
+
manifest.constants.map(({ path, ...constant }) => [
|
|
76
|
+
path,
|
|
77
|
+
{ kind: "constant" as const, path, ...constant },
|
|
78
|
+
]),
|
|
79
|
+
);
|
|
80
|
+
/** Children of the prefixes of every root and constant path, which are groups unless they resolve. */
|
|
81
|
+
const groups = new Map<string, Set<string>>();
|
|
82
|
+
for (const path of [...bases.keys(), ...constants.keys()]) {
|
|
83
|
+
let prefix = "";
|
|
84
|
+
for (const segment of pathSegments(path)) {
|
|
85
|
+
let children = groups.get(prefix);
|
|
86
|
+
if (!children) {
|
|
87
|
+
groups.set(prefix, (children = new Set()));
|
|
88
|
+
}
|
|
89
|
+
children.add(segment);
|
|
90
|
+
prefix = joinPath(prefix, segment);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const resolve = (path: string): ResolvedPath | undefined => {
|
|
95
|
+
const constant = constants.get(path);
|
|
96
|
+
if (constant) {
|
|
97
|
+
return constant;
|
|
98
|
+
}
|
|
99
|
+
const segments = pathSegments(path);
|
|
100
|
+
for (let length = segments.length; length > 0; length--) {
|
|
101
|
+
const base = bases.get(segments.slice(0, length).reduce(joinPath, ""));
|
|
102
|
+
if (!base) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let resolved: ResolvedPath = base;
|
|
106
|
+
for (const segment of segments.slice(length)) {
|
|
107
|
+
const type = typeOfId(resolved.type);
|
|
108
|
+
if (type.kind === "record") {
|
|
109
|
+
const field = type.fields.find(({ name }) => name === segment);
|
|
110
|
+
if (!field) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
resolved = {
|
|
114
|
+
kind: "target",
|
|
115
|
+
path: joinPath(resolved.path, segment),
|
|
116
|
+
target: `${resolved.target}.${field.target}`,
|
|
117
|
+
type: field.type,
|
|
118
|
+
readable: resolved.readable,
|
|
119
|
+
...(field.generated && { generated: true }),
|
|
120
|
+
...(field.declaration && { declaration: field.declaration }),
|
|
121
|
+
};
|
|
122
|
+
} else if (type.kind === "array") {
|
|
123
|
+
if (!isArrayIndexSegment(segment)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const index = Number(segment.slice(1, -1));
|
|
127
|
+
if (index < type.lower || index >= type.lower + type.length) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
resolved = {
|
|
131
|
+
kind: "target",
|
|
132
|
+
path: joinPath(resolved.path, segment),
|
|
133
|
+
target: `${resolved.target}${segment}`,
|
|
134
|
+
type: type.element,
|
|
135
|
+
readable: resolved.readable,
|
|
136
|
+
};
|
|
137
|
+
} else {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const typedChildren = (resolved: ResolvedPath | undefined) => {
|
|
146
|
+
const type =
|
|
147
|
+
resolved?.kind === "target" ? typeOfId(resolved.type) : undefined;
|
|
148
|
+
if (type?.kind === "record") {
|
|
149
|
+
return type.fields.map(({ name }) => name);
|
|
150
|
+
}
|
|
151
|
+
if (type?.kind === "array") {
|
|
152
|
+
return Array.from(
|
|
153
|
+
{ length: type.length },
|
|
154
|
+
(_, index) => `[${type.lower + index}]`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return [];
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** A path's fields or indexes, and the roots and constants below it. */
|
|
161
|
+
const children = (path: string): PathNode[] => {
|
|
162
|
+
const names = [
|
|
163
|
+
...typedChildren(resolve(path)),
|
|
164
|
+
...(groups.get(path) ?? []),
|
|
165
|
+
];
|
|
166
|
+
return [...new Set(names)].map((name) => {
|
|
167
|
+
const child = joinPath(path, name);
|
|
168
|
+
return { name, ...(resolve(child) ?? { kind: "group", path: child }) };
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const isLeaf = (node: ResolvedPath | GroupPath) =>
|
|
173
|
+
node.kind === "constant" ||
|
|
174
|
+
(node.kind === "target" &&
|
|
175
|
+
["scalar", "enum"].includes(typeOfId(node.type).kind));
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Leaf paths matching `patterns`, where `*` stands for one segment. A pattern naming a record,
|
|
179
|
+
* array or group stands for all its leaves; wildcards and expansion skip compiler-made fields.
|
|
180
|
+
* Paths that don't resolve are kept, so callers can report them.
|
|
181
|
+
*/
|
|
182
|
+
const expand = (patterns: readonly string[], limit = 2000) => {
|
|
183
|
+
const leaves = new Set<string>();
|
|
184
|
+
const add = (path: string) => {
|
|
185
|
+
leaves.add(path);
|
|
186
|
+
if (leaves.size > limit) {
|
|
187
|
+
throw new RangeError(`More than ${limit} values match`);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const below = (node: ResolvedPath | GroupPath) => {
|
|
191
|
+
if (isLeaf(node)) {
|
|
192
|
+
return add(node.path);
|
|
193
|
+
}
|
|
194
|
+
for (const child of children(node.path)) {
|
|
195
|
+
if (!(child.kind === "target" && child.generated)) {
|
|
196
|
+
below(child);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const match = (
|
|
201
|
+
prefix: string,
|
|
202
|
+
rest: readonly string[],
|
|
203
|
+
wildcard: boolean,
|
|
204
|
+
) => {
|
|
205
|
+
const [head, ...tail] = rest;
|
|
206
|
+
if (head === undefined) {
|
|
207
|
+
const node: ResolvedPath | GroupPath | undefined =
|
|
208
|
+
resolve(prefix) ??
|
|
209
|
+
(groups.has(prefix) ? { kind: "group", path: prefix } : undefined);
|
|
210
|
+
if (node) {
|
|
211
|
+
below(node);
|
|
212
|
+
} else if (!wildcard) {
|
|
213
|
+
add(prefix);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (head === "*" || head === anyIndex) {
|
|
218
|
+
for (const child of children(prefix)) {
|
|
219
|
+
if (
|
|
220
|
+
!(child.kind === "target" && child.generated) &&
|
|
221
|
+
(head === "*" || child.name.startsWith("["))
|
|
222
|
+
) {
|
|
223
|
+
match(child.path, tail, true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
match(joinPath(prefix, head), tail, wildcard);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
for (const pattern of patterns) {
|
|
231
|
+
match("", pathSegments(pattern), false);
|
|
232
|
+
}
|
|
233
|
+
return [...leaves];
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** A value as TypeScript shows it: enum members by name, BOOL as a boolean. */
|
|
237
|
+
const decode = (typeId: string, raw: unknown) => {
|
|
238
|
+
const type = typeOfId(typeId);
|
|
239
|
+
if (type.kind === "enum") {
|
|
240
|
+
return (
|
|
241
|
+
Object.entries(type.members).find(([, value]) => value === raw)?.[0] ??
|
|
242
|
+
raw
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
type.kind === "scalar" &&
|
|
247
|
+
type.iec === "BOOL" &&
|
|
248
|
+
typeof raw === "number"
|
|
249
|
+
) {
|
|
250
|
+
return raw !== 0;
|
|
251
|
+
}
|
|
252
|
+
return raw;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return { typeOf: typeOfId, resolve, children, isLeaf, expand, decode };
|
|
183
256
|
};
|
|
184
257
|
|
|
185
258
|
export type Paths = ReturnType<typeof createPaths>;
|
package/src/requests.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { RequestsReply } from "@tsplc/server/requests";
|
|
2
|
+
import type { RequestTrace } from "./api.ts";
|
|
3
|
+
import { type Entry, timelineEntries } from "./timeline.ts";
|
|
4
|
+
|
|
5
|
+
/** Traces kept for jobs to find their calls by; older ones are forgotten. */
|
|
6
|
+
const callsRemembered = 1000;
|
|
7
|
+
|
|
8
|
+
/** A call that started or ended, with its timeline entry; `added` when the entry is new. */
|
|
9
|
+
interface RequestChange {
|
|
10
|
+
request: RequestTrace;
|
|
11
|
+
entry: Entry<"request">;
|
|
12
|
+
added: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Follows the app server's request trace (`GET /tsplc/debug/requests`), asking again from before
|
|
17
|
+
* the oldest call still pending, so each call is seen when it starts and again when it ends.
|
|
18
|
+
*/
|
|
19
|
+
export const createRequestFollower = () => {
|
|
20
|
+
let newest = 0;
|
|
21
|
+
const pending = new Map<number, Entry<"request">>();
|
|
22
|
+
/** By `<job id>:<service>:<sequence>`, newest last. */
|
|
23
|
+
const calls = new Map<string, RequestTrace>();
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
/** Where the next pull starts. */
|
|
27
|
+
since: () => (pending.size ? Math.min(...pending.keys()) - 1 : newest),
|
|
28
|
+
/** Takes a reply to `?since=<since()>`, telling `changed` of each call as it is taken. */
|
|
29
|
+
accept: (
|
|
30
|
+
reply: RequestsReply,
|
|
31
|
+
changed: (change: RequestChange) => void,
|
|
32
|
+
) => {
|
|
33
|
+
// A restarted server counts from 1 again.
|
|
34
|
+
if (reply.latest < newest) {
|
|
35
|
+
newest = 0;
|
|
36
|
+
pending.clear();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const oldest = reply.requests[0]?.seq;
|
|
40
|
+
if (oldest !== undefined) {
|
|
41
|
+
for (const seq of pending.keys()) {
|
|
42
|
+
if (seq < oldest) {
|
|
43
|
+
pending.delete(seq);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const request of reply.requests) {
|
|
48
|
+
const waiting = pending.get(request.seq);
|
|
49
|
+
if (waiting && request.status === "pending") {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!waiting && request.seq <= newest) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const entry = waiting ?? timelineEntries.request(request);
|
|
56
|
+
entry.request = request;
|
|
57
|
+
newest = Math.max(newest, request.seq);
|
|
58
|
+
if (request.status === "pending") {
|
|
59
|
+
pending.set(request.seq, entry);
|
|
60
|
+
} else {
|
|
61
|
+
pending.delete(request.seq);
|
|
62
|
+
}
|
|
63
|
+
if (request.job !== undefined && request.sequence !== undefined) {
|
|
64
|
+
calls.set(
|
|
65
|
+
`${request.job}:${request.service}:${request.sequence}`,
|
|
66
|
+
request,
|
|
67
|
+
);
|
|
68
|
+
for (const key of [...calls.keys()].slice(0, -callsRemembered)) {
|
|
69
|
+
calls.delete(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
changed({ request, entry, added: !waiting });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
/** The server's trace of a call, which the PLC tags with its job and the call site's count. */
|
|
76
|
+
call: ({
|
|
77
|
+
job,
|
|
78
|
+
service,
|
|
79
|
+
sequence,
|
|
80
|
+
}: {
|
|
81
|
+
job: number;
|
|
82
|
+
service: string;
|
|
83
|
+
sequence: number;
|
|
84
|
+
}) => calls.get(`${job}:${service}:${sequence}`),
|
|
85
|
+
};
|
|
86
|
+
};
|