harden PTY FFI layer for macOS

Platform-dispatched constants where Linux and macOS diverge:
TIOCSWINSZ, O_NONBLOCK, MsghdrDarwin struct (4-byte msg_iovlen
and msg_controllen vs Linux's 8-byte size_t fields), and split
recvmsg into Linux/Darwin typed variants so scm_rights uses the
correct struct per platform.

Also: app settings dir uses ~/Library/Application Support on
macOS, removed hardcoded TERMINFO from pane spawn env, removed
debug print from native_pty resize.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-03 21:52:13 +02:00
co-authored by Claude
parent 9c7ec008dc
commit 943ab966d2
6 changed files with 141 additions and 101 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"exported_at": "2026-05-03T19:51:59Z", "exported_at": "2026-05-03T19:52:13Z",
"decisions": [ "decisions": [
{ {
"id": "D-1", "id": "D-1",
+25 -18
View File
@@ -78,22 +78,24 @@ Future<void> main() async {
preloadNamespaces: _tier0Namespaces, preloadNamespaces: _tier0Namespaces,
autoStartDaemonClient: false, autoStartDaemonClient: false,
toolchain: toolchain, toolchain: toolchain,
daemonClientFactory: kIsWeb ? null : (log, events) { daemonClientFactory: kIsWeb
final dispatcher = DaemonDispatcher(); ? null
final eventSink = _BusEventSink(events); : (log, events) {
final filesService = FilesService.atCwd(events: eventSink); final dispatcher = DaemonDispatcher();
final workRoot = filesService.root; final eventSink = _BusEventSink(events);
final paneRegistry = PaneRegistry(events: eventSink); final filesService = FilesService.atCwd(events: eventSink);
registerPaneCommands(dispatcher, paneRegistry); final workRoot = filesService.root;
registerFilesCommands(dispatcher, filesService); final paneRegistry = PaneRegistry(events: eventSink);
final editorRegistry = EditorRegistry(events: eventSink, workspaceRoot: workRoot); registerPaneCommands(dispatcher, paneRegistry);
registerEditorCommands(dispatcher, editorRegistry); registerFilesCommands(dispatcher, filesService);
final gitClient = GitClient(toolchain: toolchain, workDir: workRoot); final editorRegistry = EditorRegistry(events: eventSink, workspaceRoot: workRoot);
registerGitCommands(dispatcher, gitClient, eventSink); registerEditorCommands(dispatcher, editorRegistry);
final pql = PqlClient(workDir: workRoot, toolchain: toolchain); final gitClient = GitClient(toolchain: toolchain, workDir: workRoot);
registerPqlCommands(dispatcher, pql); registerGitCommands(dispatcher, gitClient, eventSink);
return InProcessClient(log: log, events: events, dispatcher: dispatcher); final pql = PqlClient(workDir: workRoot, toolchain: toolchain);
}, registerPqlCommands(dispatcher, pql);
return InProcessClient(log: log, events: events, dispatcher: dispatcher);
},
); );
// Register every built-in. Tier 0 activates only the four that do // Register every built-in. Tier 0 activates only the four that do
@@ -169,8 +171,13 @@ class _BusEventSink implements DaemonEventSink {
Future<Directory> _resolveAppDir() async { Future<Directory> _resolveAppDir() async {
if (kIsWeb) return Directory('/clide-web-no-disk'); if (kIsWeb) return Directory('/clide-web-no-disk');
final home = Platform.environment['HOME'] ?? '/tmp'; final home = Platform.environment['HOME'] ?? '/tmp';
final xdg = Platform.environment['XDG_CONFIG_HOME'] ?? '$home/.config'; final String base;
final dir = Directory('$xdg/clide'); if (Platform.isMacOS) {
base = '$home/Library/Application Support';
} else {
base = Platform.environment['XDG_CONFIG_HOME'] ?? '$home/.config';
}
final dir = Directory('$base/clide');
await dir.create(recursive: true); await dir.create(recursive: true);
return dir; return dir;
} }
-1
View File
@@ -56,7 +56,6 @@ class PaneRegistry {
'COLORTERM': 'truecolor', 'COLORTERM': 'truecolor',
'LANG': 'en_US.UTF-8', 'LANG': 'en_US.UTF-8',
'LC_ALL': 'en_US.UTF-8', 'LC_ALL': 'en_US.UTF-8',
'TERMINFO': '/usr/share/terminfo',
if (env != null) ...env, if (env != null) ...env,
}; };
+40 -15
View File
@@ -16,7 +16,7 @@ import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as pkg_ffi; import 'package:ffi/ffi.dart' as pkg_ffi;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Constants (POSIX / Linux) // Constants (POSIX — platform-dispatched where Linux/macOS diverge)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const int afUnix = 1; const int afUnix = 1;
@@ -25,11 +25,11 @@ const int sockStream = 1;
final int solSocket = Platform.isMacOS ? 0xffff : 1; final int solSocket = Platform.isMacOS ? 0xffff : 1;
final int scmRights = Platform.isMacOS ? 0x01 : 1; final int scmRights = Platform.isMacOS ? 0x01 : 1;
const int fIoNonblock = 0x800; // O_NONBLOCK — 04000 octal final int oNonblock = Platform.isMacOS ? 0x0004 : 0x0800;
const int fGetFl = 3; const int fGetFl = 3;
const int fSetFl = 4; const int fSetFl = 4;
const int tiocswinsz = 0x5414; // Linux x86_64; macOS differs final int tiocswinsz = Platform.isMacOS ? 0x80087467 : 0x5414;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Typedefs // Typedefs
@@ -59,6 +59,17 @@ typedef _RecvmsgD = int Function(
int flags, int flags,
); );
typedef _RecvmsgDarwinC = ffi.IntPtr Function(
ffi.Int32 sockfd,
ffi.Pointer<MsghdrDarwin> msg,
ffi.Int32 flags,
);
typedef _RecvmsgDarwinD = int Function(
int sockfd,
ffi.Pointer<MsghdrDarwin> msg,
int flags,
);
typedef _ReadC = ffi.IntPtr Function( typedef _ReadC = ffi.IntPtr Function(
ffi.Int32 fd, ffi.Int32 fd,
ffi.Pointer<ffi.Uint8> buf, ffi.Pointer<ffi.Uint8> buf,
@@ -116,8 +127,8 @@ final class Iovec extends ffi.Struct {
external int iov_len; external int iov_len;
} }
/// POSIX `struct msghdr`. Field layout matches Linux/glibc; macOS is /// Linux `struct msghdr`. msg_iovlen/msg_controllen are size_t (8 bytes
/// byte-compatible here. /// on 64-bit). macOS uses int/socklen_t (4 bytes) — see MsghdrDarwin.
final class Msghdr extends ffi.Struct { final class Msghdr extends ffi.Struct {
external ffi.Pointer<ffi.Void> msg_name; external ffi.Pointer<ffi.Void> msg_name;
@ffi.Uint32() @ffi.Uint32()
@@ -132,6 +143,22 @@ final class Msghdr extends ffi.Struct {
external int msg_flags; external int msg_flags;
} }
/// macOS `struct msghdr`. msg_iovlen is int (4 bytes), msg_controllen
/// is socklen_t (4 bytes) — smaller than Linux's size_t fields.
final class MsghdrDarwin extends ffi.Struct {
external ffi.Pointer<ffi.Void> msg_name;
@ffi.Uint32()
external int msg_namelen;
external ffi.Pointer<Iovec> msg_iov;
@ffi.Int32()
external int msg_iovlen;
external ffi.Pointer<ffi.Void> msg_control;
@ffi.Uint32()
external int msg_controllen;
@ffi.Int32()
external int msg_flags;
}
/// POSIX `struct cmsghdr` prefix. We treat the rest of the control /// POSIX `struct cmsghdr` prefix. We treat the rest of the control
/// buffer as a raw byte region and compute offsets by hand. /// buffer as a raw byte region and compute offsets by hand.
// On Linux, cmsg_len is size_t (8 bytes on 64-bit). // On Linux, cmsg_len is size_t (8 bytes on 64-bit).
@@ -183,11 +210,11 @@ ffi.DynamicLibrary _openLibc() {
return ffi.DynamicLibrary.process(); return ffi.DynamicLibrary.process();
} }
final _SocketpairD socketpair = final _SocketpairD socketpair = _libc.lookupFunction<_SocketpairC, _SocketpairD>('socketpair');
_libc.lookupFunction<_SocketpairC, _SocketpairD>('socketpair');
final _RecvmsgD recvmsg = final _RecvmsgD recvmsgLinux = _libc.lookupFunction<_RecvmsgC, _RecvmsgD>('recvmsg');
_libc.lookupFunction<_RecvmsgC, _RecvmsgD>('recvmsg');
final _RecvmsgDarwinD recvmsgDarwin = _libc.lookupFunction<_RecvmsgDarwinC, _RecvmsgDarwinD>('recvmsg');
final _ReadD read = _libc.lookupFunction<_ReadC, _ReadD>('read'); final _ReadD read = _libc.lookupFunction<_ReadC, _ReadD>('read');
@@ -195,11 +222,9 @@ final _WriteD write = _libc.lookupFunction<_WriteC, _WriteD>('write');
final _CloseD close = _libc.lookupFunction<_CloseC, _CloseD>('close'); final _CloseD close = _libc.lookupFunction<_CloseC, _CloseD>('close');
final _IoctlPtrD ioctlWinsize = final _IoctlPtrD ioctlWinsize = _libc.lookupFunction<_IoctlPtrC, _IoctlPtrD>('ioctl');
_libc.lookupFunction<_IoctlPtrC, _IoctlPtrD>('ioctl');
final _FcntlIntD fcntlInt = final _FcntlIntD fcntlInt = _libc.lookupFunction<_FcntlIntC, _FcntlIntD>('fcntl');
_libc.lookupFunction<_FcntlIntC, _FcntlIntD>('fcntl');
/// Resolve `errno` through the platform-appropriate thread-local /// Resolve `errno` through the platform-appropriate thread-local
/// accessor. glibc exposes `__errno_location`, musl the same, macOS /// accessor. glibc exposes `__errno_location`, musl the same, macOS
@@ -238,8 +263,8 @@ T withBuffer<T>(int bytes, T Function(ffi.Pointer<ffi.Uint8>) action) {
bool setNonBlocking(int fd) { bool setNonBlocking(int fd) {
final flags = fcntlInt(fd, fGetFl, 0); final flags = fcntlInt(fd, fGetFl, 0);
if (flags < 0) return false; if (flags < 0) return false;
if ((flags & fIoNonblock) != 0) return false; if ((flags & oNonblock) != 0) return false;
fcntlInt(fd, fSetFl, flags | fIoNonblock); fcntlInt(fd, fSetFl, flags | oNonblock);
return true; return true;
} }
+47 -17
View File
@@ -29,30 +29,61 @@ int recvFd(int socketFd) {
final payload = pkg_ffi.calloc<ffi.Uint8>(payloadLen); final payload = pkg_ffi.calloc<ffi.Uint8>(payloadLen);
final control = pkg_ffi.calloc<ffi.Uint8>(controlLen); final control = pkg_ffi.calloc<ffi.Uint8>(controlLen);
final iov = pkg_ffi.calloc<libc.Iovec>(); final iov = pkg_ffi.calloc<libc.Iovec>();
final msg = pkg_ffi.calloc<libc.Msghdr>();
try { try {
iov.ref.iov_base = payload; iov.ref.iov_base = payload;
iov.ref.iov_len = payloadLen; iov.ref.iov_len = payloadLen;
msg.ref.msg_name = ffi.nullptr;
msg.ref.msg_namelen = 0;
msg.ref.msg_iov = iov;
msg.ref.msg_iovlen = 1;
msg.ref.msg_control = control.cast();
msg.ref.msg_controllen = controlLen;
msg.ref.msg_flags = 0;
int received; int received;
while (true) { int msgControllen;
received = libc.recvmsg(socketFd, msg, 0);
if (received >= 0) break; if (Platform.isMacOS) {
final err = libc.errno; final msg = pkg_ffi.calloc<libc.MsghdrDarwin>();
if (err == 4 /* EINTR */) continue; try {
throw PtyException('recvmsg', 'recvmsg failed', errno: err); msg.ref.msg_name = ffi.nullptr;
msg.ref.msg_namelen = 0;
msg.ref.msg_iov = iov;
msg.ref.msg_iovlen = 1;
msg.ref.msg_control = control.cast();
msg.ref.msg_controllen = controlLen;
msg.ref.msg_flags = 0;
while (true) {
received = libc.recvmsgDarwin(socketFd, msg, 0);
if (received >= 0) break;
final err = libc.errno;
if (err == 4 /* EINTR */) continue;
throw PtyException('recvmsg', 'recvmsg failed', errno: err);
}
msgControllen = msg.ref.msg_controllen;
} finally {
pkg_ffi.calloc.free(msg);
}
} else {
final msg = pkg_ffi.calloc<libc.Msghdr>();
try {
msg.ref.msg_name = ffi.nullptr;
msg.ref.msg_namelen = 0;
msg.ref.msg_iov = iov;
msg.ref.msg_iovlen = 1;
msg.ref.msg_control = control.cast();
msg.ref.msg_controllen = controlLen;
msg.ref.msg_flags = 0;
while (true) {
received = libc.recvmsgLinux(socketFd, msg, 0);
if (received >= 0) break;
final err = libc.errno;
if (err == 4 /* EINTR */) continue;
throw PtyException('recvmsg', 'recvmsg failed', errno: err);
}
msgControllen = msg.ref.msg_controllen;
} finally {
pkg_ffi.calloc.free(msg);
}
} }
if (received == 0 || msg.ref.msg_controllen < 16) { if (received == 0 || msgControllen < 16) {
throw const PtyException( throw const PtyException(
'recvmsg', 'recvmsg',
'peer closed without sending ancillary data', 'peer closed without sending ancillary data',
@@ -84,7 +115,6 @@ int recvFd(int socketFd) {
final fdPtr = (control + dataOffset).cast<ffi.Int32>(); final fdPtr = (control + dataOffset).cast<ffi.Int32>();
return fdPtr.value; return fdPtr.value;
} finally { } finally {
pkg_ffi.calloc.free(msg);
pkg_ffi.calloc.free(iov); pkg_ffi.calloc.free(iov);
pkg_ffi.calloc.free(control); pkg_ffi.calloc.free(control);
pkg_ffi.calloc.free(payload); pkg_ffi.calloc.free(payload);
+28 -49
View File
@@ -49,42 +49,28 @@ ffi.DynamicLibrary _openLib() {
return ffi.DynamicLibrary.open('libutil.so.1'); return ffi.DynamicLibrary.open('libutil.so.1');
} }
final _forkpty = _dl.lookupFunction< final _forkpty = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Void>, ffi.Pointer<_Winsize>),
ffi.Int32 Function(ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Char>, int Function(ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Void>, ffi.Pointer<_Winsize>)>('forkpty');
ffi.Pointer<ffi.Void>, ffi.Pointer<_Winsize>),
int Function(ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Void>, ffi.Pointer<_Winsize>)>('forkpty');
final _execve = _dl.lookupFunction< final _execve = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Pointer<ffi.Char>>, ffi.Pointer<ffi.Pointer<ffi.Char>>),
ffi.Int32 Function(ffi.Pointer<ffi.Char>, int Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Pointer<ffi.Char>>, ffi.Pointer<ffi.Pointer<ffi.Char>>)>('execve');
ffi.Pointer<ffi.Pointer<ffi.Char>>, ffi.Pointer<ffi.Pointer<ffi.Char>>),
int Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Pointer<ffi.Char>>,
ffi.Pointer<ffi.Pointer<ffi.Char>>)>('execve');
final _nativeWrite = ffi.DynamicLibrary.process().lookupFunction< final _nativeWrite = ffi.DynamicLibrary.process()
ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr), .lookupFunction<ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr), int Function(int, ffi.Pointer<ffi.Void>, int)>('write');
int Function(int, ffi.Pointer<ffi.Void>, int)>('write');
final _nativeClose = ffi.DynamicLibrary.process() final _nativeClose = ffi.DynamicLibrary.process().lookupFunction<ffi.Int32 Function(ffi.Int32), int Function(int)>('close');
.lookupFunction<ffi.Int32 Function(ffi.Int32), int Function(int)>('close');
final _ioctl = ffi.DynamicLibrary.process().lookupFunction< final _ioctl = ffi.DynamicLibrary.process()
ffi.Int32 Function(ffi.Int32, ffi.UnsignedLong, ffi.Pointer<_Winsize>), .lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.UnsignedLong, ffi.Pointer<_Winsize>), int Function(int, int, ffi.Pointer<_Winsize>)>('ioctl');
int Function(int, int, ffi.Pointer<_Winsize>)>('ioctl');
final _nativeKill = ffi.DynamicLibrary.process().lookupFunction< final _nativeKill = ffi.DynamicLibrary.process().lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.Int32), int Function(int, int)>('kill');
ffi.Int32 Function(ffi.Int32, ffi.Int32), int Function(int, int)>('kill');
final _waitpid = ffi.DynamicLibrary.process().lookupFunction< final _waitpid = ffi.DynamicLibrary.process()
ffi.Int32 Function(ffi.Int32, ffi.Pointer<ffi.Int32>, ffi.Int32), .lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.Pointer<ffi.Int32>, ffi.Int32), int Function(int, ffi.Pointer<ffi.Int32>, int)>('waitpid');
int Function(int, ffi.Pointer<ffi.Int32>, int)>('waitpid');
final _chdir = ffi.DynamicLibrary.process().lookupFunction< final _chdir = ffi.DynamicLibrary.process().lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Char>), int Function(ffi.Pointer<ffi.Char>)>('chdir');
ffi.Int32 Function(ffi.Pointer<ffi.Char>),
int Function(ffi.Pointer<ffi.Char>)>('chdir');
final _exit_ = ffi.DynamicLibrary.process().lookupFunction< final _exit_ = ffi.DynamicLibrary.process().lookupFunction<ffi.Void Function(ffi.Int32), void Function(int)>('_exit');
ffi.Void Function(ffi.Int32), void Function(int)>('_exit');
final int _kTiocsWinsz = Platform.isMacOS ? 0x80087467 : 0x5414; final int _kTiocsWinsz = Platform.isMacOS ? 0x80087467 : 0x5414;
const _kSighup = 1; const _kSighup = 1;
@@ -153,15 +139,11 @@ class NativePty {
final envList = environment.entries.toList(); final envList = environment.entries.toList();
final envpN = malloc<ffi.Pointer<ffi.Char>>(envList.length + 1); final envpN = malloc<ffi.Pointer<ffi.Char>>(envList.length + 1);
for (var i = 0; i < envList.length; i++) { for (var i = 0; i < envList.length; i++) {
envpN[i] = '${envList[i].key}=${envList[i].value}' envpN[i] = '${envList[i].key}=${envList[i].value}'.toNativeUtf8(allocator: malloc).cast();
.toNativeUtf8(allocator: malloc)
.cast();
} }
envpN[envList.length] = ffi.nullptr; envpN[envList.length] = ffi.nullptr;
final wdN = (workingDirectory ?? '/') final wdN = (workingDirectory ?? '/').toNativeUtf8(allocator: malloc).cast<ffi.Char>();
.toNativeUtf8(allocator: malloc)
.cast<ffi.Char>();
final fdOut = calloc<ffi.Int32>(); final fdOut = calloc<ffi.Int32>();
final ws = calloc<_Winsize>() final ws = calloc<_Winsize>()
..ref.wsRow = rows ..ref.wsRow = rows
@@ -171,8 +153,7 @@ class NativePty {
final pid = _forkpty(fdOut, ffi.nullptr, ffi.nullptr, ws); final pid = _forkpty(fdOut, ffi.nullptr, ffi.nullptr, ws);
if (pid == -1) { if (pid == -1) {
_freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN, _freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN, fdOut, ws);
fdOut, ws);
throw StateError('forkpty() failed'); throw StateError('forkpty() failed');
} }
@@ -185,8 +166,7 @@ class NativePty {
// PARENT // PARENT
final fd = fdOut.value; final fd = fdOut.value;
_freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN, _freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN, fdOut, ws);
fdOut, ws);
final pty = NativePty._(fd, pid); final pty = NativePty._(fd, pid);
pty._spawnReader(); pty._spawnReader();
@@ -195,9 +175,13 @@ class NativePty {
static void _freeAll( static void _freeAll(
ffi.Pointer shell, ffi.Pointer shell,
ffi.Pointer<ffi.Pointer<ffi.Char>> argv, int argc, ffi.Pointer<ffi.Pointer<ffi.Char>> argv,
ffi.Pointer<ffi.Pointer<ffi.Char>> envp, int envc, int argc,
ffi.Pointer wd, ffi.Pointer fdOut, ffi.Pointer ws, ffi.Pointer<ffi.Pointer<ffi.Char>> envp,
int envc,
ffi.Pointer wd,
ffi.Pointer fdOut,
ffi.Pointer ws,
) { ) {
malloc.free(shell); malloc.free(shell);
for (var i = 0; i < argc; i++) malloc.free(argv[i]); for (var i = 0; i < argc; i++) malloc.free(argv[i]);
@@ -229,12 +213,8 @@ class NativePty {
static void _readLoop((SendPort, int) msg) { static void _readLoop((SendPort, int) msg) {
final (port, fd) = msg; final (port, fd) = msg;
final dl = ffi.DynamicLibrary.process(); final dl = ffi.DynamicLibrary.process();
final rd = dl.lookupFunction< final rd = dl.lookupFunction<ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr), int Function(int, ffi.Pointer<ffi.Void>, int)>('read');
ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr), final poll = dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<_Pollfd>, ffi.Uint32, ffi.Int32), int Function(ffi.Pointer<_Pollfd>, int, int)>('poll');
int Function(int, ffi.Pointer<ffi.Void>, int)>('read');
final poll = dl.lookupFunction<
ffi.Int32 Function(ffi.Pointer<_Pollfd>, ffi.Uint32, ffi.Int32),
int Function(ffi.Pointer<_Pollfd>, int, int)>('poll');
final buf = malloc<ffi.Uint8>(65536); final buf = malloc<ffi.Uint8>(65536);
final pfd = calloc<_Pollfd>(); final pfd = calloc<_Pollfd>();
@@ -276,9 +256,8 @@ class NativePty {
final ws = calloc<_Winsize>() final ws = calloc<_Winsize>()
..ref.wsRow = rows ..ref.wsRow = rows
..ref.wsCol = cols; ..ref.wsCol = cols;
final rc = _ioctl(_fd, _kTiocsWinsz, ws); _ioctl(_fd, _kTiocsWinsz, ws);
calloc.free(ws); calloc.free(ws);
print('[pty-resize] fd=$_fd cols=$cols rows=$rows ioctl=$rc pid=$pid');
// Explicitly signal the child to re-query its terminal size. // Explicitly signal the child to re-query its terminal size.
_nativeKill(pid, 28); // SIGWINCH = 28 on macOS/Linux _nativeKill(pid, 28); // SIGWINCH = 28 on macOS/Linux
} }