fix(ui): implement async-signal-safe fork process layout and waitpid child zombie reaping for PTY

This commit is contained in:
2026-07-16 23:11:37 +09:00
parent f52f6eb2af
commit 7781e797aa
4 changed files with 51 additions and 20 deletions
@@ -44,6 +44,12 @@ typedef _read_dart = int Function(int fd, ffi.Pointer<ffi.Void> buf, int count);
typedef _write_c = ffi.IntPtr Function(ffi.Int32 fd, ffi.Pointer<ffi.Void> buf, ffi.IntPtr count);
typedef _write_dart = int Function(int fd, ffi.Pointer<ffi.Void> buf, int count);
typedef _waitpid_c = ffi.Int32 Function(ffi.Int32 pid, ffi.Pointer<ffi.Int32> status, ffi.Int32 options);
typedef _waitpid_dart = int Function(int pid, ffi.Pointer<ffi.Int32> status, int options);
typedef _chdir_c = ffi.Int32 Function(ffi.Pointer<ffi.Char> path);
typedef _chdir_dart = int Function(ffi.Pointer<ffi.Char> path);
base class Winsize extends ffi.Struct {
@ffi.Uint16()
external int ws_row;
@@ -90,8 +96,9 @@ class PtySession {
final dup2 = libc.lookupFunction<_dup2_c, _dup2_dart>('dup2');
final execvp = libc.lookupFunction<_execvp_c, _execvp_dart>('execvp');
final ioctl = libc.lookupFunction<_ioctl_c, _ioctl_dart>('ioctl');
final chdir = libc.lookupFunction<_chdir_c, _chdir_dart>('chdir');
// 1. Open master PTY (O_RDWR = 2, O_NOCTTY = 0x00000400)
// 1. Prepare master PTY
final masterFd = posixOpenpt(2 | 0x00000400);
if (masterFd < 0) {
throw OSError('Failed to open pseudo-terminal master');
@@ -119,25 +126,46 @@ class PtySession {
env.remove('TMUX');
env.remove('TMUX_PANE');
// 3. Fork child process
// 3. Pre-allocate ALL structures on the heap before fork() for async-signal-safety (§6.7)
final pathPtr = slaveName.toNativeUtf8();
final exePtr = executable.toNativeUtf8();
final workDirPtr = workingDirectory?.toNativeUtf8();
final argsCount = arguments.length;
final argv = malloc<ffi.Pointer<ffi.Char>>(argsCount + 2);
argv[0] = exePtr.cast<ffi.Char>();
final allocatedArgs = <ffi.Pointer<Utf8>>[];
for (var i = 0; i < argsCount; i++) {
final ptr = arguments[i].toNativeUtf8();
allocatedArgs.add(ptr);
argv[i + 1] = ptr.cast<ffi.Char>();
}
argv[argsCount + 1] = ffi.Pointer.fromAddress(0);
// 4. Fork child process
final pid = fork();
if (pid < 0) {
close(masterFd);
malloc.free(pathPtr);
malloc.free(exePtr);
if (workDirPtr != null) malloc.free(workDirPtr);
malloc.free(argv);
for (final p in allocatedArgs) {
malloc.free(p);
}
throw OSError('Failed to fork child process');
}
if (pid == 0) {
// --- CHILD PROCESS BRANCH ---
// --- CHILD PROCESS BRANCH (Strictly async-signal-safe syscalls only!) ---
setsid();
final pathPtr = slaveName.toNativeUtf8();
final slaveFd = open(pathPtr.cast<ffi.Char>(), 2);
malloc.free(pathPtr);
if (slaveFd < 0) {
exit(-1);
}
// TIOCSCTTY = 0x540E
ioctl(slaveFd, 0x540E, ffi.Pointer.fromAddress(0));
dup2(slaveFd, 0);
@@ -149,25 +177,23 @@ class PtySession {
close(slaveFd);
}
if (workingDirectory != null) {
Directory.current = workingDirectory;
if (workDirPtr != null) {
chdir(workDirPtr.cast<ffi.Char>());
}
final exePtr = executable.toNativeUtf8();
final argsCount = arguments.length;
final argv = malloc<ffi.Pointer<ffi.Char>>(argsCount + 2);
argv[0] = exePtr.cast<ffi.Char>();
for (var i = 0; i < argsCount; i++) {
argv[i + 1] = arguments[i].toNativeUtf8().cast<ffi.Char>();
}
argv[argsCount + 1] = ffi.Pointer.fromAddress(0);
execvp(exePtr.cast<ffi.Char>(), argv);
exit(-2);
}
// --- PARENT PROCESS BRANCH ---
malloc.free(pathPtr);
malloc.free(exePtr);
if (workDirPtr != null) malloc.free(workDirPtr);
malloc.free(argv);
for (final p in allocatedArgs) {
malloc.free(p);
}
return PtySession._(
masterFd: masterFd,
slaveName: slaveName,
@@ -246,6 +272,12 @@ class PtySession {
ffi.Int32 Function(ffi.Int32 pid, ffi.Int32 sig),
int Function(int pid, int sig)
>('kill');
kill(childPid, 15);
kill(childPid, 15); // SIGTERM = 15
// Reap child zombie process (non-blocking WNOHANG = 1)
final waitpid = libc.lookupFunction<_waitpid_c, _waitpid_dart>('waitpid');
final statusPtr = calloc<ffi.Int32>();
waitpid(childPid, statusPtr, 1);
calloc.free(statusPtr);
}
}
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:mam_pty/mam_pty.dart';
import 'package:test/test.dart';