Skip to content

Commit 11d130c

Browse files
committed
updates to the process and window library, added more tests
1 parent 9e2e8e4 commit 11d130c

9 files changed

Lines changed: 1014 additions & 2 deletions

File tree

lang/libs/process/posix/posix.ch

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public func posix_execute(cfg : *ProcessConfig, out : *mut ProcessResult) : bool
8585
public func posix_spawn(cfg : *ProcessConfig, child : *mut ChildProcess) : bool {
8686
unsafe var stdout_pipe : [2]int;
8787
unsafe var stderr_pipe : [2]int;
88+
unsafe var stdin_pipe : [2]int;
8889

8990
if(cfg.capture_stdout) {
9091
if(pipe(&raw mut stdout_pipe[0]) != 0) { return false } else {}
@@ -95,29 +96,39 @@ public func posix_spawn(cfg : *ProcessConfig, child : *mut ChildProcess) : bool
9596
return false
9697
} else {}
9798
} else {}
99+
// Always create a stdin pipe so write_stdin/close_stdin work.
100+
if(pipe(&raw mut stdin_pipe[0]) != 0) {
101+
if(cfg.capture_stdout) { close(stdout_pipe[0]); close(stdout_pipe[1]); } else {}
102+
if(cfg.capture_stderr) { close(stderr_pipe[0]); close(stderr_pipe[1]); } else {}
103+
return false
104+
} else {}
98105

99106
var pid = fork();
100107
if(pid == -1) {
101108
if(cfg.capture_stdout) { close(stdout_pipe[0]); close(stdout_pipe[1]); } else {}
102109
if(cfg.capture_stderr) { close(stderr_pipe[0]); close(stderr_pipe[1]); } else {}
110+
close(stdin_pipe[0]); close(stdin_pipe[1]);
103111
return false
104112
} else {}
105113

106114
if(pid == 0) {
107115
if(cfg.capture_stdout) { close(stdout_pipe[0]); dup2(stdout_pipe[1], 1); close(stdout_pipe[1]); } else {}
108116
if(cfg.capture_stderr) { close(stderr_pipe[0]); dup2(stderr_pipe[1], 2); close(stderr_pipe[1]); } else {}
117+
// Child reads from stdin_pipe[0]; parent writes to stdin_pipe[1].
118+
close(stdin_pipe[1]); dup2(stdin_pipe[0], 0); close(stdin_pipe[0]);
109119
var argv = build_argv(&raw mut cfg.args);
110120
execvp(argv.ptrs[0], &raw argv.ptrs[0]);
111121
_exit(1);
112122
} else {}
113123

114124
if(cfg.capture_stdout) { close(stdout_pipe[1]); } else {}
115125
if(cfg.capture_stderr) { close(stderr_pipe[1]); } else {}
126+
close(stdin_pipe[0]); // parent writes to stdin_pipe[1]
116127

117128
child._unix.pid = pid;
118129
child._unix.stdout_fd = if(cfg.capture_stdout) stdout_pipe[0] else -1;
119130
child._unix.stderr_fd = if(cfg.capture_stderr) stderr_pipe[0] else -1;
120-
child._unix.stdin_fd = -1;
131+
child._unix.stdin_fd = stdin_pipe[1];
121132
child.is_running = true;
122133
return true
123134
}
@@ -142,6 +153,11 @@ public func posix_wait(child : *mut ChildProcess, out : *mut ProcessResult) : bo
142153
close(child._unix.stderr_fd);
143154
child._unix.stderr_fd = -1;
144155
} else {}
156+
// Close stdin pipe if still open.
157+
if(child._unix.stdin_fd >= 0) {
158+
close(child._unix.stdin_fd);
159+
child._unix.stdin_fd = -1;
160+
} else {}
145161

146162
var status : int = 0;
147163
waitpid(child._unix.pid, &raw mut status, 0);
@@ -207,6 +223,8 @@ func read_all_fd(fd : int, data : *mut vector<u8>) : bool {
207223
@extern public func close(fd : int) : int
208224
@extern public func read(fd : int, buf : *mut void, count : size_t) : isize
209225
@extern public func write(fd : int, buf : *void, count : size_t) : isize
226+
@extern public func getpid() : int
227+
@extern public func usleep(usec : int) : int
210228

211229
const _WIFEXITED_MASK = 0x7f;
212230
func WIFEXITED(status : int) : bool { return (status & _WIFEXITED_MASK) == 0; }

lang/libs/process/src/process.ch

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ public struct ProcessResult {
2323
var output : ProcessOutput;
2424
var status : ExitStatus;
2525
var success : bool;
26+
27+
/// Get stdout as a string view (assumes UTF-8).
28+
public func stdout_str(&self) : string_view {
29+
return string_view(self.output.stdout_data.data() as *char, self.output.stdout_data.size())
30+
}
31+
32+
/// Get stderr as a string view (assumes UTF-8).
33+
public func stderr_str(&self) : string_view {
34+
return string_view(self.output.stderr_data.data() as *char, self.output.stderr_data.size())
35+
}
36+
37+
/// Get the exit code.
38+
public func exit_code(&self) : int {
39+
return self.status.code
40+
}
41+
42+
/// Check whether the process exited successfully.
43+
public func is_success(&self) : bool {
44+
return self.success
45+
}
2646
}
2747

2848
public struct ProcessConfig {
@@ -165,7 +185,7 @@ public func kill(child : *mut ChildProcess, signal : int) : UT_Result {
165185
var r = TerminateProcess(child.win.h_process, 1u32);
166186
if(r == 0) {
167187
var e = ProcessError.OperationFailed(string("TerminateProcess failed"))
168-
std::replace(&mut ret, Result.Err<UnitTy, ProcessError>(std::replace(&mut e, ProcessError.NotRunning())))
188+
std::replace(&mut ret, Result.Err<UnitTy, ProcessError>(std::replace<ProcessError>(&mut e, ProcessError.NotRunning())))
169189
return std::replace<UT_Result>(&mut ret, zeroed:unsafe<UT_Result>())
170190
} else {}
171191
child.is_running = false;
@@ -178,4 +198,147 @@ public func kill(child : *mut ChildProcess, signal : int) : UT_Result {
178198
}
179199
}
180200

201+
// ---------------------------------------------------------------------------
202+
// Process status & I/O
203+
// ---------------------------------------------------------------------------
204+
205+
/// Non-blocking check whether a child process is still running.
206+
public func is_running(child : *mut ChildProcess) : bool {
207+
if(!child.is_running) {
208+
return false
209+
}
210+
comptime if(def.windows) {
211+
// WaitForSingleObject with 0 timeout = non-blocking poll.
212+
var rc = WaitForSingleObject(child.win.h_process, 0u32)
213+
if(rc == 0u32) {
214+
// Process has exited.
215+
child.is_running = false
216+
return false
217+
}
218+
return true
219+
} else {
220+
var status : int = 0
221+
var ret = waitpid(child._unix.pid, &raw mut status, 1) // WNOHANG = 1
222+
if(ret == 0) {
223+
return true // still running
224+
}
225+
child.is_running = false
226+
return false
227+
}
228+
}
229+
230+
/// Write data to the child process's stdin pipe.
231+
/// Returns Err if the child has no stdin pipe or the write fails.
232+
public func write_stdin(child : *mut ChildProcess, data : *vector<u8>) : UT_Result {
233+
var ret = zeroed:unsafe<UT_Result>()
234+
comptime if(def.windows) {
235+
var e = ProcessError.OperationFailed(string("write_stdin not implemented on Windows"))
236+
std::replace(&mut ret, Result.Err<UnitTy, ProcessError>(std::replace<ProcessError>(&mut e, ProcessError.NotRunning())))
237+
return std::replace<UT_Result>(&mut ret, zeroed:unsafe<UT_Result>())
238+
} else {
239+
if(child._unix.stdin_fd < 0) {
240+
var e = ProcessError.InvalidArgs(string("no stdin pipe"))
241+
std::replace(&mut ret, Result.Err<UnitTy, ProcessError>(std::replace<ProcessError>(&mut e, ProcessError.NotRunning())))
242+
return std::replace<UT_Result>(&mut ret, zeroed:unsafe<UT_Result>())
243+
}
244+
if(data.size() > 0) {
245+
var written = write(child._unix.stdin_fd, data.data() as *void, data.size())
246+
if(written < 0) {
247+
var e = ProcessError.IoError(string("write to stdin failed"))
248+
std::replace(&mut ret, Result.Err<UnitTy, ProcessError>(std::replace<ProcessError>(&mut e, ProcessError.NotRunning())))
249+
return std::replace<UT_Result>(&mut ret, zeroed:unsafe<UT_Result>())
250+
}
251+
}
252+
std::replace(&mut ret, Result.Ok<UnitTy, ProcessError>(UnitTy{}))
253+
return std::replace<UT_Result>(&mut ret, zeroed:unsafe<UT_Result>())
254+
}
255+
}
256+
257+
/// Close the stdin pipe of a child process (sends EOF to the child).
258+
public func close_stdin(child : *mut ChildProcess) : UT_Result {
259+
var ret = zeroed:unsafe<UT_Result>()
260+
comptime if(def.windows) {
261+
var e = ProcessError.OperationFailed(string("close_stdin not implemented on Windows"))
262+
std::replace(&mut ret, Result.Err<UnitTy, ProcessError>(std::replace<ProcessError>(&mut e, ProcessError.NotRunning())))
263+
return std::replace<UT_Result>(&mut ret, zeroed:unsafe<UT_Result>())
264+
} else {
265+
if(child._unix.stdin_fd >= 0) {
266+
close(child._unix.stdin_fd)
267+
child._unix.stdin_fd = -1
268+
}
269+
std::replace(&mut ret, Result.Ok<UnitTy, ProcessError>(UnitTy{}))
270+
return std::replace<UT_Result>(&mut ret, zeroed:unsafe<UT_Result>())
271+
}
272+
}
273+
274+
/// Reap the child process and return its exit status without blocking.
275+
/// If the process is still running, returns NotRunning error.
276+
public func try_wait(child : *mut ChildProcess) : PR_Result {
277+
var ret = zeroed:unsafe<PR_Result>()
278+
if(!child.is_running) {
279+
var e = ProcessError.NotRunning()
280+
pr_err(e, &mut ret)
281+
return std::replace<PR_Result>(&mut ret, zeroed:unsafe<PR_Result>())
282+
}
283+
comptime if(def.windows) {
284+
var e = ProcessError.OperationFailed(string("try_wait not implemented on Windows"))
285+
pr_err(e, &mut ret)
286+
return std::replace<PR_Result>(&mut ret, zeroed:unsafe<PR_Result>())
287+
} else {
288+
var status : int = 0
289+
var pid = waitpid(child._unix.pid, &raw mut status, 1) // WNOHANG
290+
if(pid == 0) {
291+
var e = ProcessError.NotRunning()
292+
pr_err(e, &mut ret)
293+
return std::replace<PR_Result>(&mut ret, zeroed:unsafe<PR_Result>())
294+
}
295+
var result = zeroed:unsafe<ProcessResult>()
296+
var exit_code : int = 0
297+
var signaled : bool = false
298+
var signal_no : int = 0
299+
if(WIFEXITED(status)) { exit_code = WEXITSTATUS(status) }
300+
else if(WIFSIGNALED(status)) { signaled = true; signal_no = WTERMSIG(status); exit_code = -1 }
301+
child.is_running = false
302+
result.status.code = exit_code
303+
result.status.signaled = signaled
304+
result.status.signal = signal_no
305+
result.success = (exit_code == 0 && !signaled)
306+
pr_ok(&mut result, &mut ret)
307+
return std::replace<PR_Result>(&mut ret, zeroed:unsafe<PR_Result>())
308+
}
309+
}
310+
311+
// ---------------------------------------------------------------------------
312+
// Process-wide utilities
313+
// ---------------------------------------------------------------------------
314+
315+
/// Get the current process ID.
316+
public func current_pid() : int {
317+
comptime if(def.windows) {
318+
return GetCurrentProcessId() as int
319+
} else {
320+
return getpid()
321+
}
322+
}
323+
324+
/// Sleep for the given number of milliseconds.
325+
public func sleep_ms(ms : int) {
326+
comptime if(def.windows) {
327+
Sleep(ms as u32)
328+
} else {
329+
usleep(ms * 1000)
330+
}
331+
}
332+
333+
/// Get the PID of a child process.
334+
public func child_pid(child : *mut ChildProcess) : int {
335+
comptime if(def.windows) {
336+
// Windows doesn't expose a PID from the HANDLE in a portable way;
337+
// return 0 as a sentinel.
338+
return 0
339+
} else {
340+
return child._unix.pid
341+
}
342+
}
343+
181344
} // end namespace process

lang/libs/process/src/types.ch

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public variant ProcessError {
88
OperationFailed(msg : string)
99
NotRunning()
1010
TimedOut()
11+
IoError(msg : string)
1112

1213
func message(&self) : string {
1314
switch(self) {
@@ -23,6 +24,11 @@ public variant ProcessError {
2324
}
2425
NotRunning() => return string("ProcessError: process is not running")
2526
TimedOut() => return string("ProcessError: operation timed out")
27+
IoError(msg) => {
28+
var s = string("ProcessError: I/O error: ")
29+
s.append_view(msg.to_view())
30+
return s
31+
}
2632
}
2733
}
2834
}

lang/libs/process/win/win.ch

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,19 @@ public namespace process {
1717
@stdcall
1818
public func TerminateProcess(hProcess : HANDLE, uExitCode : UINT) : BOOL;
1919

20+
@dllimport
21+
@extern
22+
@stdcall
23+
public func WaitForSingleObject(hHandle : HANDLE, dwMilliseconds : DWORD) : DWORD;
24+
25+
@dllimport
26+
@extern
27+
@stdcall
28+
public func GetCurrentProcessId() : DWORD;
29+
30+
@dllimport
31+
@extern
32+
@stdcall
33+
public func Sleep(dwMilliseconds : DWORD) : void;
34+
2035
} // end namespace process

lang/libs/window/posix/linux.ch

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ const GDK_ACTION_COPY = 2
193193
@extern public func gtk_widget_add_events(widget : *mut GtkWidget, events : int)
194194

195195
@extern public func g_signal_connect_data(instance : *mut void, signal : *char, handler : *mut void, data : *mut void, destroy_data : *mut void, connect_flags : int) : u64
196+
@extern public func g_idle_add(function : *mut void, data : *mut void) : u32
196197

197198
// GObject object-data — used for the stable per-widget callback context
198199
// (g_object_set_data_full auto-frees the context when the widget is destroyed)
@@ -1044,4 +1045,15 @@ public func window_quit() {
10441045
}
10451046
}
10461047

1048+
func linux_empty_callback(data : *mut void) : int {
1049+
// Return 0 so GTK removes the idle source after firing once.
1050+
return 0
1051+
}
1052+
1053+
/// Post an empty event to wake up the message loop.
1054+
/// Useful for waking up the message loop from another thread.
1055+
public func window_post_empty_event() {
1056+
g_idle_add(linux_empty_callback as *mut void, null)
1057+
}
1058+
10471059
} // end namespace window

lang/libs/window/win/win.ch

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ func win_set_layered_alpha(hwnd : HWND, alpha : BYTE) {
297297
@extern @stdcall @dllimport public func GetSystemMetrics(nIndex : int) : int
298298
// NOTE: the SDK header maps LoadImage -> LoadImageW (the real user32 export).
299299
@extern @stdcall @dllimport public func LoadImageW(hInstance : HINSTANCE, name : LPCWSTR, type : UINT, cx : int, cy : int, fuLoad : UINT) : HANDLE
300+
@extern @stdcall @dllimport public func PostMessageA(hwnd : HWND, msg : UINT, wp : WPARAM, lp : LPARAM) : BOOL
300301

301302
// shell32 (drag & drop)
302303
@extern @stdcall @dllimport public func DragAcceptFiles(hwnd : HWND, fAccept : BOOL) : void
@@ -1064,4 +1065,23 @@ public func window_quit() {
10641065
PostQuitMessage(0)
10651066
}
10661067

1068+
/// Returns 1 if the last window_run() returned because a window was destroyed
1069+
/// (the user closed it) rather than because window_quit() was called.
1070+
/// On Windows this mirrors the Linux backend's g_quit_by_destroy flag.
1071+
public func window_quit_by_destroy() : int {
1072+
// On Windows, WM_DESTROY always calls PostQuitMessage which exits the
1073+
// message loop, so the distinction between "user closed" and
1074+
// "window_quit()" is not tracked separately. Return 0 for now — the
1075+
// webview library guards its destroy path with window_is_created() checks
1076+
// which is sufficient on this platform.
1077+
return 0
1078+
}
1079+
1080+
/// Post a empty message to the window's message queue.
1081+
/// Useful for waking up the message loop from another thread.
1082+
public func window_post_empty_event() {
1083+
// PostMessage with WM_NULL is the standard way to wake a message loop.
1084+
PostMessageA(null, 0u32, 0, 0)
1085+
}
1086+
10671087
} // end namespace window

lang/tests/process/chemical.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ import test
77
import test_env
88
import process
99
import environment
10+
import window

0 commit comments

Comments
 (0)