@tsplc/agent 0.0.0-stage → 0.2.1
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/README.md +12 -2
- package/dist/cli.js +574 -0
- package/dist/cli.js.map +15 -0
- package/dist/client.js +140 -0
- package/dist/client.js.map +11 -0
- package/dist/index.js +525 -0
- package/dist/index.js.map +14 -0
- package/package.json +48 -4
- package/src/agent.ts +361 -0
- package/src/cli.ts +60 -0
- package/src/client.ts +119 -0
- package/src/discovery.ts +84 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +86 -0
- package/src/releases.ts +77 -0
- package/src/server-process.ts +96 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// packages/agent/src/agent.ts
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
5
|
+
import { mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
6
|
+
import { join as join2 } from "node:path";
|
|
7
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import {
|
|
10
|
+
explicitDownload,
|
|
11
|
+
findControllerLogin,
|
|
12
|
+
gatewayImage,
|
|
13
|
+
loginCommandSchema,
|
|
14
|
+
openControllerLogin,
|
|
15
|
+
resolveConfig,
|
|
16
|
+
updateController
|
|
17
|
+
} from "@tsplc/build";
|
|
18
|
+
import { z as z3 } from "zod";
|
|
19
|
+
|
|
20
|
+
// packages/agent/src/discovery.ts
|
|
21
|
+
import { Resolver } from "node:dns/promises";
|
|
22
|
+
import { createConnection } from "node:net";
|
|
23
|
+
import { networkInterfaces as osInterfaces } from "node:os";
|
|
24
|
+
var runtimePort = 11740;
|
|
25
|
+
var largestPrefix = 22;
|
|
26
|
+
var probeTimeoutMs = 500;
|
|
27
|
+
var probesAtOnce = 128;
|
|
28
|
+
var networkInterfaces = () => Object.entries(osInterfaces()).flatMap(([name, addresses]) => (addresses ?? []).filter((address) => address.family === "IPv4" && !address.internal).map((address) => ({
|
|
29
|
+
name,
|
|
30
|
+
address: address.address,
|
|
31
|
+
prefixLength: address.netmask.split(".").reduce((bits, octet) => bits + Number(octet).toString(2).replaceAll("0", "").length, 0)
|
|
32
|
+
})));
|
|
33
|
+
var toNumber = (address) => address.split(".").reduce((value, octet) => value * 256 + Number(octet), 0);
|
|
34
|
+
var toAddress = (value) => [24, 16, 8, 0].map((shift) => Math.floor(value / 2 ** shift) % 256).join(".");
|
|
35
|
+
var subnetHosts = ({ address, prefixLength }) => {
|
|
36
|
+
if (prefixLength < largestPrefix)
|
|
37
|
+
throw new Error(`The /${prefixLength} network is too large to scan; the limit is /${largestPrefix}.`);
|
|
38
|
+
const size = 2 ** (32 - prefixLength);
|
|
39
|
+
const network = toNumber(address) - toNumber(address) % size;
|
|
40
|
+
const hosts = size <= 2 ? Array.from({ length: size }, (_, index) => network + index) : Array.from({ length: size - 2 }, (_, index) => network + index + 1);
|
|
41
|
+
return hosts.map(toAddress).filter((host) => host !== address);
|
|
42
|
+
};
|
|
43
|
+
var accepts = (host, port) => new Promise((resolve) => {
|
|
44
|
+
const socket = createConnection({ host, port });
|
|
45
|
+
const timer = setTimeout(() => finish(false), probeTimeoutMs);
|
|
46
|
+
const finish = (open) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
socket.destroy();
|
|
49
|
+
resolve(open);
|
|
50
|
+
};
|
|
51
|
+
socket.once("connect", () => finish(true));
|
|
52
|
+
socket.once("error", () => finish(false));
|
|
53
|
+
});
|
|
54
|
+
var resolver = new Resolver({ timeout: 1000, tries: 1 });
|
|
55
|
+
var hostname = (address) => resolver.reverse(address).then((names) => names[0] ?? null, () => null);
|
|
56
|
+
var discoverControllers = async (network, port = runtimePort) => {
|
|
57
|
+
const hosts = subnetHosts(network);
|
|
58
|
+
const found = [];
|
|
59
|
+
for (let start = 0;start < hosts.length; start += probesAtOnce) {
|
|
60
|
+
const batch = hosts.slice(start, start + probesAtOnce);
|
|
61
|
+
const open = await Promise.all(batch.map((host) => accepts(host, port)));
|
|
62
|
+
for (const [index, host] of batch.entries())
|
|
63
|
+
if (open[index])
|
|
64
|
+
found.push({ address: host, port, hostname: await hostname(host) });
|
|
65
|
+
}
|
|
66
|
+
return found;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// packages/agent/src/protocol.ts
|
|
70
|
+
import { z } from "zod";
|
|
71
|
+
var targetSchema = z.strictObject({
|
|
72
|
+
host: z.string().nonempty(),
|
|
73
|
+
port: z.number().int().min(1).max(65535)
|
|
74
|
+
});
|
|
75
|
+
var networkInterfaceSchema = z.strictObject({
|
|
76
|
+
name: z.string(),
|
|
77
|
+
address: z.ipv4(),
|
|
78
|
+
prefixLength: z.number().int().min(0).max(32)
|
|
79
|
+
});
|
|
80
|
+
var controllerSchema = z.strictObject({
|
|
81
|
+
address: z.ipv4(),
|
|
82
|
+
port: z.number().int(),
|
|
83
|
+
hostname: z.string().nullable()
|
|
84
|
+
});
|
|
85
|
+
var credentialsSchema = z.strictObject({
|
|
86
|
+
username: z.string().nonempty(),
|
|
87
|
+
password: z.string().nonempty()
|
|
88
|
+
});
|
|
89
|
+
var variableSchema = z.strictObject({ path: z.string(), type: z.string() });
|
|
90
|
+
var releaseManifestSchema = z.strictObject({
|
|
91
|
+
config: z.record(z.string(), z.unknown()),
|
|
92
|
+
variables: z.array(variableSchema),
|
|
93
|
+
server: z.string().nonempty().optional()
|
|
94
|
+
});
|
|
95
|
+
var deployOptionsSchema = z.strictObject({
|
|
96
|
+
credentials: credentialsSchema,
|
|
97
|
+
download: z.boolean(),
|
|
98
|
+
initializeUser: z.boolean()
|
|
99
|
+
});
|
|
100
|
+
var eventSchema = z.discriminatedUnion("kind", [
|
|
101
|
+
z.strictObject({ kind: z.literal("progress"), message: z.string() }),
|
|
102
|
+
z.strictObject({ kind: z.literal("done"), result: z.unknown() }),
|
|
103
|
+
z.strictObject({
|
|
104
|
+
kind: z.literal("error"),
|
|
105
|
+
message: z.string(),
|
|
106
|
+
downloadRequired: z.boolean()
|
|
107
|
+
})
|
|
108
|
+
]);
|
|
109
|
+
var releaseSchema = z.strictObject({
|
|
110
|
+
id: z.string(),
|
|
111
|
+
deployedAt: z.iso.datetime(),
|
|
112
|
+
operation: z.enum(["download", "online-change"]),
|
|
113
|
+
controllerIds: z.strictObject({ codeId: z.string(), dataId: z.string() }),
|
|
114
|
+
server: z.string().optional()
|
|
115
|
+
});
|
|
116
|
+
var statusSchema = z.strictObject({
|
|
117
|
+
target: targetSchema.nullable(),
|
|
118
|
+
release: releaseSchema.nullable(),
|
|
119
|
+
server: z.strictObject({ executable: z.string(), running: z.boolean() }).nullable(),
|
|
120
|
+
login: z.strictObject({ state: z.string(), forced: z.array(z.string()) }).nullable()
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// packages/agent/src/releases.ts
|
|
124
|
+
import { createHash } from "node:crypto";
|
|
125
|
+
import { existsSync } from "node:fs";
|
|
126
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
127
|
+
import { dirname, join, posix } from "node:path";
|
|
128
|
+
import { z as z2 } from "zod";
|
|
129
|
+
var filePrefix = "files/";
|
|
130
|
+
var releasePath = (name) => {
|
|
131
|
+
const path = posix.normalize(name.slice(filePrefix.length));
|
|
132
|
+
if (posix.isAbsolute(path) || path.startsWith("..") || path.includes("\\"))
|
|
133
|
+
throw new Error(`Invalid release file name: ${name}`);
|
|
134
|
+
return path;
|
|
135
|
+
};
|
|
136
|
+
var readUpload = async (form) => {
|
|
137
|
+
const manifest = releaseManifestSchema.parse(JSON.parse(z2.string().parse(form.get("manifest"))));
|
|
138
|
+
const files = await Promise.all([...form.entries()].filter(([name]) => name.startsWith(filePrefix)).map(async ([name, value]) => ({
|
|
139
|
+
path: releasePath(name),
|
|
140
|
+
content: new Uint8Array(await z2.instanceof(File).parse(value).arrayBuffer())
|
|
141
|
+
})));
|
|
142
|
+
if (manifest.server && !files.some((file) => file.path === manifest.server))
|
|
143
|
+
throw new Error(`The release names a server ${manifest.server} it doesn't contain`);
|
|
144
|
+
return { manifest, files };
|
|
145
|
+
};
|
|
146
|
+
var saveRelease = async (releases, { manifest, files }) => {
|
|
147
|
+
const hash = createHash("sha256").update(JSON.stringify(manifest));
|
|
148
|
+
for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path)))
|
|
149
|
+
hash.update(file.path).update(createHash("sha256").update(file.content).digest("hex"));
|
|
150
|
+
const id = hash.digest("hex").slice(0, 16);
|
|
151
|
+
const directory = join(releases, id);
|
|
152
|
+
if (!existsSync(directory)) {
|
|
153
|
+
await mkdir(releases, { recursive: true });
|
|
154
|
+
const staging = await mkdtemp(join(releases, ".upload-"));
|
|
155
|
+
try {
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
await mkdir(dirname(join(staging, file.path)), { recursive: true });
|
|
158
|
+
await writeFile(join(staging, file.path), file.content, {
|
|
159
|
+
mode: file.path === manifest.server ? 493 : 420
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
await writeFile(join(staging, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
163
|
+
await rename(staging, directory);
|
|
164
|
+
} catch (cause) {
|
|
165
|
+
await rm(staging, { recursive: true, force: true });
|
|
166
|
+
throw cause;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { id, directory, manifest };
|
|
170
|
+
};
|
|
171
|
+
var loadRelease = async (releases, id) => {
|
|
172
|
+
const directory = join(releases, id);
|
|
173
|
+
const manifest = releaseManifestSchema.parse(JSON.parse(await readFile(join(directory, "manifest.json"), "utf8")));
|
|
174
|
+
return { id, directory, manifest };
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// packages/agent/src/server-process.ts
|
|
178
|
+
import { dirname as dirname2 } from "node:path";
|
|
179
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
180
|
+
var healthyWithinMs = 60000;
|
|
181
|
+
var settleMs = 5000;
|
|
182
|
+
var restartDelayMs = 2000;
|
|
183
|
+
var createServerSupervisor = (options) => {
|
|
184
|
+
let current;
|
|
185
|
+
const spawn = (executable) => {
|
|
186
|
+
const child = Bun.spawn([executable], {
|
|
187
|
+
cwd: dirname2(executable),
|
|
188
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
189
|
+
onExit: (_, code) => {
|
|
190
|
+
if (current?.process !== child)
|
|
191
|
+
return;
|
|
192
|
+
options.log(`Server exited (${code}); restarting it in ${restartDelayMs / 1000} s`);
|
|
193
|
+
delay(restartDelayMs).then(() => {
|
|
194
|
+
if (current?.process === child)
|
|
195
|
+
current = { executable, process: spawn(executable) };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return child;
|
|
200
|
+
};
|
|
201
|
+
const start = (executable) => {
|
|
202
|
+
current = { executable, process: spawn(executable) };
|
|
203
|
+
return current.process;
|
|
204
|
+
};
|
|
205
|
+
const stop = async () => {
|
|
206
|
+
const running = current;
|
|
207
|
+
current = undefined;
|
|
208
|
+
if (!running)
|
|
209
|
+
return;
|
|
210
|
+
running.process.kill();
|
|
211
|
+
await running.process.exited;
|
|
212
|
+
};
|
|
213
|
+
const healthy = async (child) => {
|
|
214
|
+
if (!options.healthUrl) {
|
|
215
|
+
await delay(settleMs);
|
|
216
|
+
return child.exitCode === null;
|
|
217
|
+
}
|
|
218
|
+
const deadline = Date.now() + healthyWithinMs;
|
|
219
|
+
while (Date.now() < deadline && child.exitCode === null) {
|
|
220
|
+
if (await fetch(options.healthUrl).then((response) => response.ok, () => false))
|
|
221
|
+
return true;
|
|
222
|
+
await delay(1000);
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
start,
|
|
228
|
+
stop,
|
|
229
|
+
replace: async (executable) => {
|
|
230
|
+
const previous = current?.executable;
|
|
231
|
+
await stop();
|
|
232
|
+
let failure;
|
|
233
|
+
const started = await (async () => healthy(start(executable)))().catch((error) => {
|
|
234
|
+
failure = error;
|
|
235
|
+
return false;
|
|
236
|
+
});
|
|
237
|
+
if (started)
|
|
238
|
+
return;
|
|
239
|
+
await stop();
|
|
240
|
+
if (previous)
|
|
241
|
+
start(previous);
|
|
242
|
+
const reason = failure instanceof Error ? failure.message : `it did not start${options.healthUrl ? ` answering ${options.healthUrl}` : ""}`;
|
|
243
|
+
throw new Error(`The new server failed: ${reason}. ${previous ? "The previous one is running again." : ""}`.trim(), { cause: failure });
|
|
244
|
+
},
|
|
245
|
+
status: () => current && { executable: current.executable, running: current.process.exitCode === null }
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// packages/agent/src/agent.ts
|
|
250
|
+
var execute = promisify(execFile);
|
|
251
|
+
var network = "tsplc-agent";
|
|
252
|
+
var gateway = { host: "tsplc-agent-gateway", port: 1217 };
|
|
253
|
+
var loginContainer = "tsplc-agent-login";
|
|
254
|
+
var credentialNames = {
|
|
255
|
+
usernameEnv: "TSPLC_DEVICE_USERNAME",
|
|
256
|
+
passwordEnv: "TSPLC_DEVICE_PASSWORD"
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
class AgentError extends Error {
|
|
260
|
+
status;
|
|
261
|
+
downloadRequired;
|
|
262
|
+
constructor(message, status, downloadRequired = false) {
|
|
263
|
+
super(message);
|
|
264
|
+
this.status = status;
|
|
265
|
+
this.downloadRequired = downloadRequired;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
var json = (body, status = 200) => Response.json(body, { status });
|
|
269
|
+
var readJson = async (path, schema) => existsSync2(path) ? schema.parse(JSON.parse(await readFile2(path, "utf8"))) : null;
|
|
270
|
+
var docker = (...args) => execute("docker", args).then(({ stdout }) => stdout.trim());
|
|
271
|
+
var ensureGateway = async () => {
|
|
272
|
+
if (!await docker("network", "ls", "--quiet", "--filter", `name=^${network}$`))
|
|
273
|
+
await docker("network", "create", network);
|
|
274
|
+
if (await docker("container", "ls", "--quiet", "--filter", `name=^${gateway.host}$`))
|
|
275
|
+
return;
|
|
276
|
+
await docker("rm", "--force", gateway.host);
|
|
277
|
+
await docker("run", "--detach", "--restart", "unless-stopped", "--platform", "linux/amd64", "--network", network, "--name", gateway.host, gatewayImage);
|
|
278
|
+
};
|
|
279
|
+
var withCredentials = async (credentials, run) => {
|
|
280
|
+
process.env[credentialNames.usernameEnv] = credentials.username;
|
|
281
|
+
process.env[credentialNames.passwordEnv] = credentials.password;
|
|
282
|
+
try {
|
|
283
|
+
return await run();
|
|
284
|
+
} finally {
|
|
285
|
+
delete process.env[credentialNames.usernameEnv];
|
|
286
|
+
delete process.env[credentialNames.passwordEnv];
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var followProgress = (controller, emit) => {
|
|
290
|
+
const jobs = join2(controller, ".codesys/controller/jobs");
|
|
291
|
+
const since = Date.now();
|
|
292
|
+
let following = true;
|
|
293
|
+
const done = (async () => {
|
|
294
|
+
let log;
|
|
295
|
+
let offset = 0;
|
|
296
|
+
while (following) {
|
|
297
|
+
await delay2(2000);
|
|
298
|
+
if (!log && existsSync2(jobs)) {
|
|
299
|
+
for (const job of await readdir(jobs)) {
|
|
300
|
+
const path = join2(jobs, job, ".codesys/controller/progress.log");
|
|
301
|
+
if (existsSync2(path) && (await stat(path)).mtimeMs >= since)
|
|
302
|
+
log = path;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!log)
|
|
306
|
+
continue;
|
|
307
|
+
const text = await readFile2(log, "utf8");
|
|
308
|
+
for (const line of text.slice(offset).split(`
|
|
309
|
+
`).filter(Boolean))
|
|
310
|
+
emit({ kind: "progress", message: line.replace(/^\S+Z /, "") });
|
|
311
|
+
offset = text.length;
|
|
312
|
+
}
|
|
313
|
+
})();
|
|
314
|
+
return async () => {
|
|
315
|
+
following = false;
|
|
316
|
+
await done;
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
var createAgent = async (options) => {
|
|
320
|
+
const paths = {
|
|
321
|
+
releases: join2(options.state, "releases"),
|
|
322
|
+
controller: join2(options.state, "controller"),
|
|
323
|
+
release: join2(options.state, "release.json"),
|
|
324
|
+
target: join2(options.state, "target.json")
|
|
325
|
+
};
|
|
326
|
+
await mkdir2(options.state, { recursive: true });
|
|
327
|
+
const server = createServerSupervisor({ healthUrl: options.serverHealthUrl, log: options.log });
|
|
328
|
+
const deployed = await readJson(paths.release, releaseSchema);
|
|
329
|
+
if (deployed?.server)
|
|
330
|
+
server.start(join2(paths.releases, deployed.server));
|
|
331
|
+
let busy;
|
|
332
|
+
const requireTarget = async () => {
|
|
333
|
+
const target = await readJson(paths.target, targetSchema);
|
|
334
|
+
if (!target)
|
|
335
|
+
throw new AgentError("Select the controller first: tsplc target", 409);
|
|
336
|
+
return target;
|
|
337
|
+
};
|
|
338
|
+
const controllerConfig = (release, target) => ({
|
|
339
|
+
...resolveConfig({
|
|
340
|
+
...release.manifest.config,
|
|
341
|
+
online: { network, gateway, device: target, credentials: credentialNames }
|
|
342
|
+
}, join2(release.directory, "manifest.json")),
|
|
343
|
+
outDir: paths.controller
|
|
344
|
+
});
|
|
345
|
+
const currentRelease = async () => {
|
|
346
|
+
const release = await readJson(paths.release, releaseSchema);
|
|
347
|
+
if (!release)
|
|
348
|
+
throw new AgentError("Nothing has been deployed yet: tsplc deploy", 409);
|
|
349
|
+
return loadRelease(paths.releases, release.id);
|
|
350
|
+
};
|
|
351
|
+
const operation = (name, run) => {
|
|
352
|
+
if (busy)
|
|
353
|
+
throw new AgentError(`The agent is busy with ${busy}`, 409);
|
|
354
|
+
busy = name;
|
|
355
|
+
const encoder = new TextEncoder;
|
|
356
|
+
let listening = true;
|
|
357
|
+
return new Response(new ReadableStream({
|
|
358
|
+
start: async (controller) => {
|
|
359
|
+
const write = (text) => {
|
|
360
|
+
if (listening)
|
|
361
|
+
controller.enqueue(encoder.encode(text));
|
|
362
|
+
};
|
|
363
|
+
const emit = (event) => write(`${JSON.stringify(event)}
|
|
364
|
+
`);
|
|
365
|
+
const heartbeat = setInterval(() => write(`
|
|
366
|
+
`), 15000);
|
|
367
|
+
try {
|
|
368
|
+
emit({ kind: "done", result: await run(emit) });
|
|
369
|
+
} catch (error) {
|
|
370
|
+
options.log(`${name} failed: ${error instanceof Error ? error.stack : String(error)}`);
|
|
371
|
+
emit({
|
|
372
|
+
kind: "error",
|
|
373
|
+
message: error instanceof Error ? error.message : String(error),
|
|
374
|
+
downloadRequired: error instanceof AgentError && error.downloadRequired || error instanceof Error && error.message.includes(explicitDownload)
|
|
375
|
+
});
|
|
376
|
+
} finally {
|
|
377
|
+
clearInterval(heartbeat);
|
|
378
|
+
busy = undefined;
|
|
379
|
+
if (listening)
|
|
380
|
+
controller.close();
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
cancel: () => {
|
|
384
|
+
listening = false;
|
|
385
|
+
}
|
|
386
|
+
}), { headers: { "Content-Type": "application/x-ndjson" } });
|
|
387
|
+
};
|
|
388
|
+
const deploy = async (form) => {
|
|
389
|
+
const settings = deployOptionsSchema.parse(JSON.parse(z3.string().parse(form.get("options"))));
|
|
390
|
+
const upload = await readUpload(form);
|
|
391
|
+
const target = await requireTarget();
|
|
392
|
+
if (await findControllerLogin(loginContainer))
|
|
393
|
+
throw new AgentError("A login is open; log out before deploying: tsplc logout", 409);
|
|
394
|
+
return operation("a deployment", async (emit) => {
|
|
395
|
+
const release = await saveRelease(paths.releases, upload);
|
|
396
|
+
const config = controllerConfig(release, target);
|
|
397
|
+
if (!settings.download && !existsSync2(join2(paths.controller, ".codesys/controller/current.json")))
|
|
398
|
+
throw new AgentError("The controller has no application downloaded by this agent; only a full download can deploy", 409, true);
|
|
399
|
+
const previous = await readJson(paths.release, releaseSchema);
|
|
400
|
+
const serverPath = release.manifest.server && `${release.id}/${release.manifest.server}`;
|
|
401
|
+
if (serverPath) {
|
|
402
|
+
emit({ kind: "progress", message: "Starting the new server" });
|
|
403
|
+
await server.replace(join2(paths.releases, serverPath));
|
|
404
|
+
}
|
|
405
|
+
const kind = settings.download ? "download" : "online-change";
|
|
406
|
+
emit({ kind: "progress", message: `Starting the ${kind}; CODESYS takes minutes to start` });
|
|
407
|
+
await ensureGateway();
|
|
408
|
+
const stopFollowing = followProgress(paths.controller, emit);
|
|
409
|
+
const receipt = await withCredentials(settings.credentials, () => updateController(config, kind, { initializeUser: settings.initializeUser })).catch(async (failure) => {
|
|
410
|
+
if (serverPath) {
|
|
411
|
+
emit({ kind: "progress", message: "Restoring the previous server" });
|
|
412
|
+
await (previous?.server ? server.replace(join2(paths.releases, previous.server)) : server.stop());
|
|
413
|
+
}
|
|
414
|
+
throw failure;
|
|
415
|
+
}).finally(stopFollowing);
|
|
416
|
+
const deployedRelease = {
|
|
417
|
+
id: release.id,
|
|
418
|
+
deployedAt: new Date().toISOString(),
|
|
419
|
+
operation: kind,
|
|
420
|
+
controllerIds: receipt.controllerIds,
|
|
421
|
+
server: serverPath ?? previous?.server
|
|
422
|
+
};
|
|
423
|
+
await writeFile2(paths.release, JSON.stringify(deployedRelease, null, 2));
|
|
424
|
+
return deployedRelease;
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
const openLogin = async (credentials) => {
|
|
428
|
+
const release = await currentRelease();
|
|
429
|
+
const config = controllerConfig(release, await requireTarget());
|
|
430
|
+
return operation("a login", async (emit) => {
|
|
431
|
+
if (await findControllerLogin(loginContainer))
|
|
432
|
+
throw new AgentError("A login is already open", 409);
|
|
433
|
+
emit({ kind: "progress", message: "Logging in; CODESYS takes minutes to start" });
|
|
434
|
+
await ensureGateway();
|
|
435
|
+
const login = await withCredentials(credentials, () => openControllerLogin(config, loginContainer));
|
|
436
|
+
return login.send({ command: "state" });
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
const requireLogin = async () => {
|
|
440
|
+
const login = await findControllerLogin(loginContainer);
|
|
441
|
+
if (!login)
|
|
442
|
+
throw new AgentError("No login is open: tsplc login", 409);
|
|
443
|
+
return login;
|
|
444
|
+
};
|
|
445
|
+
const status = async () => {
|
|
446
|
+
const login = await findControllerLogin(loginContainer);
|
|
447
|
+
const state = login && await login.ready() && await login.send({ command: "state" });
|
|
448
|
+
return {
|
|
449
|
+
target: await readJson(paths.target, targetSchema),
|
|
450
|
+
release: await readJson(paths.release, releaseSchema),
|
|
451
|
+
server: server.status() ?? null,
|
|
452
|
+
login: state && "state" in state ? state : null
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
const route = async (request) => {
|
|
456
|
+
const { pathname } = new URL(request.url);
|
|
457
|
+
const key = `${request.method} ${pathname}`;
|
|
458
|
+
switch (key) {
|
|
459
|
+
case "GET /status":
|
|
460
|
+
return json(await status());
|
|
461
|
+
case "GET /interfaces":
|
|
462
|
+
return json(networkInterfaces());
|
|
463
|
+
case "POST /discover": {
|
|
464
|
+
const { name } = z3.strictObject({ name: z3.string() }).parse(await request.json());
|
|
465
|
+
const selected = networkInterfaces().find((network) => network.name === name);
|
|
466
|
+
if (!selected)
|
|
467
|
+
throw new AgentError(`No IPv4 interface named ${name}`, 404);
|
|
468
|
+
return json(await discoverControllers(selected));
|
|
469
|
+
}
|
|
470
|
+
case "PUT /target": {
|
|
471
|
+
const target = targetSchema.parse(await request.json());
|
|
472
|
+
await writeFile2(paths.target, JSON.stringify(target, null, 2));
|
|
473
|
+
return json(target);
|
|
474
|
+
}
|
|
475
|
+
case "GET /variables":
|
|
476
|
+
return json((await currentRelease()).manifest.variables);
|
|
477
|
+
case "POST /deploy":
|
|
478
|
+
return deploy(await request.formData());
|
|
479
|
+
case "POST /login":
|
|
480
|
+
return openLogin(credentialsSchema.parse(await request.json()));
|
|
481
|
+
case "POST /login/command":
|
|
482
|
+
return json(await (await requireLogin()).send(loginCommandSchema.parse(await request.json())));
|
|
483
|
+
case "DELETE /login":
|
|
484
|
+
await (await requireLogin()).close();
|
|
485
|
+
return json({});
|
|
486
|
+
default:
|
|
487
|
+
return json({ error: `No route ${key}` }, 404);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
const expected = Buffer.from(`Bearer ${options.token}`);
|
|
491
|
+
return {
|
|
492
|
+
fetch: async (request) => {
|
|
493
|
+
const actual = Buffer.from(request.headers.get("authorization") ?? "");
|
|
494
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
|
|
495
|
+
return json({ error: "Unauthorized" }, 401);
|
|
496
|
+
try {
|
|
497
|
+
return await route(request);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof AgentError)
|
|
500
|
+
return json({ error: error.message }, error.status);
|
|
501
|
+
if (error instanceof z3.ZodError)
|
|
502
|
+
return json({ error: z3.prettifyError(error) }, 400);
|
|
503
|
+
options.log(`${request.method} ${request.url} failed: ${error instanceof Error ? error.stack : String(error)}`);
|
|
504
|
+
return json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
stop: () => server.stop()
|
|
508
|
+
};
|
|
509
|
+
};
|
|
510
|
+
export {
|
|
511
|
+
controllerSchema,
|
|
512
|
+
createAgent,
|
|
513
|
+
credentialsSchema,
|
|
514
|
+
deployOptionsSchema,
|
|
515
|
+
eventSchema,
|
|
516
|
+
networkInterfaceSchema,
|
|
517
|
+
releaseManifestSchema,
|
|
518
|
+
releaseSchema,
|
|
519
|
+
statusSchema,
|
|
520
|
+
targetSchema,
|
|
521
|
+
variableSchema
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
//# debugId=015E4865253C96F064756E2164756E21
|
|
525
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/agent.ts", "../src/discovery.ts", "../src/protocol.ts", "../src/releases.ts", "../src/server-process.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { execFile } from \"node:child_process\";\nimport { timingSafeEqual } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, readdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { promisify } from \"node:util\";\nimport {\n explicitDownload,\n findControllerLogin,\n gatewayImage,\n loginCommandSchema,\n openControllerLogin,\n resolveConfig,\n updateController,\n} from \"@tsplc/build\";\nimport { z } from \"zod\";\nimport { discoverControllers, networkInterfaces } from \"./discovery\";\nimport {\n type AgentEvent,\n type AgentStatus,\n type Credentials,\n credentialsSchema,\n deployOptionsSchema,\n type Release,\n releaseSchema,\n type Target,\n targetSchema,\n} from \"./protocol\";\nimport { loadRelease, readUpload, saveRelease } from \"./releases\";\nimport { createServerSupervisor } from \"./server-process\";\n\nconst execute = promisify(execFile);\n\n/** Builder containers reach the gateway by name on this Docker network. */\nconst network = \"tsplc-agent\";\nconst gateway = { host: \"tsplc-agent-gateway\", port: 1217 };\nconst loginContainer = \"tsplc-agent-login\";\n/** CODESYS reads the controller user from these for one operation; the agent never stores it. */\nconst credentialNames = {\n usernameEnv: \"TSPLC_DEVICE_USERNAME\",\n passwordEnv: \"TSPLC_DEVICE_PASSWORD\",\n};\n\nclass AgentError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly downloadRequired = false,\n ) {\n super(message);\n }\n}\n\nconst json = (body: unknown, status = 200) => Response.json(body, { status });\n\nconst readJson = async <T>(path: string, schema: z.ZodType<T>) =>\n existsSync(path) ? schema.parse(JSON.parse(await readFile(path, \"utf8\"))) : null;\n\nconst docker = (...args: string[]) => execute(\"docker\", args).then(({ stdout }) => stdout.trim());\n\n/** The gateway container stays up between operations, as a CODESYS gateway service would. */\nconst ensureGateway = async () => {\n if (!(await docker(\"network\", \"ls\", \"--quiet\", \"--filter\", `name=^${network}$`)))\n await docker(\"network\", \"create\", network);\n if (await docker(\"container\", \"ls\", \"--quiet\", \"--filter\", `name=^${gateway.host}$`)) return;\n await docker(\"rm\", \"--force\", gateway.host);\n await docker(\n \"run\",\n \"--detach\",\n \"--restart\",\n \"unless-stopped\",\n \"--platform\",\n \"linux/amd64\",\n \"--network\",\n network,\n \"--name\",\n gateway.host,\n gatewayImage,\n );\n};\n\n/** Hands the controller user to CODESYS through the environment for the duration of `run`. */\nconst withCredentials = async <T>(credentials: Credentials, run: () => Promise<T>) => {\n process.env[credentialNames.usernameEnv] = credentials.username;\n process.env[credentialNames.passwordEnv] = credentials.password;\n try {\n return await run();\n } finally {\n delete process.env[credentialNames.usernameEnv];\n delete process.env[credentialNames.passwordEnv];\n }\n};\n\n/** Relays the newest controller job's progress log, which CODESYS appends to as it works. */\nconst followProgress = (controller: string, emit: (event: AgentEvent) => void) => {\n const jobs = join(controller, \".codesys/controller/jobs\");\n const since = Date.now();\n let following = true;\n const done = (async () => {\n let log: string | undefined;\n let offset = 0;\n while (following) {\n await delay(2000);\n if (!log && existsSync(jobs)) {\n for (const job of await readdir(jobs)) {\n const path = join(jobs, job, \".codesys/controller/progress.log\");\n if (existsSync(path) && (await stat(path)).mtimeMs >= since) log = path;\n }\n }\n if (!log) continue;\n const text = await readFile(log, \"utf8\");\n for (const line of text.slice(offset).split(\"\\n\").filter(Boolean))\n emit({ kind: \"progress\", message: line.replace(/^\\S+Z /, \"\") });\n offset = text.length;\n }\n })();\n return async () => {\n following = false;\n await done;\n };\n};\n\nexport interface AgentOptions {\n /** Releases, the controller's download baseline and the selected target live here. */\n state: string;\n token: string;\n /** The deployed server's health route; without it a server that stays up 5 s counts as started. */\n serverHealthUrl?: string;\n log: (message: string) => void;\n}\n\n/** The plant agent's routes. It runs one deployment or login operation at a time. */\nexport const createAgent = async (options: AgentOptions) => {\n const paths = {\n releases: join(options.state, \"releases\"),\n controller: join(options.state, \"controller\"),\n release: join(options.state, \"release.json\"),\n target: join(options.state, \"target.json\"),\n };\n await mkdir(options.state, { recursive: true });\n const server = createServerSupervisor({ healthUrl: options.serverHealthUrl, log: options.log });\n const deployed = await readJson(paths.release, releaseSchema);\n if (deployed?.server) server.start(join(paths.releases, deployed.server));\n let busy: string | undefined;\n\n const requireTarget = async () => {\n const target = await readJson(paths.target, targetSchema);\n if (!target) throw new AgentError(\"Select the controller first: tsplc target\", 409);\n return target;\n };\n\n /** The release's build config, aimed at `target` through the agent's gateway. */\n const controllerConfig = (release: Awaited<ReturnType<typeof saveRelease>>, target: Target) => ({\n ...resolveConfig(\n {\n ...release.manifest.config,\n online: { network, gateway, device: target, credentials: credentialNames },\n },\n join(release.directory, \"manifest.json\"),\n ),\n // The baseline outlives releases: an online change compares against the last download.\n outDir: paths.controller,\n });\n\n const currentRelease = async () => {\n const release = await readJson(paths.release, releaseSchema);\n if (!release) throw new AgentError(\"Nothing has been deployed yet: tsplc deploy\", 409);\n return loadRelease(paths.releases, release.id);\n };\n\n /** Streams one exclusive operation's events, then its result, as newline-delimited JSON. */\n const operation = (\n name: string,\n run: (emit: (event: AgentEvent) => void) => Promise<unknown>,\n ) => {\n if (busy) throw new AgentError(`The agent is busy with ${busy}`, 409);\n busy = name;\n const encoder = new TextEncoder();\n // A client that disconnects doesn't stop the operation; it just stops hearing about it.\n let listening = true;\n return new Response(\n new ReadableStream({\n start: async (controller) => {\n const write = (text: string) => {\n if (listening) controller.enqueue(encoder.encode(text));\n };\n const emit = (event: AgentEvent) => write(`${JSON.stringify(event)}\\n`);\n // CODESYS can stay silent for minutes; blank lines keep proxies from closing the stream.\n const heartbeat = setInterval(() => write(\"\\n\"), 15_000);\n try {\n emit({ kind: \"done\", result: await run(emit) });\n } catch (error) {\n options.log(`${name} failed: ${error instanceof Error ? error.stack : String(error)}`);\n emit({\n kind: \"error\",\n message: error instanceof Error ? error.message : String(error),\n downloadRequired:\n (error instanceof AgentError && error.downloadRequired) ||\n (error instanceof Error && error.message.includes(explicitDownload)),\n });\n } finally {\n clearInterval(heartbeat);\n busy = undefined;\n if (listening) controller.close();\n }\n },\n cancel: () => {\n listening = false;\n },\n }),\n { headers: { \"Content-Type\": \"application/x-ndjson\" } },\n );\n };\n\n const deploy = async (form: FormData) => {\n const settings = deployOptionsSchema.parse(JSON.parse(z.string().parse(form.get(\"options\"))));\n const upload = await readUpload(form);\n const target = await requireTarget();\n if (await findControllerLogin(loginContainer))\n throw new AgentError(\"A login is open; log out before deploying: tsplc logout\", 409);\n return operation(\"a deployment\", async (emit) => {\n const release = await saveRelease(paths.releases, upload);\n const config = controllerConfig(release, target);\n if (\n !settings.download &&\n !existsSync(join(paths.controller, \".codesys/controller/current.json\"))\n )\n throw new AgentError(\n \"The controller has no application downloaded by this agent; only a full download can deploy\",\n 409,\n true,\n );\n const previous = await readJson(paths.release, releaseSchema);\n // Server paths are relative to the releases directory.\n const serverPath = release.manifest.server && `${release.id}/${release.manifest.server}`;\n if (serverPath) {\n emit({ kind: \"progress\", message: \"Starting the new server\" });\n await server.replace(join(paths.releases, serverPath));\n }\n const kind = settings.download ? \"download\" : \"online-change\";\n emit({ kind: \"progress\", message: `Starting the ${kind}; CODESYS takes minutes to start` });\n await ensureGateway();\n const stopFollowing = followProgress(paths.controller, emit);\n const receipt = await withCredentials(settings.credentials, () =>\n updateController(config, kind, { initializeUser: settings.initializeUser }),\n )\n .catch(async (failure: unknown) => {\n if (serverPath) {\n emit({ kind: \"progress\", message: \"Restoring the previous server\" });\n await (previous?.server\n ? server.replace(join(paths.releases, previous.server))\n : server.stop());\n }\n throw failure;\n })\n .finally(stopFollowing);\n const deployedRelease: Release = {\n id: release.id,\n deployedAt: new Date().toISOString(),\n operation: kind,\n controllerIds: receipt.controllerIds,\n // A release without a server leaves the previous one running.\n server: serverPath ?? previous?.server,\n };\n await writeFile(paths.release, JSON.stringify(deployedRelease, null, 2));\n return deployedRelease;\n });\n };\n\n const openLogin = async (credentials: Credentials) => {\n const release = await currentRelease();\n const config = controllerConfig(release, await requireTarget());\n return operation(\"a login\", async (emit) => {\n if (await findControllerLogin(loginContainer))\n throw new AgentError(\"A login is already open\", 409);\n emit({ kind: \"progress\", message: \"Logging in; CODESYS takes minutes to start\" });\n await ensureGateway();\n const login = await withCredentials(credentials, () =>\n openControllerLogin(config, loginContainer),\n );\n return login.send({ command: \"state\" });\n });\n };\n\n const requireLogin = async () => {\n const login = await findControllerLogin(loginContainer);\n if (!login) throw new AgentError(\"No login is open: tsplc login\", 409);\n return login;\n };\n\n const status = async (): Promise<AgentStatus> => {\n const login = await findControllerLogin(loginContainer);\n // A login still opening has its container but doesn't answer yet.\n const state = login && (await login.ready()) && (await login.send({ command: \"state\" }));\n return {\n target: await readJson(paths.target, targetSchema),\n release: await readJson(paths.release, releaseSchema),\n server: server.status() ?? null,\n login: state && \"state\" in state ? state : null,\n };\n };\n\n const route = async (request: Request): Promise<Response> => {\n const { pathname } = new URL(request.url);\n const key = `${request.method} ${pathname}`;\n switch (key) {\n case \"GET /status\":\n return json(await status());\n case \"GET /interfaces\":\n return json(networkInterfaces());\n case \"POST /discover\": {\n const { name } = z.strictObject({ name: z.string() }).parse(await request.json());\n const selected = networkInterfaces().find((network) => network.name === name);\n if (!selected) throw new AgentError(`No IPv4 interface named ${name}`, 404);\n return json(await discoverControllers(selected));\n }\n case \"PUT /target\": {\n const target = targetSchema.parse(await request.json());\n await writeFile(paths.target, JSON.stringify(target, null, 2));\n return json(target);\n }\n case \"GET /variables\":\n return json((await currentRelease()).manifest.variables);\n case \"POST /deploy\":\n return deploy(await request.formData());\n case \"POST /login\":\n return openLogin(credentialsSchema.parse(await request.json()));\n case \"POST /login/command\":\n return json(\n await (await requireLogin()).send(loginCommandSchema.parse(await request.json())),\n );\n case \"DELETE /login\":\n await (await requireLogin()).close();\n return json({});\n default:\n return json({ error: `No route ${key}` }, 404);\n }\n };\n\n const expected = Buffer.from(`Bearer ${options.token}`);\n return {\n fetch: async (request: Request) => {\n const actual = Buffer.from(request.headers.get(\"authorization\") ?? \"\");\n if (actual.length !== expected.length || !timingSafeEqual(actual, expected))\n return json({ error: \"Unauthorized\" }, 401);\n try {\n return await route(request);\n } catch (error) {\n if (error instanceof AgentError) return json({ error: error.message }, error.status);\n if (error instanceof z.ZodError) return json({ error: z.prettifyError(error) }, 400);\n options.log(\n `${request.method} ${request.url} failed: ${error instanceof Error ? error.stack : String(error)}`,\n );\n return json({ error: error instanceof Error ? error.message : String(error) }, 500);\n }\n },\n /** Stops the supervised server; a login container keeps running for the next agent. */\n stop: () => server.stop(),\n };\n};\n",
|
|
6
|
+
"import { Resolver } from \"node:dns/promises\";\nimport { createConnection } from \"node:net\";\nimport { networkInterfaces as osInterfaces } from \"node:os\";\nimport type { DiscoveredController, NetworkInterface } from \"./protocol\";\n\n/** CODESYS runtimes accept engineering connections on their TCP block driver port. */\nexport const runtimePort = 11740;\n/** A /22 is 1022 hosts: a sweep at 128 probes a time takes a few seconds. */\nconst largestPrefix = 22;\nconst probeTimeoutMs = 500;\nconst probesAtOnce = 128;\n\n/** The machine's IPv4 networks, which `discoverControllers` scans one at a time. */\nexport const networkInterfaces = (): NetworkInterface[] =>\n Object.entries(osInterfaces()).flatMap(([name, addresses]) =>\n (addresses ?? [])\n .filter((address) => address.family === \"IPv4\" && !address.internal)\n .map((address) => ({\n name,\n address: address.address,\n prefixLength: address.netmask\n .split(\".\")\n .reduce((bits, octet) => bits + Number(octet).toString(2).replaceAll(\"0\", \"\").length, 0),\n })),\n );\n\nconst toNumber = (address: string) =>\n address.split(\".\").reduce((value, octet) => value * 256 + Number(octet), 0);\nconst toAddress = (value: number) =>\n [24, 16, 8, 0].map((shift) => Math.floor(value / 2 ** shift) % 256).join(\".\");\n\n/** Every host address on the interface's subnet, except the interface's own. */\nexport const subnetHosts = ({ address, prefixLength }: NetworkInterface) => {\n if (prefixLength < largestPrefix)\n throw new Error(\n `The /${prefixLength} network is too large to scan; the limit is /${largestPrefix}.`,\n );\n const size = 2 ** (32 - prefixLength);\n const network = toNumber(address) - (toNumber(address) % size);\n // /31 and /32 networks have no network or broadcast address to skip.\n const hosts =\n size <= 2\n ? Array.from({ length: size }, (_, index) => network + index)\n : Array.from({ length: size - 2 }, (_, index) => network + index + 1);\n return hosts.map(toAddress).filter((host) => host !== address);\n};\n\nconst accepts = (host: string, port: number) =>\n new Promise<boolean>((resolve) => {\n const socket = createConnection({ host, port });\n // A silent host leaves the connection pending for the OS's own timeout, which is minutes.\n const timer = setTimeout(() => finish(false), probeTimeoutMs);\n const finish = (open: boolean) => {\n clearTimeout(timer);\n socket.destroy();\n resolve(open);\n };\n socket.once(\"connect\", () => finish(true));\n socket.once(\"error\", () => finish(false));\n });\n\nconst resolver = new Resolver({ timeout: 1000, tries: 1 });\nconst hostname = (address: string) =>\n resolver.reverse(address).then(\n (names) => names[0] ?? null,\n // Plant networks rarely have reverse DNS; the address alone identifies the controller.\n () => null,\n );\n\n/** Finds the hosts on `network` that accept CODESYS runtime connections. */\nexport const discoverControllers = async (\n network: NetworkInterface,\n port = runtimePort,\n): Promise<DiscoveredController[]> => {\n const hosts = subnetHosts(network);\n const found: DiscoveredController[] = [];\n for (let start = 0; start < hosts.length; start += probesAtOnce) {\n const batch = hosts.slice(start, start + probesAtOnce);\n const open = await Promise.all(batch.map((host) => accepts(host, port)));\n for (const [index, host] of batch.entries())\n if (open[index]) found.push({ address: host, port, hostname: await hostname(host) });\n }\n return found;\n};\n",
|
|
7
|
+
"import { z } from \"zod\";\n\n/** The controller the agent deploys to, as its runtime's TCP block driver address. */\nexport const targetSchema = z.strictObject({\n host: z.string().nonempty(),\n port: z.number().int().min(1).max(65535),\n});\nexport type Target = z.infer<typeof targetSchema>;\n\nexport const networkInterfaceSchema = z.strictObject({\n name: z.string(),\n address: z.ipv4(),\n prefixLength: z.number().int().min(0).max(32),\n});\nexport type NetworkInterface = z.infer<typeof networkInterfaceSchema>;\n\n/** A host on the scanned subnet that accepts connections on the CODESYS runtime port. */\nexport const controllerSchema = z.strictObject({\n address: z.ipv4(),\n port: z.number().int(),\n hostname: z.string().nullable(),\n});\nexport type DiscoveredController = z.infer<typeof controllerSchema>;\n\n/** The controller's user; the agent passes it to CODESYS for one operation and never stores it. */\nexport const credentialsSchema = z.strictObject({\n username: z.string().nonempty(),\n password: z.string().nonempty(),\n});\nexport type Credentials = z.infer<typeof credentialsSchema>;\n\n/** A published leaf of the deployed program, as CODESYS expressions name it. */\nexport const variableSchema = z.strictObject({ path: z.string(), type: z.string() });\nexport type Variable = z.infer<typeof variableSchema>;\n\n/**\n * What `tsplc deploy` uploads beside its files. `config` is the build config with paths\n * relative to the release: `st/`, `template.project` and `libraries/`.\n */\nexport const releaseManifestSchema = z.strictObject({\n config: z.record(z.string(), z.unknown()),\n variables: z.array(variableSchema),\n /** The server executable's path in the release, when the project builds one. */\n server: z.string().nonempty().optional(),\n});\nexport type ReleaseManifest = z.infer<typeof releaseManifestSchema>;\n\nexport const deployOptionsSchema = z.strictObject({\n credentials: credentialsSchema,\n /** A full download stops the application; without it only an online change is applied. */\n download: z.boolean(),\n initializeUser: z.boolean(),\n});\nexport type DeployOptions = z.infer<typeof deployOptionsSchema>;\n\n/** Long operations stream one of these per line. */\nexport const eventSchema = z.discriminatedUnion(\"kind\", [\n z.strictObject({ kind: z.literal(\"progress\"), message: z.string() }),\n z.strictObject({ kind: z.literal(\"done\"), result: z.unknown() }),\n z.strictObject({\n kind: z.literal(\"error\"),\n message: z.string(),\n /** The controller has no application from this agent yet, so only a full download can deploy. */\n downloadRequired: z.boolean(),\n }),\n]);\nexport type AgentEvent = z.infer<typeof eventSchema>;\n\nexport const releaseSchema = z.strictObject({\n id: z.string(),\n deployedAt: z.iso.datetime(),\n operation: z.enum([\"download\", \"online-change\"]),\n controllerIds: z.strictObject({ codeId: z.string(), dataId: z.string() }),\n /** The running server executable, relative to the releases directory. */\n server: z.string().optional(),\n});\nexport type Release = z.infer<typeof releaseSchema>;\n\nexport const statusSchema = z.strictObject({\n target: targetSchema.nullable(),\n release: releaseSchema.nullable(),\n server: z.strictObject({ executable: z.string(), running: z.boolean() }).nullable(),\n /** The open login's application state and forced expressions. */\n login: z.strictObject({ state: z.string(), forced: z.array(z.string()) }).nullable(),\n});\nexport type AgentStatus = z.infer<typeof statusSchema>;\n",
|
|
8
|
+
"import { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, mkdtemp, readFile, rename, rm, writeFile } from \"node:fs/promises\";\nimport { dirname, join, posix } from \"node:path\";\nimport { z } from \"zod\";\nimport { type ReleaseManifest, releaseManifestSchema } from \"./protocol\";\n\n/** Uploaded files are named `files/<path in the release>`. */\nconst filePrefix = \"files/\";\n\nconst releasePath = (name: string) => {\n const path = posix.normalize(name.slice(filePrefix.length));\n if (posix.isAbsolute(path) || path.startsWith(\"..\") || path.includes(\"\\\\\"))\n throw new Error(`Invalid release file name: ${name}`);\n return path;\n};\n\n/** An uploaded release, from `tsplc deploy`'s form. */\nexport const readUpload = async (form: FormData) => {\n const manifest = releaseManifestSchema.parse(JSON.parse(z.string().parse(form.get(\"manifest\"))));\n const files = await Promise.all(\n [...form.entries()]\n .filter(([name]) => name.startsWith(filePrefix))\n .map(async ([name, value]) => ({\n path: releasePath(name),\n content: new Uint8Array(await z.instanceof(File).parse(value).arrayBuffer()),\n })),\n );\n if (manifest.server && !files.some((file) => file.path === manifest.server))\n throw new Error(`The release names a server ${manifest.server} it doesn't contain`);\n return { manifest, files };\n};\n\n/**\n * Stores a release under `releases/<id>`, where the id hashes its contents, so the same\n * upload twice is one release. Each release is complete before it appears.\n */\nexport const saveRelease = async (\n releases: string,\n { manifest, files }: Awaited<ReturnType<typeof readUpload>>,\n) => {\n const hash = createHash(\"sha256\").update(JSON.stringify(manifest));\n for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path)))\n hash.update(file.path).update(createHash(\"sha256\").update(file.content).digest(\"hex\"));\n const id = hash.digest(\"hex\").slice(0, 16);\n const directory = join(releases, id);\n if (!existsSync(directory)) {\n await mkdir(releases, { recursive: true });\n const staging = await mkdtemp(join(releases, \".upload-\"));\n try {\n for (const file of files) {\n await mkdir(dirname(join(staging, file.path)), { recursive: true });\n // Uploads lose their mode; the server must stay executable outside Windows.\n await writeFile(join(staging, file.path), file.content, {\n mode: file.path === manifest.server ? 0o755 : 0o644,\n });\n }\n await writeFile(join(staging, \"manifest.json\"), JSON.stringify(manifest, null, 2));\n await rename(staging, directory);\n } catch (cause) {\n await rm(staging, { recursive: true, force: true });\n throw cause;\n }\n }\n return { id, directory, manifest };\n};\n\nexport const loadRelease = async (\n releases: string,\n id: string,\n): Promise<{ id: string; directory: string; manifest: ReleaseManifest }> => {\n const directory = join(releases, id);\n const manifest = releaseManifestSchema.parse(\n JSON.parse(await readFile(join(directory, \"manifest.json\"), \"utf8\")),\n );\n return { id, directory, manifest };\n};\n",
|
|
9
|
+
"import type { Subprocess } from \"bun\";\nimport { dirname } from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\n\nconst healthyWithinMs = 60_000;\n/** Without a health URL, a server that stays up this long counts as started. */\nconst settleMs = 5000;\nconst restartDelayMs = 2000;\n\n/**\n * Runs the deployed server executable, restarting it whenever it exits, and swaps it for\n * another release's. The server reads its own environment and secrets, as it would anywhere.\n */\nexport const createServerSupervisor = (options: {\n /** The server's `/health` route, which answers once it is listening. */\n healthUrl?: string;\n log: (message: string) => void;\n}) => {\n let current: { executable: string; process: Subprocess } | undefined;\n\n const spawn = (executable: string): Subprocess => {\n const child = Bun.spawn([executable], {\n cwd: dirname(executable),\n stdio: [\"ignore\", \"inherit\", \"inherit\"],\n onExit: (_, code) => {\n // `stop` and `replace` clear or change `current` before killing a server.\n if (current?.process !== child) return;\n options.log(`Server exited (${code}); restarting it in ${restartDelayMs / 1000} s`);\n void delay(restartDelayMs).then(() => {\n if (current?.process === child) current = { executable, process: spawn(executable) };\n });\n },\n });\n return child;\n };\n\n const start = (executable: string) => {\n current = { executable, process: spawn(executable) };\n return current.process;\n };\n\n const stop = async () => {\n const running = current;\n current = undefined;\n if (!running) return;\n running.process.kill();\n await running.process.exited;\n };\n\n const healthy = async (child: Subprocess) => {\n if (!options.healthUrl) {\n await delay(settleMs);\n return child.exitCode === null;\n }\n const deadline = Date.now() + healthyWithinMs;\n while (Date.now() < deadline && child.exitCode === null) {\n if (\n await fetch(options.healthUrl).then(\n (response) => response.ok,\n () => false,\n )\n )\n return true;\n await delay(1000);\n }\n return false;\n };\n\n return {\n start,\n stop,\n /** Replaces the running server; if the new one doesn't come up, the previous one runs again. */\n replace: async (executable: string) => {\n const previous = current?.executable;\n await stop();\n let failure: unknown;\n const started = await (async () => healthy(start(executable)))().catch((error: unknown) => {\n failure = error;\n return false;\n });\n if (started) return;\n await stop();\n if (previous) start(previous);\n const reason =\n failure instanceof Error\n ? failure.message\n : `it did not start${options.healthUrl ? ` answering ${options.healthUrl}` : \"\"}`;\n throw new Error(\n `The new server failed: ${reason}. ${previous ? \"The previous one is running again.\" : \"\"}`.trim(),\n { cause: failure },\n );\n },\n status: () =>\n current && { executable: current.executable, running: current.process.exitCode === null },\n };\n};\n"
|
|
10
|
+
],
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "015E4865253C96F064756E2164756E21",
|
|
13
|
+
"names": []
|
|
14
|
+
}
|