Previous guard refused any CODESYS.exe in tasklist regardless of which
exe path the launcher was configured for. This broke the multi-install
setup the README documents (codesys-21 + codesys-22 entries are
supposed to coexist), and refused to launch any time the user had a
manual CODESYS window open from a different install.
Different CODESYS installs (e.g. SP21 + SP22) are designed to run in
parallel -- they're separate processes, separate IPC, separate file
locks. The only genuine conflict is two instances of the SAME exe
trying to attach to the SAME .project file (CODESYS pops 'project is
currently in use'). The same-exe case can't share IPC with us anyway
since we didn't spawn it.
Implementation:
- New findRunningCodesys() returns [{pid, exePath}] via PowerShell
Get-Process (tasklist doesn't expose ExecutablePath; WMIC is
deprecated on modern Windows).
- pathsEqual() exported helper: case-insensitive, slash-normalised,
trims trailing separators.
- Spawn-guard now filters by pathsEqual(p.exePath, config.codesysPath).
Refusal message names the conflicting exe and PIDs explicitly.
- shutdown_codesys orphan-killer also filters by exe path so we never
kill a CODESYS instance the user owns or that belongs to a different
MCP entry.
Tests:
- 6 new pathsEqual cases (identical / case-insensitive / slash-mix /
trailing-sep / different installs / different drives).
- detect test for the new --print-config caveat copy (no longer
warns 'only one at a time'; warns about same-.project conflict).
- 58/58 pass.
Also updates --print-config CAVEAT in src/detect.ts to reflect that
multiple entries can be active simultaneously, with the only hard rule
being don't open the same .project from two CODESYS instances.
New flags:
- --print-config: scan installs and emit a JSON block per install with
derived server names (codesys-sp21-patch5, codesys-sp22-patch1, etc.)
- --sp <n>: filter to one SP family; collapses entry name to 'codesys'
when exactly one install matches
- --name <name>: override the entry name (only valid with --sp narrowing
to one)
Side effect: --detect now reuses the same detector and additionally
prints the derived profile name + suggested server entry name per
install, so even users sticking to manual config get the values
without guessing.
Refactored install discovery into src/detect.ts so both --detect and
--print-config share one implementation. New unit test fixture covers
version parsing, missing-exe, dedup, sort order, --sp filter behaviour,
--name override constraints, and verifies the emitted JSON parses back
once // comments are stripped.
The output also surfaces the multi-install caveat from launcher.ts:
the double-spawn guard refuses to start a second CODESYS.exe even on
a different exe path, so only one configured entry can be active at
a time.
create_folder v2 (positional foldername) returned None silently against
the SP22 Application object -- no exception raised, no folder created.
Investigation showed:
- ScriptObject.create_folder(foldername) is documented to "create a
folder in the structured view of the parent node", but on Application
specifically it's a silent no-op (the structured view isn't pinned to
POU view there).
- ScriptProject.create_folder(foldername, structured_view=None) on the
project itself with explicit SV_POU GUID
({21AF5390-2942-461a-BF89-951AAF6999F1}) is the documented and
reliable pathway -- the resulting folder appears under Application
in the IDE tree because that's where SV_POU lives.
v3 fix: try strategies in order until one returns non-None:
(1) primary_project.create_folder(name, SV_POU_GUID) -- new, primary
(2) parent.create_folder(name) positional -- pre-SP21 path
(3) parent.create_folder(foldername=name) -- alt keyword
(4) primary_project.create_folder(name) -- default view
(5) parent.create_object(typeUuid='85d1215e-...') -- alt factory
(6) parent.add(script_engine.types.IecFolder, name=name) -- legacy
Each strategy guards on hasattr + return-value-not-None, so a silent
no-op falls through instead of being mistaken for success.
ScriptManager: dropped the in-memory template cache. Each loadTemplate
call now reads the .py from disk fresh. Cost: ~1ms per call vs ~1.5s
of CODESYS execution time -- invisible. Win: edits to dist/scripts/
take effect without an MCP restart, which makes iterating on script-
side fixes (like this very create_folder loop) much faster. Existing
"cache hit" unit test rewritten as "two loads return equal content".
tests/test-fixes.mjs: standalone harness that drives a single persistent
CODESYS through HeadlessExecutor + CodesysLauncher to verify the four
broken-tool fixes end-to-end. Useful for regression-testing without
needing a vsc reboot loop. Currently only smoke-tests
create_folder + compile + cross-project; expand as more fixes need
verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.3.0 returning-watcher architecture — background thread polls for commands
and marshals execution onto the CODESYS UI thread, keeping the IDE fully
responsive between operations. File-based IPC with atomic writes, async mutex
command serialization, headless fallback, and 35 passing tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>