lasal-mcp 0.1.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/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/core/envelope.js +8 -0
- package/dist/core/errors.js +12 -0
- package/dist/core/http.js +19 -0
- package/dist/core/process.js +30 -0
- package/dist/core/response.js +37 -0
- package/dist/core/scratch.js +6 -0
- package/dist/core/staticServer.js +88 -0
- package/dist/server.js +230 -0
- package/dist/state.js +57 -0
- package/dist/tools/applyProjectChanges.js +371 -0
- package/dist/tools/deployAll.js +320 -0
- package/dist/tools/hmiBrowser.js +224 -0
- package/dist/tools/hmiRuntime.js +273 -0
- package/dist/tools/inspectProject.js +162 -0
- package/dist/tools/inspectVisuProject.js +474 -0
- package/dist/tools/larsRuntime.js +536 -0
- package/dist/tools/lasalApps.js +111 -0
- package/dist/tools/plcControl.js +530 -0
- package/dist/tools/plcDiagnostics.js +172 -0
- package/dist/tools/readClassSource.js +147 -0
- package/dist/tools/selectProject.js +47 -0
- package/dist/tools/setTargetIp.js +114 -0
- package/dist/tools/status.js +146 -0
- package/dist/tools/visuControl.js +447 -0
- package/dist/tools/visuDashboard.js +571 -0
- package/dist/utils/batchScript.js +257 -0
- package/dist/utils/config.js +34 -0
- package/dist/utils/editTransaction.js +39 -0
- package/dist/utils/engine.js +163 -0
- package/dist/utils/lars.js +471 -0
- package/dist/utils/lasalXml.js +758 -0
- package/dist/utils/preflight.js +194 -0
- package/dist/utils/projectScanner.js +212 -0
- package/dist/utils/resolvePaths.js +46 -0
- package/dist/utils/respond.js +14 -0
- package/dist/utils/scriptRunner.js +161 -0
- package/dist/utils/visuDashboardIO.js +198 -0
- package/dist/utils/visuPropertyEncoding.js +174 -0
- package/dist/utils/visuScript.js +262 -0
- package/package.json +64 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { writeFileSync, readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { CLASS2_EXE, SCRATCH, killClass2 } from "./engine.js";
|
|
5
|
+
import { runEngineScript } from "./scriptRunner.js";
|
|
6
|
+
import { ensureScratch } from "../core/scratch.js";
|
|
7
|
+
export function validateMbcsEncodable(s) {
|
|
8
|
+
for (let i = 0; i < s.length; i++) {
|
|
9
|
+
const code = s.charCodeAt(i);
|
|
10
|
+
if (code > 0xff) {
|
|
11
|
+
throw new Error(`String contains character '${s[i]}' (U+${code.toString(16).padStart(4, "0")}) at position ${i} ` +
|
|
12
|
+
`which is not representable in mbcs/latin1. Path or value: "${s}"`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function emitPy27String(s) {
|
|
17
|
+
validateMbcsEncodable(s);
|
|
18
|
+
return `u"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}".encode('mbcs')`;
|
|
19
|
+
}
|
|
20
|
+
export function emitPath(p) {
|
|
21
|
+
return emitPy27String(p);
|
|
22
|
+
}
|
|
23
|
+
function emitPy27StringList(arr) {
|
|
24
|
+
return `[${arr.map(emitPy27String).join(", ")}]`;
|
|
25
|
+
}
|
|
26
|
+
/** Build the body of a batch.py Python 2.7 script from a list of operations. */
|
|
27
|
+
export function buildBatchScript(lcpPath, ops, logPath, stepsPath) {
|
|
28
|
+
const lines = [
|
|
29
|
+
"# -*- coding: utf-8 -*-",
|
|
30
|
+
"import sigmatek.lasal.batch as batch",
|
|
31
|
+
"import sys",
|
|
32
|
+
"import traceback",
|
|
33
|
+
"batch.SetExceptionOnError(True)",
|
|
34
|
+
`batch.OpenLogfile(${emitPath(logPath)})`,
|
|
35
|
+
"try:",
|
|
36
|
+
` prj = batch.LoadProject(${emitPath(lcpPath)})`,
|
|
37
|
+
"",
|
|
38
|
+
];
|
|
39
|
+
const expectedSteps = [];
|
|
40
|
+
for (let i = 0; i < ops.length; i++) {
|
|
41
|
+
const op = ops[i];
|
|
42
|
+
if (!op)
|
|
43
|
+
continue;
|
|
44
|
+
const label = `${i}_${op.type}`;
|
|
45
|
+
expectedSteps.push(label);
|
|
46
|
+
const opLines = [];
|
|
47
|
+
switch (op.type) {
|
|
48
|
+
case "create_network":
|
|
49
|
+
opLines.push(`batch.CreateNetwork(prj, ${emitPy27String(op.name)})`);
|
|
50
|
+
break;
|
|
51
|
+
case "delete_network":
|
|
52
|
+
opLines.push(`batch.DeleteNetwork(prj, ${emitPy27String(op.name)}, ${op.deleteConnections ? "True" : "False"}, False)`);
|
|
53
|
+
break;
|
|
54
|
+
case "rename_network":
|
|
55
|
+
opLines.push(`batch.RenameNetwork(prj, ${emitPy27String(op.oldName)}, ${emitPy27String(op.newName)})`);
|
|
56
|
+
break;
|
|
57
|
+
case "duplicate_network":
|
|
58
|
+
opLines.push(`batch.DuplicateNetwork(prj, ${emitPy27String(op.name)}, ${emitPy27String(op.newName)})`);
|
|
59
|
+
break;
|
|
60
|
+
case "add_object":
|
|
61
|
+
opLines.push(`batch.CreateObject(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.className)}, ${emitPy27String(op.objectName)}, ${op.x ?? 0}, ${op.y ?? 0}, ${op.visualized ? "True" : "False"})`);
|
|
62
|
+
break;
|
|
63
|
+
case "remove_object":
|
|
64
|
+
opLines.push(`batch.DeleteObject(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.objectName)}, ${op.deleteConnections !== false ? "True" : "False"})`);
|
|
65
|
+
break;
|
|
66
|
+
case "rename_object":
|
|
67
|
+
opLines.push(`batch.RenameObject(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.oldName)}, ${emitPy27String(op.newName)})`);
|
|
68
|
+
break;
|
|
69
|
+
case "change_object_class":
|
|
70
|
+
opLines.push(`batch.ChangeClass(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.objectName)}, ${emitPy27String(op.className)}, False)`);
|
|
71
|
+
break;
|
|
72
|
+
case "create_connection": {
|
|
73
|
+
const net = op.network;
|
|
74
|
+
if (net) {
|
|
75
|
+
opLines.push(`batch.CreateConnection(prj, ${emitPy27String(net)}, ${emitPy27String(op.fromObject)}, ${emitPy27String(op.fromClient)}, ${emitPy27String(net)}, ${emitPy27String(op.toObject)}, ${emitPy27String(op.toServer)})`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
opLines.push(`batch.CreateConnection2(prj, ${emitPy27String(op.fromObject)}, ${emitPy27String(op.fromClient)}, ${emitPy27String(op.toObject)}, ${emitPy27String(op.toServer)})`);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case "delete_connection": {
|
|
83
|
+
const net = op.network;
|
|
84
|
+
if (net) {
|
|
85
|
+
opLines.push(`batch.DeleteConnection(prj, ${emitPy27String(net)}, ${emitPy27String(op.objectName)}, ${emitPy27String(op.clientName)})`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
opLines.push(`batch.DeleteConnection2(prj, ${emitPy27String(op.objectName)}, ${emitPy27String(op.clientName)})`);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "set_init_value": {
|
|
93
|
+
const net = op.network;
|
|
94
|
+
if (net) {
|
|
95
|
+
opLines.push(`batch.SetInitValue(prj, ${emitPy27String(net)}, ${emitPy27String(op.objectName)}, ${emitPy27String(op.channelName)}, ${emitPy27String(op.value)})`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
opLines.push(`batch.SetInitValue2(prj, ${emitPy27String(op.objectName)}, ${emitPy27String(op.channelName)}, ${emitPy27String(op.value)})`);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "delete_class":
|
|
103
|
+
opLines.push(`batch.DeleteClass(prj, ${emitPy27String(op.className)}, ${op.force ? "True" : "False"})`);
|
|
104
|
+
break;
|
|
105
|
+
case "compile": {
|
|
106
|
+
const optName = op.optionName ?? "RebuildAll";
|
|
107
|
+
opLines.push(`batch.Compile(prj, batch.CompileOptions.${optName})`);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "download": {
|
|
111
|
+
const conn = op.connection ?? "";
|
|
112
|
+
opLines.push(`batch.Download(prj, ${emitPy27String(conn)}, ${op.addLoaderAnyway ? "True" : "False"}, False)`);
|
|
113
|
+
const stateJsonPath = logPath.replace(/\.log$/, ".state.json");
|
|
114
|
+
opLines.push("state_map = {}", "for attr_name in dir(batch.PLCStates):", " if not attr_name.startswith('_'):", " try: state_map[int(getattr(batch.PLCStates, attr_name))] = attr_name", " except: pass", `state_val = batch.GetPlcState(prj, ${emitPy27String(conn)})`, "state_int = int(state_val)", "state_name = state_map.get(state_int, 'Unknown(%d)' % state_int)", "result = {'stateValue': state_int, 'stateName': state_name}", "import json", `f_state = open(${emitPath(stateJsonPath)}, 'w')`, "json.dump(result, f_state)", "f_state.close()");
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "set_task_order":
|
|
118
|
+
opLines.push(`batch.SetTaskOrder(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.objectName)}, ${emitPy27String(op.task)}, ${emitPy27String(String(op.position))})`);
|
|
119
|
+
break;
|
|
120
|
+
case "set_task_time":
|
|
121
|
+
opLines.push(`batch.SetTaskTime(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.objectName)}, ${emitPy27String(op.task)}, ${emitPy27String(op.time)})`);
|
|
122
|
+
break;
|
|
123
|
+
case "set_task_cpu_core":
|
|
124
|
+
opLines.push(`batch.SetTaskCPUCore(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.objectName)}, ${emitPy27String(op.task)}, ${Number(op.core)})`);
|
|
125
|
+
break;
|
|
126
|
+
case "set_multi_cpu_core":
|
|
127
|
+
opLines.push(`batch.SetMultiCPUCore(prj, ${op.multiCore ? "True" : "False"})`);
|
|
128
|
+
break;
|
|
129
|
+
case "set_visualized_flag":
|
|
130
|
+
opLines.push(`batch.SetVisualizedFlag(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.objectName)}, ${op.isVisualized ? "True" : "False"})`);
|
|
131
|
+
break;
|
|
132
|
+
case "set_comment_network":
|
|
133
|
+
opLines.push(`batch.SetCommentNetwork(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.comment)})`);
|
|
134
|
+
break;
|
|
135
|
+
case "set_comment_object":
|
|
136
|
+
opLines.push(`batch.SetCommentObject(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.objectName)}, ${emitPy27String(op.comment)})`);
|
|
137
|
+
break;
|
|
138
|
+
case "set_network_options":
|
|
139
|
+
opLines.push(`batch.SetNetworkOptions(prj, ${emitPy27String(op.network)}, ${emitPy27StringList(op.optionNames)}, ${op.resetAllOthers ? "True" : "False"})`);
|
|
140
|
+
break;
|
|
141
|
+
case "reset_network_options":
|
|
142
|
+
opLines.push(`batch.ResetNetworkOptions(prj, ${emitPy27String(op.network)}, ${emitPy27StringList(op.optionNames)})`);
|
|
143
|
+
break;
|
|
144
|
+
case "move_network_to_folder":
|
|
145
|
+
opLines.push(`batch.MoveNetworkToFolder(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.folder)})`);
|
|
146
|
+
break;
|
|
147
|
+
case "set_parameter_value":
|
|
148
|
+
opLines.push(`batch.SetParameterValue(prj, ${emitPy27String(op.network)}, ${emitPy27String(op.objectName)}, ${emitPy27String(op.parameterName)}, ${emitPy27String(op.value)})`);
|
|
149
|
+
break;
|
|
150
|
+
case "save":
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
for (const opLine of opLines) {
|
|
154
|
+
lines.push(` ${opLine}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push(` f_step = open(${emitPath(stepsPath)}, "a"); f_step.write("STEP ${label} OK\\n"); f_step.close()`, "");
|
|
157
|
+
}
|
|
158
|
+
lines.push(" batch.Save(prj)", " batch.CloseProject(prj)", "except Exception as e:", " traceback.print_exc()", " sys.exit(1)");
|
|
159
|
+
return {
|
|
160
|
+
script: lines.join("\n") + "\n",
|
|
161
|
+
expectedSteps,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function buildRawScript(lcpPath, bodyLines, logPath, stepsPath, expectedSteps) {
|
|
165
|
+
const lines = [
|
|
166
|
+
"# -*- coding: utf-8 -*-",
|
|
167
|
+
"import sigmatek.lasal.batch as batch",
|
|
168
|
+
"import sys",
|
|
169
|
+
"import json",
|
|
170
|
+
"import traceback",
|
|
171
|
+
"batch.SetExceptionOnError(True)",
|
|
172
|
+
`batch.OpenLogfile(${emitPath(logPath)})`,
|
|
173
|
+
"try:",
|
|
174
|
+
` prj = batch.LoadProject(${emitPath(lcpPath)})`,
|
|
175
|
+
];
|
|
176
|
+
for (const line of bodyLines) {
|
|
177
|
+
lines.push(` ${line}`);
|
|
178
|
+
}
|
|
179
|
+
for (const step of expectedSteps) {
|
|
180
|
+
lines.push(` f = open(${emitPath(stepsPath)}, "a"); f.write("STEP ${step} OK\\n"); f.close()`);
|
|
181
|
+
}
|
|
182
|
+
lines.push(" batch.CloseProject(prj)", "except Exception as e:", " traceback.print_exc()", " sys.exit(1)");
|
|
183
|
+
return lines.join("\n") + "\n";
|
|
184
|
+
}
|
|
185
|
+
export async function runScript(script, logPath, timeoutMs = 120_000, expectedSteps = [], stepsPath) {
|
|
186
|
+
ensureScratch();
|
|
187
|
+
const id = randomUUID();
|
|
188
|
+
const scriptPath = join(SCRATCH, `${id}.py`);
|
|
189
|
+
if (!stepsPath)
|
|
190
|
+
stepsPath = join(SCRATCH, `${id}.steps`);
|
|
191
|
+
writeFileSync(scriptPath, script, "utf-8");
|
|
192
|
+
const command = `"${CLASS2_EXE}" /script:"${scriptPath}"`;
|
|
193
|
+
const result = await runEngineScript(scriptPath, {
|
|
194
|
+
exe: CLASS2_EXE,
|
|
195
|
+
argsFor: (p) => [`/script:${p}`],
|
|
196
|
+
timeoutMs,
|
|
197
|
+
logEncoding: "latin1",
|
|
198
|
+
killOnFailure: killClass2,
|
|
199
|
+
expectedSteps,
|
|
200
|
+
stepsPath,
|
|
201
|
+
}, logPath);
|
|
202
|
+
return {
|
|
203
|
+
ok: result.ok,
|
|
204
|
+
exitCode: result.exitCode,
|
|
205
|
+
logPath: result.logPath,
|
|
206
|
+
errors: result.errors,
|
|
207
|
+
warnings: result.warnings,
|
|
208
|
+
logTail: result.logTail,
|
|
209
|
+
durationMs: result.durationMs,
|
|
210
|
+
command,
|
|
211
|
+
steps: result.steps,
|
|
212
|
+
timedOut: result.timedOut,
|
|
213
|
+
hints: result.hints,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export async function runBatchOps(lcpPath, ops, timeoutMs = 120_000) {
|
|
217
|
+
ensureScratch();
|
|
218
|
+
const id = randomUUID();
|
|
219
|
+
const scriptPath = join(SCRATCH, `${id}.py`);
|
|
220
|
+
const logPath = join(SCRATCH, `${id}.log`);
|
|
221
|
+
const stepsPath = join(SCRATCH, `${id}.steps`);
|
|
222
|
+
const { script, expectedSteps } = buildBatchScript(lcpPath, ops, logPath, stepsPath);
|
|
223
|
+
writeFileSync(scriptPath, script, "utf-8");
|
|
224
|
+
const command = `"${CLASS2_EXE}" /script:"${scriptPath}"`;
|
|
225
|
+
killClass2();
|
|
226
|
+
const result = await runEngineScript(scriptPath, {
|
|
227
|
+
exe: CLASS2_EXE,
|
|
228
|
+
argsFor: (p) => [`/script:${p}`],
|
|
229
|
+
timeoutMs,
|
|
230
|
+
logEncoding: "latin1",
|
|
231
|
+
killOnFailure: killClass2,
|
|
232
|
+
expectedSteps,
|
|
233
|
+
stepsPath,
|
|
234
|
+
}, logPath);
|
|
235
|
+
let postDownloadState;
|
|
236
|
+
const stateJsonPath = logPath.replace(/\.log$/, ".state.json");
|
|
237
|
+
if (existsSync(stateJsonPath)) {
|
|
238
|
+
try {
|
|
239
|
+
postDownloadState = JSON.parse(readFileSync(stateJsonPath, "utf-8"));
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
ok: result.ok,
|
|
245
|
+
exitCode: result.exitCode,
|
|
246
|
+
logPath: result.logPath,
|
|
247
|
+
errors: result.errors,
|
|
248
|
+
warnings: result.warnings,
|
|
249
|
+
logTail: result.logTail,
|
|
250
|
+
durationMs: result.durationMs,
|
|
251
|
+
command,
|
|
252
|
+
steps: result.steps,
|
|
253
|
+
timedOut: result.timedOut,
|
|
254
|
+
hints: result.hints,
|
|
255
|
+
postDownloadState,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const envInt = z.coerce.number().int().positive();
|
|
3
|
+
const ConfigSchema = z.object({
|
|
4
|
+
LASAL_MCP_TIMEOUT_COMPILE: envInt.default(600_000),
|
|
5
|
+
LASAL_MCP_TIMEOUT_DOWNLOAD: envInt.default(600_000),
|
|
6
|
+
LASAL_MCP_TIMEOUT_VISU: envInt.default(300_000),
|
|
7
|
+
LASAL_MCP_TIMEOUT_SCRIPT: envInt.default(120_000),
|
|
8
|
+
LASAL_MCP_HMI_DIR: z.string().default("C:\\lslvisu"),
|
|
9
|
+
LASAL_MCP_SCRATCH_MAX_AGE_H: envInt.default(24),
|
|
10
|
+
LASAL_MCP_LARS_GC_MIN_AGE_H: envInt.default(0),
|
|
11
|
+
LASAL_MCP_LARS_GC_STATIONS_DIRS: z.string().default(""),
|
|
12
|
+
});
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
const raw = {};
|
|
15
|
+
for (const key of ConfigSchema.keyof().options) {
|
|
16
|
+
const val = process.env[key];
|
|
17
|
+
if (val !== undefined)
|
|
18
|
+
raw[key] = val;
|
|
19
|
+
}
|
|
20
|
+
return ConfigSchema.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
const cfg = loadConfig();
|
|
23
|
+
export const TIMEOUTS = {
|
|
24
|
+
compile: cfg.LASAL_MCP_TIMEOUT_COMPILE,
|
|
25
|
+
download: cfg.LASAL_MCP_TIMEOUT_DOWNLOAD,
|
|
26
|
+
visu: cfg.LASAL_MCP_TIMEOUT_VISU,
|
|
27
|
+
script: cfg.LASAL_MCP_TIMEOUT_SCRIPT,
|
|
28
|
+
};
|
|
29
|
+
export const HMI_DIR = cfg.LASAL_MCP_HMI_DIR;
|
|
30
|
+
export const SCRATCH_MAX_AGE_H = cfg.LASAL_MCP_SCRATCH_MAX_AGE_H;
|
|
31
|
+
export const LARS_GC_MIN_AGE_H = cfg.LASAL_MCP_LARS_GC_MIN_AGE_H;
|
|
32
|
+
export const LARS_GC_STATIONS_DIRS = cfg.LASAL_MCP_LARS_GC_STATIONS_DIRS.split(";")
|
|
33
|
+
.map((s) => s.trim())
|
|
34
|
+
.filter(Boolean);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { join, basename } from "path";
|
|
3
|
+
import { SCRATCH } from "./engine.js";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
export class EditTransaction {
|
|
6
|
+
runId = randomUUID();
|
|
7
|
+
backupDir;
|
|
8
|
+
backups = new Map(); // originalPath -> backupPath
|
|
9
|
+
constructor() {
|
|
10
|
+
this.backupDir = join(SCRATCH, `backup-${this.runId}`);
|
|
11
|
+
}
|
|
12
|
+
backup(filePath) {
|
|
13
|
+
if (this.backups.has(filePath))
|
|
14
|
+
return;
|
|
15
|
+
if (!existsSync(filePath))
|
|
16
|
+
return;
|
|
17
|
+
if (!existsSync(this.backupDir)) {
|
|
18
|
+
mkdirSync(this.backupDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const backupFile = join(this.backupDir, `${randomUUID()}-${basename(filePath)}`);
|
|
21
|
+
copyFileSync(filePath, backupFile);
|
|
22
|
+
this.backups.set(filePath, backupFile);
|
|
23
|
+
}
|
|
24
|
+
commit() {
|
|
25
|
+
this.backups.clear();
|
|
26
|
+
}
|
|
27
|
+
rollback() {
|
|
28
|
+
const restored = [];
|
|
29
|
+
for (const [originalPath, backupPath] of this.backups.entries()) {
|
|
30
|
+
try {
|
|
31
|
+
copyFileSync(backupPath, originalPath);
|
|
32
|
+
restored.push(originalPath);
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
}
|
|
36
|
+
this.backups.clear();
|
|
37
|
+
return { restored };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync, unlinkSync } from "fs";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { SCRATCH_MAX_AGE_H } from "./config.js";
|
|
6
|
+
// ─── Executable Paths ────────────────────────────────────────────────────────
|
|
7
|
+
export const VISUDESIGNER_EXE = process.env.LASAL_VISUDESIGNER_EXE || "C:\\Program Files\\Sigmatek\\Lasal\\VISUDesigner\\VISUDesigner.exe";
|
|
8
|
+
export const CLASS2_EXE = process.env.LASAL_CLASS2_EXE || "C:\\Program Files (x86)\\Sigmatek\\Lasal\\Class2\\Bin\\Lasal2.exe";
|
|
9
|
+
function extractVersion(name) {
|
|
10
|
+
const m = name.match(/V(\d+(?:_\d+)*)/);
|
|
11
|
+
if (!m?.[1])
|
|
12
|
+
return [0];
|
|
13
|
+
return m[1].split("_").map(Number);
|
|
14
|
+
}
|
|
15
|
+
function compareVersions(a, b) {
|
|
16
|
+
const len = Math.max(a.length, b.length);
|
|
17
|
+
for (let i = 0; i < len; i++) {
|
|
18
|
+
const diff = (b[i] ?? 0) - (a[i] ?? 0);
|
|
19
|
+
if (diff !== 0)
|
|
20
|
+
return diff;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
function versionSort(names) {
|
|
25
|
+
return [...names].sort((a, b) => compareVersions(extractVersion(a), extractVersion(b)));
|
|
26
|
+
}
|
|
27
|
+
export function resolveDataServiceExe() {
|
|
28
|
+
if (process.env.LASAL_DATASERVICE_EXE) {
|
|
29
|
+
return { path: process.env.LASAL_DATASERVICE_EXE, searched: [] };
|
|
30
|
+
}
|
|
31
|
+
const root = "C:\\Program Files";
|
|
32
|
+
const searched = [];
|
|
33
|
+
if (!existsSync(root)) {
|
|
34
|
+
return { path: "", searched: [`${root} (does not exist)`] };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const dirs = readdirSync(root).filter((d) => d.startsWith("Lasal VISUDesigner V"));
|
|
38
|
+
const sortedDirs = versionSort(dirs);
|
|
39
|
+
for (const dir of sortedDirs) {
|
|
40
|
+
const parentPath = join(root, dir);
|
|
41
|
+
try {
|
|
42
|
+
const subDirs = readdirSync(parentPath).filter((d) => d.startsWith("Lasal VISUDataService V"));
|
|
43
|
+
const sortedSubDirs = versionSort(subDirs);
|
|
44
|
+
for (const subDir of sortedSubDirs) {
|
|
45
|
+
const exePath = join(parentPath, subDir, "Windows", "LasalVISUDataService.exe");
|
|
46
|
+
searched.push(exePath);
|
|
47
|
+
if (existsSync(exePath)) {
|
|
48
|
+
return { path: exePath, searched };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}
|
|
54
|
+
if (dirs.length === 0) {
|
|
55
|
+
searched.push(`${root} (no 'Lasal VISUDesigner V*' directories found)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
return { path: "", searched };
|
|
60
|
+
}
|
|
61
|
+
const _dsResolved = resolveDataServiceExe();
|
|
62
|
+
export const DATASERVICE_EXE = _dsResolved.path;
|
|
63
|
+
export const DATASERVICE_SEARCHED = _dsResolved.searched;
|
|
64
|
+
function findEdgePath() {
|
|
65
|
+
const paths = [
|
|
66
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
67
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
68
|
+
];
|
|
69
|
+
for (const p of paths) {
|
|
70
|
+
if (existsSync(p))
|
|
71
|
+
return p;
|
|
72
|
+
}
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
export const EDGE_EXE = process.env.LASAL_EDGE_EXE || findEdgePath();
|
|
76
|
+
// ─── Scratch Directory & Cleanup ─────────────────────────────────────────────
|
|
77
|
+
export const SCRATCH = join(tmpdir(), "lasal-mcp");
|
|
78
|
+
export function cleanupScratch() {
|
|
79
|
+
if (!existsSync(SCRATCH))
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
const files = readdirSync(SCRATCH);
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const cutoff = SCRATCH_MAX_AGE_H * 60 * 60 * 1000;
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
const filePath = join(SCRATCH, file);
|
|
87
|
+
try {
|
|
88
|
+
const stats = statSync(filePath);
|
|
89
|
+
if (now - stats.mtimeMs > cutoff) {
|
|
90
|
+
unlinkSync(filePath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Ignore single file deletion failures
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Ignore folder reading failures
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ─── Centralized Process Killing & Status ────────────────────────────────────
|
|
103
|
+
export function isProcessRunning(imageName) {
|
|
104
|
+
try {
|
|
105
|
+
const out = execSync(`tasklist /FI "IMAGENAME eq ${imageName}" /NH`, { stdio: "pipe", encoding: "utf-8" });
|
|
106
|
+
return out.toLowerCase().includes(imageName.toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function getProcessPid(imageName) {
|
|
113
|
+
try {
|
|
114
|
+
const out = execSync(`tasklist /FI "IMAGENAME eq ${imageName}" /FO CSV /NH`, { stdio: "pipe", encoding: "utf-8" });
|
|
115
|
+
const m = out.match(/"([^"]+)"\s*,\s*"(\d+)"/);
|
|
116
|
+
if (m && m[1] && m[2] && m[1].toLowerCase() === imageName.toLowerCase()) {
|
|
117
|
+
return parseInt(m[2]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
export function killClass2() {
|
|
124
|
+
try {
|
|
125
|
+
execSync(`taskkill /IM "Lasal2.exe" /F /T`, { stdio: "pipe" });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Not running
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function killVisuDesigner() {
|
|
132
|
+
try {
|
|
133
|
+
execSync(`taskkill /IM "VISUDesigner.exe" /F /T`, { stdio: "pipe" });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Not running
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function killDataService(pid) {
|
|
140
|
+
if (pid) {
|
|
141
|
+
try {
|
|
142
|
+
execSync(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Failed to kill by PID
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
try {
|
|
150
|
+
execSync(`taskkill /IM "LasalVISUDataService.exe" /F /T`, { stdio: "pipe" });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Not running
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ─── Global Engine Mutex ─────────────────────────────────────────────────────
|
|
158
|
+
let currentPromise = Promise.resolve();
|
|
159
|
+
export async function withEngineLock(fn) {
|
|
160
|
+
const next = currentPromise.then(fn);
|
|
161
|
+
currentPromise = next.catch(() => { });
|
|
162
|
+
return next;
|
|
163
|
+
}
|