devicectl-core 0.1.0__py3-none-any.whl
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.
- devicectl/__init__.py +18 -0
- devicectl/cli/__init__.py +1 -0
- devicectl/cli/command.py +95 -0
- devicectl/cli/exits.py +32 -0
- devicectl/cli/fanout.py +142 -0
- devicectl/cli/main.py +69 -0
- devicectl/cli/output.py +299 -0
- devicectl/cli/parser.py +80 -0
- devicectl/cli/report.py +86 -0
- devicectl/cli/target.py +26 -0
- devicectl/clock.py +57 -0
- devicectl/devtools/__init__.py +6 -0
- devicectl/devtools/frontlint.py +935 -0
- devicectl/devtools/htmcheck.py +396 -0
- devicectl/devtools/rendercheck.py +384 -0
- devicectl/doctor.py +112 -0
- devicectl/errors.py +68 -0
- devicectl/fields.py +564 -0
- devicectl/meta.py +64 -0
- devicectl/paths.py +40 -0
- devicectl/progress.py +77 -0
- devicectl/report.py +67 -0
- devicectl/testing.py +199 -0
- devicectl/trace.py +333 -0
- devicectl/web/__init__.py +1 -0
- devicectl/web/agents.py +94 -0
- devicectl/web/events.py +171 -0
- devicectl/web/http.py +243 -0
- devicectl/web/progress.py +101 -0
- devicectl/web/server.py +1013 -0
- devicectl/web/static/core.css +3034 -0
- devicectl/web/static/js/api.js +198 -0
- devicectl/web/static/js/band.js +640 -0
- devicectl/web/static/js/chart.js +400 -0
- devicectl/web/static/js/drafts.js +312 -0
- devicectl/web/static/js/notify.js +272 -0
- devicectl/web/static/js/panels.js +432 -0
- devicectl/web/static/js/shell.js +672 -0
- devicectl/web/static/js/trace.js +133 -0
- devicectl/web/static/js/ui.js +1139 -0
- devicectl/web/static/vendor/preact-htm.module.js +27 -0
- devicectl/web/worker.py +697 -0
- devicectl_core-0.1.0.dist-info/METADATA +131 -0
- devicectl_core-0.1.0.dist-info/RECORD +47 -0
- devicectl_core-0.1.0.dist-info/WHEEL +4 -0
- devicectl_core-0.1.0.dist-info/licenses/LICENSE +287 -0
- devicectl_core-0.1.0.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* Talking to the program serving this page: requests, and the event stream.
|
|
2
|
+
*
|
|
3
|
+
* Every write carries the X-UI-Request header, which a cross-origin form
|
|
4
|
+
* cannot set -- cookie authentication on its own would let any other page
|
|
5
|
+
* drive this server through the browser of whoever has it open. The
|
|
6
|
+
* server refuses a POST without it; that refusal is the whole mechanism,
|
|
7
|
+
* and the header's name is arbitrary beyond being one a simple request may
|
|
8
|
+
* not carry.
|
|
9
|
+
*
|
|
10
|
+
* Two failure modes are told apart deliberately. A request that reached
|
|
11
|
+
* the server and was refused carries the server's own sentence -- a value
|
|
12
|
+
* out of range, a setting the device will not take, a device that did not
|
|
13
|
+
* answer -- and is shown as it arrived. A request that never reached the
|
|
14
|
+
* server at all means the program behind this page has gone, and every
|
|
15
|
+
* control on it is about to lie about a device it can no longer see. That
|
|
16
|
+
* is a different thing, and `offline` on the error says so.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const UI_HEADER = 'X-UI-Request';
|
|
20
|
+
|
|
21
|
+
/* What the program calls itself, for the one sentence this module writes
|
|
22
|
+
* on its own: "the ... server is not answering". Set once, from the
|
|
23
|
+
* program's own api.js, before anything is asked for. */
|
|
24
|
+
let program = 'the';
|
|
25
|
+
|
|
26
|
+
export function configure({ name }) {
|
|
27
|
+
program = name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ApiError extends Error {
|
|
31
|
+
constructor(message, { status = 0, offline = false, traceable = false } = {}) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'ApiError';
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.offline = offline;
|
|
36
|
+
/* The server says the device -- or the program, while talking to it --
|
|
37
|
+
* is what failed, which is the one kind of failure a trace can explain.
|
|
38
|
+
* See js/trace.js. */
|
|
39
|
+
this.traceable = traceable;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Told when a request could not reach the server at all.
|
|
44
|
+
*
|
|
45
|
+
* The event stream notices too, but only on its own schedule -- a click is
|
|
46
|
+
* often the first thing that finds out, so it says so here rather than
|
|
47
|
+
* raising a toast about "Failed to fetch" and leaving the page looking
|
|
48
|
+
* healthy. See `useServerLink` in shell.js.
|
|
49
|
+
*/
|
|
50
|
+
const unreachable = new Set();
|
|
51
|
+
|
|
52
|
+
export function onUnreachable(fn) {
|
|
53
|
+
unreachable.add(fn);
|
|
54
|
+
return () => unreachable.delete(fn);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* A query string from an object, dropping anything unset -- so an id can
|
|
58
|
+
* be passed as undefined on a page that has not chosen a device yet. */
|
|
59
|
+
export function query(params) {
|
|
60
|
+
const parts = Object.entries(params || {})
|
|
61
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
62
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
|
|
63
|
+
return parts.length ? `?${parts.join('&')}` : '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* One request. `body` is sent as JSON, `raw` as bytes; a GET carries
|
|
67
|
+
* neither and no UI header, because the header is there to make a write
|
|
68
|
+
* impossible to forge and a read is not a write. */
|
|
69
|
+
export async function send(method, path, { params, body, raw, signal } = {}) {
|
|
70
|
+
const headers = {};
|
|
71
|
+
let payload;
|
|
72
|
+
if (raw !== undefined) {
|
|
73
|
+
payload = raw;
|
|
74
|
+
headers['Content-Type'] = 'application/octet-stream';
|
|
75
|
+
} else if (body !== undefined) {
|
|
76
|
+
payload = JSON.stringify(body);
|
|
77
|
+
headers['Content-Type'] = 'application/json';
|
|
78
|
+
}
|
|
79
|
+
headers[UI_HEADER] = '1';
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await fetch(`${path}${query(params)}`, {
|
|
83
|
+
method,
|
|
84
|
+
headers,
|
|
85
|
+
body: payload,
|
|
86
|
+
signal,
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
/* An aborted request is the caller's own doing -- a panel that
|
|
90
|
+
* unmounted, a navigation -- and says nothing about the server. */
|
|
91
|
+
if (err?.name === 'AbortError') throw err;
|
|
92
|
+
for (const fn of [...unreachable]) fn();
|
|
93
|
+
throw new ApiError(`the ${program} server is not answering`, { offline: true });
|
|
94
|
+
}
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
let doc = null;
|
|
97
|
+
if (text) {
|
|
98
|
+
try {
|
|
99
|
+
doc = JSON.parse(text);
|
|
100
|
+
} catch {
|
|
101
|
+
doc = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new ApiError(doc?.error || text || response.statusText, {
|
|
106
|
+
status: response.status,
|
|
107
|
+
traceable: Boolean(doc?.traceable),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return doc;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const get = (path, options) => send('GET', path, options);
|
|
114
|
+
export const post = (path, body, options) => send('POST', path, { body, ...options });
|
|
115
|
+
export const upload = (path, bytes, options) =>
|
|
116
|
+
send('POST', path, { raw: bytes, ...options });
|
|
117
|
+
|
|
118
|
+
/* A file the server streams, saved wherever the browser puts downloads.
|
|
119
|
+
*
|
|
120
|
+
* Not `send`: what comes back is a backup, an export or a trace report
|
|
121
|
+
* rather than a document to parse, and it is named by the server's own
|
|
122
|
+
* `Content-Disposition` -- the filename carries the station, the unit or
|
|
123
|
+
* the moment, which is what makes a folder of them readable a week later.
|
|
124
|
+
* `fallback` is the name to use when the server did not send one.
|
|
125
|
+
*
|
|
126
|
+
* A failure still arrives as text, and it is the server's sentence, so it
|
|
127
|
+
* is raised as `ApiError` exactly as it would be from any other request.
|
|
128
|
+
*/
|
|
129
|
+
export async function download(path, { params, fallback = 'download' } = {}) {
|
|
130
|
+
const response = await fetch(`${path}${query(params)}`, {
|
|
131
|
+
headers: { [UI_HEADER]: '1' },
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const text = await response.text();
|
|
135
|
+
let doc = null;
|
|
136
|
+
try {
|
|
137
|
+
doc = JSON.parse(text);
|
|
138
|
+
} catch {
|
|
139
|
+
doc = null;
|
|
140
|
+
}
|
|
141
|
+
throw new ApiError(doc?.error || text || response.statusText, {
|
|
142
|
+
status: response.status,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const named = /filename="([^"]+)"/.exec(response.headers.get('Content-Disposition') || '');
|
|
146
|
+
const blob = await response.blob();
|
|
147
|
+
const link = document.createElement('a');
|
|
148
|
+
link.href = URL.createObjectURL(blob);
|
|
149
|
+
link.download = named ? named[1] : fallback;
|
|
150
|
+
link.click();
|
|
151
|
+
URL.revokeObjectURL(link.href);
|
|
152
|
+
return link.download;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* The event stream. One connection for the whole page: the server
|
|
156
|
+
* multiplexes by event name, and browsers only allow a handful of
|
|
157
|
+
* connections per origin anyway.
|
|
158
|
+
*
|
|
159
|
+
* `handlers` is a map of event name to a function taking the parsed
|
|
160
|
+
* payload, plus the two reserved names `onopen` and `onerror`. Returns
|
|
161
|
+
* the way to close it.
|
|
162
|
+
*/
|
|
163
|
+
export function subscribe(path, handlers) {
|
|
164
|
+
let source = null;
|
|
165
|
+
let retry = null;
|
|
166
|
+
|
|
167
|
+
const open = () => {
|
|
168
|
+
source = new EventSource(path);
|
|
169
|
+
for (const [name, fn] of Object.entries(handlers)) {
|
|
170
|
+
if (name === 'onopen' || name === 'onerror') continue;
|
|
171
|
+
source.addEventListener(name, (event) => {
|
|
172
|
+
try {
|
|
173
|
+
fn(event.data ? JSON.parse(event.data) : null);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error('bad event payload', name, err);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
source.onopen = () => handlers.onopen?.();
|
|
180
|
+
source.onerror = () => {
|
|
181
|
+
handlers.onerror?.();
|
|
182
|
+
/* EventSource reconnects on its own, but not after the server has
|
|
183
|
+
* gone away for good; a slow explicit retry covers a restart. */
|
|
184
|
+
if (source.readyState === EventSource.CLOSED && !retry) {
|
|
185
|
+
retry = setTimeout(() => {
|
|
186
|
+
retry = null;
|
|
187
|
+
open();
|
|
188
|
+
}, 3000);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
open();
|
|
194
|
+
return () => {
|
|
195
|
+
if (retry) clearTimeout(retry);
|
|
196
|
+
if (source) source.close();
|
|
197
|
+
};
|
|
198
|
+
}
|