Skip to content

Commit bca7dcb

Browse files
committed
fix: bound Linux child process execution
Replace the unbounded POSIX system call with fork, exec, wait, and process-group cleanup. Add native timeout regression coverage and document the Linux runtime and optional WSL PATH setup.
1 parent 1a81ac0 commit bca7dcb

7 files changed

Lines changed: 279 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010

1111
- Changed `EncodingFixTool.sh` to launch the native Linux64 executable directly instead of invoking the Windows EXE through WSL interoperability.
1212
- Made Git execution, shell quoting, user config discovery, and path containment platform-correct; Linux avoids treating its UTF-8 default encoding as a legacy ANSI candidate while Windows retains active-code-page compatibility.
13+
- Updated the agent skill with the native Linux runtime layout, optional WSL `PATH` setup, and fresh-shell verification.
14+
15+
### Fixed
16+
17+
- Bounded Linux Git command execution to 30 seconds and terminated the command process group on timeout so child processes are not left running.
1318

1419
## [v1.5] - 2026-06-24
1520

TASKS.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Tasks
22

3-
Next task ID: T-010
3+
Next task ID: T-011
44

55
## Summary
66
Open tasks: 1 (In Progress: 0, Next Today: 0, Next This Week: 0, Next Later: 1, Blocked: 0)
7-
Done tasks: 8
7+
Done tasks: 9
88

99
## In Progress
1010

@@ -14,23 +14,43 @@ Done tasks: 8
1414

1515
## Next - Later
1616

17-
### T-009 [CLI] Bound POSIX child-process execution
17+
### T-010 [BUILD] Repair Delphi 12 command-line Linux64 RTL loading
1818
Outcome:
19-
- Linux command execution enforces the same 30-second upper bound as Windows without depending on an optional external `timeout` command.
20-
- Timeout cleanup terminates and reaps the child process without leaving Git processes behind.
21-
- Native Linux regression coverage verifies successful commands and timeout behavior.
19+
- Delphi 12 command-line Linux64 builds can load the installed `Posix.Unistd.dcu`.
20+
- DelphiAIKit and raw MSBuild rebuild Linux64 projects as reliably as the RAD Studio IDE.
2221
Proof:
23-
- Run: `wsl.exe -e bash tests/Invoke-EncodingFixTool.Linux64.Tests.sh`
24-
Expect: exit=0, timeout regression passes, and no test-owned child process remains.
2522
- Run: `& $env:DAK_EXE build --project src\EncodingFixTool.dproj --delphi 23.0 --platform Linux64 --config Release --target Rebuild --ai --show-warnings`
2623
Expect: exit=0, zero warnings, zero errors.
27-
Touches: src/EncodingFixToolCore.pas, tests/Invoke-EncodingFixTool.Linux64.Tests.sh
28-
Verify: integration-test, build-only
24+
- Run a minimal Linux64 program whose only dependency is `System.SysUtils` through `dcclinux64.exe`.
25+
Expect: compilation reaches the linker instead of failing with F2063 for `Posix.Unistd`.
26+
Touches: Delphi 12 installation or DelphiAIKit build environment
27+
Verify: build-only
28+
Notes: The project search path contains the expected Delphi 12 Linux64 release RTL folder, and namespace variants do not change the failure. A minimal `uses Posix.Unistd` probe fails under `dcclinux64.exe`; `System.SysUtils` fails transitively on the same unit, while other POSIX DCUs load. Raw MSBuild reproduces DAK, so this is not caused by EncodingFixTool conditionals or DAK command construction. RAD Studio succeeds through its in-process Linux64 compiler DLL.
2929

3030
## Blocked
3131

3232
## Done
3333

34+
### T-009 [CLI] Bound POSIX child-process execution
35+
Completed: 2026-07-11
36+
Outcome:
37+
- Linux command execution enforces the same 30-second upper bound as Windows without depending on an optional external `timeout` command.
38+
- Timeout cleanup terminates and reaps the child process group without leaving Git processes behind.
39+
- Native Linux regression coverage verifies successful commands and timeout behavior.
40+
Proof:
41+
- PASS: RAD Studio Linux64 Release rebuild.
42+
Result: zero errors; produced `bin/Linux64/EncodingFixTool` as a native ELF executable.
43+
- PASS: `wsl.exe -e bash tests/Invoke-EncodingFixTool.Linux64.Tests.sh`
44+
Result: exit=0 in 36.2 seconds; timeout regression passed and verified that both the shell and descendant process were gone.
45+
- PASS: `.\bin\EncodingFixTool.Tests.exe`
46+
Result: exit=0, 31 passed, 0 failed, 0 ignored.
47+
- PASS: `powershell -NoProfile -ExecutionPolicy Bypass -File tests\Invoke-EncodingFixTool.Tests.ps1`
48+
Result: exit=0, output contains `EncodingFixTool CLI tests passed.`
49+
- DIAGNOSTIC: DelphiAIKit, raw MSBuild, and a minimal `dcclinux64.exe` probe all fail in the installed command-line toolchain with F2063 for `Posix.Unistd`; tracked separately as T-010.
50+
Touches: src/EncodingFixToolCore.pas, tests/Invoke-EncodingFixTool.Linux64.Tests.sh, agent-skill/encodingfix-delphi-cleanup/
51+
Verify: integration-test, build-only
52+
Notes: Strict RED/GREEN was completed. RED reached the 35-second emergency harness limit with exit 124. GREEN enforces the production timeout internally, terminates the process group, reaps the child, and emits `process timed out`. The IDE-built Linux64 executable supplied the native runtime proof; T-010 isolates the unrelated standalone compiler installation failure.
53+
3454
### T-008 [CLI] Add native Linux64 support
3555
Completed: 2026-07-10
3656
Outcome:

agent-skill/encodingfix-delphi-cleanup/SKILL.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,35 @@ Run EncodingFixTool after AI/editor work may have changed Delphi line endings or
1616

1717
Do not use it for unrelated dirty files, vendored code, non-Delphi cleanup, or as a replacement for the repo's build/test gates. Binary `.dfm` files are safe because EncodingFixTool skips them.
1818

19+
## Runtime Setup
20+
21+
The Linux/WSL runtime consists of both files in this layout:
22+
23+
```text
24+
bin/EncodingFixTool.sh
25+
bin/Linux64/EncodingFixTool
26+
```
27+
28+
The wrapper launches the native Linux64 binary directly. It does not use `EncodingFixTool.exe`, `wslpath`, or WSL Windows interoperability. Keep the wrapper executable and the `Linux64` directory next to it.
29+
30+
Adding `bin` to `PATH` is optional. It is needed only to invoke `EncodingFixTool.sh` by name from any directory; repo-local `./bin/EncodingFixTool.sh` works without it. For Bash login shells, add the native Linux path to `~/.profile`:
31+
32+
```bash
33+
case ":$PATH:" in
34+
*:/absolute/path/to/EncodingFix/bin:*) ;;
35+
*) export PATH="/absolute/path/to/EncodingFix/bin:$PATH" ;;
36+
esac
37+
```
38+
39+
Add the same idempotent entry to `~/.bashrc` when direct non-login interactive shells must resolve it. Non-interactive automation does not reliably read either file; give the runner an explicit `PATH` or use the absolute/repo-local wrapper path.
40+
41+
For a checkout on a mounted Windows drive, convert the directory once when determining that native path, for example `wslpath -u 'F:\projects\MaxLogic\EncodingFix\bin'`. Verify the shell modes configured by our setup before relying on the command:
42+
43+
```bash
44+
command -v EncodingFixTool.sh
45+
EncodingFixTool.sh help >/dev/null
46+
```
47+
1948
## Core Workflow
2049

2150
1. Check the dirty set first:
@@ -28,7 +57,7 @@ If unrelated user changes are dirty, avoid broad cleanup. Narrow `path=...`, ove
2857

2958
2. In a Git worktree, prefer the AI cleanup preset.
3059

31-
From Linux or WSL, run the Bash wrapper. It launches the native Linux64 binary directly, so Linux paths are passed through unchanged and WSL Windows interoperability is not involved. If the repo `bin` directory is on `PATH`, prefer:
60+
From Linux or WSL, run the Bash wrapper. Linux paths are passed through unchanged. If the repo `bin` directory is on `PATH`, prefer:
3261

3362
```bash
3463
EncodingFixTool.sh path=. preset=delphi-ai scope=git-changed format=json

agent-skill/encodingfix-delphi-cleanup/evals/evals.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"id": 3,
1616
"prompt": "The worktree has my unrelated changes plus your generated Delphi units. Fix only your files' line endings and tell me what happened.",
1717
"expected_output": "The agent should not run a broad scope=git-changed cleanup over all dirty files. It should narrow path or extensions, ask if needed, run dry when scope is unclear, and report scanned/changed/skipped/failed counts."
18+
},
19+
{
20+
"id": 4,
21+
"prompt": "Set up EncodingFixTool for an agent running natively in WSL without Windows interop. Explain whether PATH is required and verify the installation.",
22+
"expected_output": "The agent should keep bin/EncodingFixTool.sh beside bin/Linux64/EncodingFixTool, explain that PATH is optional unless the wrapper must run by name, use a native Linux bin path in ~/.profile for login shells and ~/.bashrc for non-login interactive Bash when requested, require an explicit path or PATH for non-interactive automation, and verify command resolution plus help output."
1823
}
1924
]
2025
}

agent-skill/encodingfix-delphi-cleanup/references/encodingfix-tool.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ Use this reference when the standard `preset=delphi-ai scope=git-changed` workfl
66

77
Prefer `EncodingFixTool` from `PATH` on Windows or `EncodingFixTool.sh` on Linux/WSL. Repo-local paths are `.\bin\EncodingFixTool.exe` on Windows and `./bin/EncodingFixTool.sh` on Linux/WSL. The shell launcher executes `bin/Linux64/EncodingFixTool` directly and does not use Windows interoperability.
88

9+
On Linux/WSL, the wrapper and binary are one runtime bundle:
10+
11+
```text
12+
bin/EncodingFixTool.sh
13+
bin/Linux64/EncodingFixTool
14+
```
15+
16+
`PATH` is optional. Add the absolute native `bin` path to `~/.profile` for login shells and to `~/.bashrc` for direct non-login interactive Bash shells. Non-interactive automation should receive an explicit `PATH` or use the absolute/repo-local wrapper path. Verify configured shells with `command -v EncodingFixTool.sh` followed by `EncodingFixTool.sh help`.
17+
918
## Delphi AI Preset
1019

1120
`preset=delphi-ai` is designed for AI/editor cleanup:
@@ -88,4 +97,4 @@ EncodingFixTool path=. preset=agent scope=git-changed format=json
8897

8998
## Install Or Copy
9099

91-
To install this as an agent skill, copy the `encodingfix-delphi-cleanup/` folder into the agent's skills directory. Keep `SKILL.md`, `references/`, and `evals/` together.
100+
To install this as an agent skill, copy the `encodingfix-delphi-cleanup/` folder into the agent's skills directory. Keep `SKILL.md`, `references/`, and `evals/` together. The skill files do not install the CLI runtime: install or build the Windows executable separately, and on Linux/WSL keep `EncodingFixTool.sh` together with `Linux64/EncodingFixTool` as shown above.

src/EncodingFixToolCore.pas

Lines changed: 146 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
interface
2424

2525
uses
26-
System.Character, System.Classes, System.Diagnostics, System.Generics.Collections, System.IOUtils, System.StrUtils,
26+
System.Character, System.Classes, System.Generics.Collections, System.IOUtils, System.StrUtils,
2727
System.SyncObjs, System.SysUtils, System.Threading;
2828

2929
type
@@ -100,16 +100,75 @@ TSafeConsole = class
100100
implementation
101101

102102
uses
103-
System.JSON,
103+
System.Diagnostics, System.JSON,
104104
System.WideStrUtils,
105-
{$IFDEF POSIX}
106-
Posix.Stdlib,
107-
{$ELSE}
105+
{$IFDEF MSWINDOWS}
108106
Winapi.Windows,
109107
{$ENDIF}
110108
AutoFree;
111109

110+
{$IFDEF POSIX}
111+
type
112+
TPosixTimespec = record
113+
Seconds: Int64;
114+
Nanoseconds: Int64;
115+
end;
116+
112117
const
118+
cLibC = 'libc.so.6';
119+
cPosixInterrupted = 4;
120+
cPosixNoHang = 1;
121+
cPosixSignalKill = 9;
122+
123+
function PosixClockGetTime(aClockId: integer; aTime: Pointer): integer; cdecl;
124+
external cLibC name 'clock_gettime';
125+
function PosixErrorLocation: PInteger; cdecl; external cLibC name '__errno_location';
126+
function PosixExecv(aPath: PAnsiChar; aArgv: Pointer): integer; cdecl; external cLibC name 'execv';
127+
procedure PosixExit(aStatus: integer); cdecl; external cLibC name '_exit';
128+
function PosixFork: integer; cdecl; external cLibC name 'fork';
129+
function PosixKill(aPid, aSignal: integer): integer; cdecl; external cLibC name 'kill';
130+
function PosixSetProcessGroup(aPid, aProcessGroup: integer): integer; cdecl; external cLibC name 'setpgid';
131+
function PosixUSleep(aMicroseconds: Cardinal): integer; cdecl; external cLibC name 'usleep';
132+
function PosixWaitPid(aPid: integer; aStatus: PInteger; aOptions: integer): integer; cdecl;
133+
external cLibC name 'waitpid';
134+
135+
function PosixLastError: integer;
136+
begin
137+
Result := PosixErrorLocation^;
138+
end;
139+
140+
function PosixMonotonicMilliseconds: Int64;
141+
var
142+
lTime: TPosixTimespec;
143+
begin
144+
if PosixClockGetTime(1, @lTime) <> 0 then
145+
begin
146+
raise Exception.Create('clock_gettime failed');
147+
end;
148+
Result := (lTime.Seconds * 1000) + (lTime.Nanoseconds div 1000000);
149+
end;
150+
151+
function PosixWaitForChild(aPid: integer; out aStatus: integer): boolean;
152+
var
153+
lWaitPid: integer;
154+
begin
155+
repeat
156+
lWaitPid := PosixWaitPid(aPid, @aStatus, 0);
157+
if lWaitPid = aPid then
158+
begin
159+
Exit(True);
160+
end;
161+
if (lWaitPid < 0) and (PosixLastError <> cPosixInterrupted) then
162+
begin
163+
Exit(False);
164+
end;
165+
until False;
166+
end;
167+
{$ENDIF}
168+
169+
const
170+
cCommandPollMs = 10;
171+
cCommandTimeoutMs = 30000;
113172
cUtf8Bom0 = Byte($EF);
114173
cUtf8Bom1 = Byte($BB);
115174
cUtf8Bom2 = Byte($BF);
@@ -378,7 +437,16 @@ function TEncodingFixTool.RunCommand(const aCommandLine, aWorkingDirectory: stri
378437
lStartupInfo: TStartupInfo;
379438
lWaitResult: Cardinal;
380439
{$ELSE}
440+
lArguments: array[0..3] of PAnsiChar;
381441
lCommandLine: UTF8String;
442+
lPid: integer;
443+
lReaped: boolean;
444+
lShellArg: UTF8String;
445+
lShellName: UTF8String;
446+
lShellPath: UTF8String;
447+
lStartTime: Int64;
448+
lStatus: integer;
449+
lWaitPid: integer;
382450
{$ENDIF}
383451
begin
384452
{$IFDEF MSWINDOWS}
@@ -394,7 +462,7 @@ function TEncodingFixTool.RunCommand(const aCommandLine, aWorkingDirectory: stri
394462
RaiseLastOSError;
395463
end;
396464
try
397-
lWaitResult := WaitForSingleObject(lProcessInformation.hProcess, 30000);
465+
lWaitResult := WaitForSingleObject(lProcessInformation.hProcess, cCommandTimeoutMs);
398466
if lWaitResult <> WAIT_OBJECT_0 then
399467
begin
400468
TerminateProcess(lProcessInformation.hProcess, 1);
@@ -411,7 +479,78 @@ function TEncodingFixTool.RunCommand(const aCommandLine, aWorkingDirectory: stri
411479
end;
412480
{$ELSE}
413481
lCommandLine := UTF8String('cd ' + QuoteShellArg(aWorkingDirectory) + ' && ' + aCommandLine);
414-
Result := _system(PAnsiChar(lCommandLine));
482+
lShellArg := UTF8String('-c');
483+
lShellName := UTF8String('sh');
484+
lShellPath := UTF8String('/bin/sh');
485+
lArguments[0] := PAnsiChar(lShellName);
486+
lArguments[1] := PAnsiChar(lShellArg);
487+
lArguments[2] := PAnsiChar(lCommandLine);
488+
lArguments[3] := nil;
489+
lPid := PosixFork;
490+
if lPid < 0 then
491+
begin
492+
raise Exception.Create('fork failed');
493+
end;
494+
if lPid = 0 then
495+
begin
496+
if PosixSetProcessGroup(0, 0) <> 0 then
497+
begin
498+
PosixExit(126);
499+
end;
500+
PosixExecv(PAnsiChar(lShellPath), @lArguments[0]);
501+
PosixExit(127);
502+
end;
503+
504+
lReaped := False;
505+
try
506+
PosixSetProcessGroup(lPid, lPid);
507+
lStartTime := PosixMonotonicMilliseconds;
508+
repeat
509+
lWaitPid := PosixWaitPid(lPid, @lStatus, cPosixNoHang);
510+
if lWaitPid = lPid then
511+
begin
512+
lReaped := True;
513+
Break;
514+
end;
515+
if lWaitPid < 0 then
516+
begin
517+
if PosixLastError = cPosixInterrupted then
518+
begin
519+
Continue;
520+
end;
521+
raise Exception.Create('waitpid failed');
522+
end;
523+
if PosixMonotonicMilliseconds - lStartTime >= cCommandTimeoutMs then
524+
begin
525+
if PosixKill(-lPid, cPosixSignalKill) <> 0 then
526+
begin
527+
PosixKill(lPid, cPosixSignalKill);
528+
end;
529+
if not PosixWaitForChild(lPid, lStatus) then
530+
begin
531+
raise Exception.Create('waitpid failed after timeout');
532+
end;
533+
lReaped := True;
534+
raise Exception.Create('process timed out');
535+
end;
536+
PosixUSleep(cCommandPollMs * 1000);
537+
until False;
538+
539+
if (lStatus and $7F) = 0 then
540+
Result := (lStatus shr 8) and $FF
541+
else
542+
Result := 128 + (lStatus and $7F);
543+
finally
544+
if not lReaped then
545+
begin
546+
PosixKill(-lPid, cPosixSignalKill);
547+
PosixKill(lPid, cPosixSignalKill);
548+
if not PosixWaitForChild(lPid, lStatus) then
549+
begin
550+
raise Exception.Create('waitpid cleanup failed');
551+
end;
552+
end;
553+
end;
415554
{$ENDIF}
416555
end;
417556

0 commit comments

Comments
 (0)