@tsplc/build 0.1.0 → 0.2.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/dist/cli.js +57 -13
- package/dist/cli.js.map +8 -8
- package/dist/index.js +146 -15
- package/dist/index.js.map +7 -7
- package/docker/Dockerfile.dockerignore +1 -0
- package/docker/edge-gateway/Dockerfile +17 -0
- package/docker/runner.ts +31 -6
- package/package.json +3 -3
- package/python/build.py +6 -1
- package/python/codesys_login.py +100 -0
- package/python/codesys_online.py +47 -20
- package/python/test_codesys_login.py +120 -0
- package/python/test_codesys_online.py +14 -0
- package/python/worker.py +13 -0
- package/src/controller-online.ts +170 -16
- package/src/controller-session.ts +72 -3
- package/src/diagnostics.ts +2 -0
- package/src/docker-build.ts +15 -0
- package/src/docker-run.ts +3 -4
- package/src/host/codesys-worker.ts +9 -6
- package/src/index.ts +19 -2
|
@@ -3,7 +3,7 @@ import { copyFile, cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/p
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import type { ResolvedConfig } from "./config";
|
|
6
|
-
import { controllerResultSchema } from "./controller-online";
|
|
6
|
+
import { controllerResultSchema, explicitDownload, loginCommandSchema } from "./controller-online";
|
|
7
7
|
import {
|
|
8
8
|
createCodesysWorker,
|
|
9
9
|
withWorkerLock,
|
|
@@ -74,12 +74,12 @@ const readBaseline = async (
|
|
|
74
74
|
JSON.stringify(baseline.target) !== JSON.stringify(expected.target)
|
|
75
75
|
) {
|
|
76
76
|
throw new Error(
|
|
77
|
-
|
|
77
|
+
`Downloaded baseline uses a different image, template or controller; ${explicitDownload}`,
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
const project = resolve(previous, "project");
|
|
81
81
|
if (JSON.stringify(await projectFiles(project)) !== JSON.stringify(baseline.files)) {
|
|
82
|
-
throw new Error(
|
|
82
|
+
throw new Error(`Saved online project/reference files changed; ${explicitDownload}`);
|
|
83
83
|
}
|
|
84
84
|
return { baseline, project };
|
|
85
85
|
};
|
|
@@ -227,3 +227,72 @@ export const runControllerSession = async (
|
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Called inside the builder container: holds one login to the running application, made
|
|
233
|
+
* from the committed baseline, and serves its commands on `port` until a DELETE closes it.
|
|
234
|
+
*/
|
|
235
|
+
export const serveControllerLogin = async (
|
|
236
|
+
config: ResolvedConfig,
|
|
237
|
+
options: { imageId: string; port: number },
|
|
238
|
+
) => {
|
|
239
|
+
if (!config.online) {
|
|
240
|
+
throw new Error("Controller logins require online settings");
|
|
241
|
+
}
|
|
242
|
+
const root = resolve(config.outDir, ".codesys/controller");
|
|
243
|
+
const { network, gateway, device } = config.online;
|
|
244
|
+
const { baseline, project } = await readBaseline(root, {
|
|
245
|
+
imageId: options.imageId,
|
|
246
|
+
templateHash: sha256(await readFile(config.template)),
|
|
247
|
+
target: { network, gateway, device, controller: config.controller },
|
|
248
|
+
});
|
|
249
|
+
const jobConfig = { ...config, outDir: resolve(root, "login") };
|
|
250
|
+
await rm(jobConfig.outDir, { recursive: true, force: true });
|
|
251
|
+
// CODESYS writes into an open project, and the committed baseline must not change.
|
|
252
|
+
await cp(project, resolve(jobConfig.outDir, "project"), { recursive: true });
|
|
253
|
+
const worker = createCodesysWorker(jobConfig);
|
|
254
|
+
await withWorkerShutdown(jobConfig, worker, () =>
|
|
255
|
+
withWorkerLock(jobConfig, async () => {
|
|
256
|
+
const session = await worker.launch();
|
|
257
|
+
const send = (command: Record<string, unknown>) =>
|
|
258
|
+
worker.request(session, { action: "login", arguments: command }, config.timeouts.build);
|
|
259
|
+
await send({
|
|
260
|
+
command: "open",
|
|
261
|
+
project: await toWindowsPath(
|
|
262
|
+
resolve(jobConfig.outDir, "project", "Application.project"),
|
|
263
|
+
config.codesys.wine,
|
|
264
|
+
),
|
|
265
|
+
online: config.online,
|
|
266
|
+
baseline,
|
|
267
|
+
});
|
|
268
|
+
const closed = Promise.withResolvers<void>();
|
|
269
|
+
// The worker has one mailbox, so commands run one at a time.
|
|
270
|
+
let previous: Promise<unknown> = Promise.resolve();
|
|
271
|
+
const server = Bun.serve({
|
|
272
|
+
port: options.port,
|
|
273
|
+
idleTimeout: 0,
|
|
274
|
+
fetch: async (request) => {
|
|
275
|
+
if (request.method === "GET") return Response.json({ ok: true });
|
|
276
|
+
const close = request.method === "DELETE";
|
|
277
|
+
const command = close
|
|
278
|
+
? { command: "close" }
|
|
279
|
+
: loginCommandSchema.parse(await request.json());
|
|
280
|
+
const result = previous.then(() => send(command));
|
|
281
|
+
previous = result.catch(() => undefined);
|
|
282
|
+
try {
|
|
283
|
+
return Response.json({ ok: true, result: await result });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// The worker reports a Python traceback; its last line names the failure.
|
|
286
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
+
const reason = message.trim().split("\n").at(-1) ?? message;
|
|
288
|
+
return Response.json({ ok: false, error: reason.replace(/^\w+: /, "") });
|
|
289
|
+
} finally {
|
|
290
|
+
if (close) closed.resolve();
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
await closed.promise;
|
|
295
|
+
await server.stop();
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
};
|
package/src/diagnostics.ts
CHANGED
|
@@ -41,6 +41,8 @@ export class CodesysCompilerError extends Error {
|
|
|
41
41
|
|
|
42
42
|
// A failed container job publishes diagnostics separately from its successful build receipt.
|
|
43
43
|
export const buildFailureFile = ".codesys/docker/failure.json";
|
|
44
|
+
/** Why the last download, online change or inspection failed, for the host to report. */
|
|
45
|
+
export const controllerFailureFile = ".codesys/controller/failure.txt";
|
|
44
46
|
export const buildFailureSchema = z.strictObject({
|
|
45
47
|
operationId: z.uuid(),
|
|
46
48
|
message: z.string(),
|
package/src/docker-build.ts
CHANGED
|
@@ -90,6 +90,19 @@ export const configureRuntime = async () => {
|
|
|
90
90
|
return runtimeImage;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
export const gatewayImage = "tsplc-edge-gateway:4.22.0.0";
|
|
94
|
+
|
|
95
|
+
/** Build the CODESYS Edge Gateway that builder containers reach a plant controller through. */
|
|
96
|
+
export const configureGateway = async () => {
|
|
97
|
+
const context = resolve(packageDirectory, "docker/edge-gateway");
|
|
98
|
+
console.log(`Preparing the CODESYS gateway (${gatewayImage}).`);
|
|
99
|
+
await runDocker(
|
|
100
|
+
["build", "--platform", "linux/amd64", "--progress", "plain", "-t", gatewayImage, context],
|
|
101
|
+
(status) => `Gateway image build failed (${status}). Review the image-build output`,
|
|
102
|
+
);
|
|
103
|
+
return gatewayImage;
|
|
104
|
+
};
|
|
105
|
+
|
|
93
106
|
export const buildInDocker = async (config: ResolvedConfig, image: string) => {
|
|
94
107
|
const requestDirectory = await mkdtemp(resolve(tmpdir(), "codesys-docker-"));
|
|
95
108
|
try {
|
|
@@ -98,6 +111,8 @@ export const buildInDocker = async (config: ResolvedConfig, image: string) => {
|
|
|
98
111
|
const failurePath = resolve(config.outDir, buildFailureFile);
|
|
99
112
|
await rm(failurePath, { force: true });
|
|
100
113
|
const args = [
|
|
114
|
+
"run",
|
|
115
|
+
"--rm",
|
|
101
116
|
...(await prepareContainerRun(config, requestDirectory, { network: "none", sources: true })),
|
|
102
117
|
image,
|
|
103
118
|
"--request",
|
package/src/docker-run.ts
CHANGED
|
@@ -56,8 +56,9 @@ async function mountLibraries(libraries: ResolvedConfig["libraries"], directory:
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Write the builder container's request and return the `docker run` options that precede
|
|
59
|
-
* the image
|
|
60
|
-
* and host-only Docker, CODESYS and runtime settings
|
|
59
|
+
* the image; the caller chooses `--rm` or `--detach`. The request names only container paths;
|
|
60
|
+
* host paths enter through these mounts, and host-only Docker, CODESYS and runtime settings
|
|
61
|
+
* stay outside.
|
|
61
62
|
*/
|
|
62
63
|
export const prepareContainerRun = async (
|
|
63
64
|
config: ResolvedConfig,
|
|
@@ -83,8 +84,6 @@ export const prepareContainerRun = async (
|
|
|
83
84
|
}),
|
|
84
85
|
);
|
|
85
86
|
return [
|
|
86
|
-
"run",
|
|
87
|
-
"--rm",
|
|
88
87
|
"--init",
|
|
89
88
|
...(config.docker?.cpuSet ? ["--cpuset-cpus", config.docker.cpuSet] : []),
|
|
90
89
|
"--network",
|
|
@@ -30,7 +30,9 @@ const sessionSchema = z.object({
|
|
|
30
30
|
|
|
31
31
|
type Session = z.infer<typeof sessionSchema>;
|
|
32
32
|
|
|
33
|
-
type WorkerCommand =
|
|
33
|
+
type WorkerCommand =
|
|
34
|
+
| { action: "build" | "online" | "ping" | "stop" }
|
|
35
|
+
| { action: "login"; arguments: Record<string, unknown> };
|
|
34
36
|
|
|
35
37
|
const hasCode = (error: unknown, code: string) =>
|
|
36
38
|
error instanceof Error && "code" in error && error.code === code;
|
|
@@ -170,6 +172,7 @@ export const createCodesysWorker = (config: ResolvedConfig) => {
|
|
|
170
172
|
diagnostics: z.array(compilerDiagnosticSchema).optional(),
|
|
171
173
|
id: z.literal(id),
|
|
172
174
|
ok: z.boolean(),
|
|
175
|
+
result: z.unknown(),
|
|
173
176
|
})
|
|
174
177
|
.safeParse(result);
|
|
175
178
|
if (!response.success) {
|
|
@@ -185,7 +188,7 @@ export const createCodesysWorker = (config: ResolvedConfig) => {
|
|
|
185
188
|
}
|
|
186
189
|
throw new Error(message);
|
|
187
190
|
}
|
|
188
|
-
return;
|
|
191
|
+
return response.data.result;
|
|
189
192
|
}
|
|
190
193
|
if (!isProcessAlive(session.pid)) {
|
|
191
194
|
throw new Error(
|
|
@@ -216,6 +219,7 @@ export const createCodesysWorker = (config: ResolvedConfig) => {
|
|
|
216
219
|
JSON.stringify({
|
|
217
220
|
builder: await readFile(resolve(pythonDirectory, "build.py"), "utf8"),
|
|
218
221
|
online: await readFile(resolve(pythonDirectory, "codesys_online.py"), "utf8"),
|
|
222
|
+
login: await readFile(resolve(pythonDirectory, "codesys_login.py"), "utf8"),
|
|
219
223
|
command: codesysCommand,
|
|
220
224
|
codesys: config.codesys,
|
|
221
225
|
launcher: getHostCommand(config.codesys),
|
|
@@ -242,10 +246,9 @@ export const createCodesysWorker = (config: ResolvedConfig) => {
|
|
|
242
246
|
await mkdir(directory, { recursive: true });
|
|
243
247
|
await copyFile(resolve(pythonDirectory, "build.py"), resolve(directory, "build.py"));
|
|
244
248
|
await copyFile(resolve(pythonDirectory, "worker.py"), resolve(directory, "worker.py"));
|
|
245
|
-
|
|
246
|
-
resolve(pythonDirectory,
|
|
247
|
-
|
|
248
|
-
);
|
|
249
|
+
for (const script of ["codesys_online.py", "codesys_login.py"]) {
|
|
250
|
+
await copyFile(resolve(pythonDirectory, script), resolve(directory, script));
|
|
251
|
+
}
|
|
249
252
|
console.log(`Starting CODESYS. Log: ${directory}/codesys.log`);
|
|
250
253
|
const pid = await host.startWorker(directory);
|
|
251
254
|
const session = { directory, fingerprint, pid };
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,19 @@ export {
|
|
|
8
8
|
type ResolvedConfig,
|
|
9
9
|
resolveConfig,
|
|
10
10
|
} from "./config";
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
type ControllerLogin,
|
|
13
|
+
type ControllerResult,
|
|
14
|
+
explicitDownload,
|
|
15
|
+
findControllerLogin,
|
|
16
|
+
inspectController,
|
|
17
|
+
type LoginCommand,
|
|
18
|
+
loginCommandSchema,
|
|
19
|
+
type LoginResult,
|
|
20
|
+
loginResultSchema,
|
|
21
|
+
openControllerLogin,
|
|
22
|
+
updateController,
|
|
23
|
+
} from "./controller-online";
|
|
12
24
|
export { type CodesysCompilerDiagnostic, CodesysCompilerError } from "./diagnostics";
|
|
13
25
|
export { runProject } from "./run";
|
|
14
26
|
export { loadSources, type SourceObject } from "./sources";
|
|
@@ -22,5 +34,10 @@ export {
|
|
|
22
34
|
type InstalledLibrary,
|
|
23
35
|
} from "./libraries.ts";
|
|
24
36
|
|
|
25
|
-
export {
|
|
37
|
+
export {
|
|
38
|
+
configureDocker,
|
|
39
|
+
configureGateway,
|
|
40
|
+
configureRuntime,
|
|
41
|
+
gatewayImage,
|
|
42
|
+
} from "./docker-build.ts";
|
|
26
43
|
export { readBuild } from "./artifacts.ts";
|