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:
@@ -56,7 +56,6 @@ class PaneRegistry {
|
||||
'COLORTERM': 'truecolor',
|
||||
'LANG': 'en_US.UTF-8',
|
||||
'LC_ALL': 'en_US.UTF-8',
|
||||
'TERMINFO': '/usr/share/terminfo',
|
||||
if (env != null) ...env,
|
||||
};
|
||||
|
||||
|
||||
+40
-15
@@ -16,7 +16,7 @@ import 'dart:io' show Platform;
|
||||
import 'package:ffi/ffi.dart' as pkg_ffi;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants (POSIX / Linux)
|
||||
// Constants (POSIX — platform-dispatched where Linux/macOS diverge)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const int afUnix = 1;
|
||||
@@ -25,11 +25,11 @@ const int sockStream = 1;
|
||||
final int solSocket = Platform.isMacOS ? 0xffff : 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 fSetFl = 4;
|
||||
|
||||
const int tiocswinsz = 0x5414; // Linux x86_64; macOS differs
|
||||
final int tiocswinsz = Platform.isMacOS ? 0x80087467 : 0x5414;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
@@ -59,6 +59,17 @@ typedef _RecvmsgD = int Function(
|
||||
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(
|
||||
ffi.Int32 fd,
|
||||
ffi.Pointer<ffi.Uint8> buf,
|
||||
@@ -116,8 +127,8 @@ final class Iovec extends ffi.Struct {
|
||||
external int iov_len;
|
||||
}
|
||||
|
||||
/// POSIX `struct msghdr`. Field layout matches Linux/glibc; macOS is
|
||||
/// byte-compatible here.
|
||||
/// Linux `struct msghdr`. msg_iovlen/msg_controllen are size_t (8 bytes
|
||||
/// on 64-bit). macOS uses int/socklen_t (4 bytes) — see MsghdrDarwin.
|
||||
final class Msghdr extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.Void> msg_name;
|
||||
@ffi.Uint32()
|
||||
@@ -132,6 +143,22 @@ final class Msghdr extends ffi.Struct {
|
||||
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
|
||||
/// buffer as a raw byte region and compute offsets by hand.
|
||||
// On Linux, cmsg_len is size_t (8 bytes on 64-bit).
|
||||
@@ -183,11 +210,11 @@ ffi.DynamicLibrary _openLibc() {
|
||||
return ffi.DynamicLibrary.process();
|
||||
}
|
||||
|
||||
final _SocketpairD socketpair =
|
||||
_libc.lookupFunction<_SocketpairC, _SocketpairD>('socketpair');
|
||||
final _SocketpairD socketpair = _libc.lookupFunction<_SocketpairC, _SocketpairD>('socketpair');
|
||||
|
||||
final _RecvmsgD recvmsg =
|
||||
_libc.lookupFunction<_RecvmsgC, _RecvmsgD>('recvmsg');
|
||||
final _RecvmsgD recvmsgLinux = _libc.lookupFunction<_RecvmsgC, _RecvmsgD>('recvmsg');
|
||||
|
||||
final _RecvmsgDarwinD recvmsgDarwin = _libc.lookupFunction<_RecvmsgDarwinC, _RecvmsgDarwinD>('recvmsg');
|
||||
|
||||
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 _IoctlPtrD ioctlWinsize =
|
||||
_libc.lookupFunction<_IoctlPtrC, _IoctlPtrD>('ioctl');
|
||||
final _IoctlPtrD ioctlWinsize = _libc.lookupFunction<_IoctlPtrC, _IoctlPtrD>('ioctl');
|
||||
|
||||
final _FcntlIntD fcntlInt =
|
||||
_libc.lookupFunction<_FcntlIntC, _FcntlIntD>('fcntl');
|
||||
final _FcntlIntD fcntlInt = _libc.lookupFunction<_FcntlIntC, _FcntlIntD>('fcntl');
|
||||
|
||||
/// Resolve `errno` through the platform-appropriate thread-local
|
||||
/// 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) {
|
||||
final flags = fcntlInt(fd, fGetFl, 0);
|
||||
if (flags < 0) return false;
|
||||
if ((flags & fIoNonblock) != 0) return false;
|
||||
fcntlInt(fd, fSetFl, flags | fIoNonblock);
|
||||
if ((flags & oNonblock) != 0) return false;
|
||||
fcntlInt(fd, fSetFl, flags | oNonblock);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,30 +29,61 @@ int recvFd(int socketFd) {
|
||||
final payload = pkg_ffi.calloc<ffi.Uint8>(payloadLen);
|
||||
final control = pkg_ffi.calloc<ffi.Uint8>(controlLen);
|
||||
final iov = pkg_ffi.calloc<libc.Iovec>();
|
||||
final msg = pkg_ffi.calloc<libc.Msghdr>();
|
||||
|
||||
try {
|
||||
iov.ref.iov_base = payload;
|
||||
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;
|
||||
while (true) {
|
||||
received = libc.recvmsg(socketFd, msg, 0);
|
||||
if (received >= 0) break;
|
||||
final err = libc.errno;
|
||||
if (err == 4 /* EINTR */) continue;
|
||||
throw PtyException('recvmsg', 'recvmsg failed', errno: err);
|
||||
int msgControllen;
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
final msg = pkg_ffi.calloc<libc.MsghdrDarwin>();
|
||||
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.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(
|
||||
'recvmsg',
|
||||
'peer closed without sending ancillary data',
|
||||
@@ -84,7 +115,6 @@ int recvFd(int socketFd) {
|
||||
final fdPtr = (control + dataOffset).cast<ffi.Int32>();
|
||||
return fdPtr.value;
|
||||
} finally {
|
||||
pkg_ffi.calloc.free(msg);
|
||||
pkg_ffi.calloc.free(iov);
|
||||
pkg_ffi.calloc.free(control);
|
||||
pkg_ffi.calloc.free(payload);
|
||||
|
||||
+28
-49
@@ -49,42 +49,28 @@ ffi.DynamicLibrary _openLib() {
|
||||
return ffi.DynamicLibrary.open('libutil.so.1');
|
||||
}
|
||||
|
||||
final _forkpty = _dl.lookupFunction<
|
||||
ffi.Int32 Function(ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Char>,
|
||||
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 _forkpty = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Char>, 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<
|
||||
ffi.Int32 Function(ffi.Pointer<ffi.Char>,
|
||||
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 _execve = _dl.lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Char>, 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<
|
||||
ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr),
|
||||
int Function(int, ffi.Pointer<ffi.Void>, int)>('write');
|
||||
final _nativeWrite = ffi.DynamicLibrary.process()
|
||||
.lookupFunction<ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr), int Function(int, ffi.Pointer<ffi.Void>, int)>('write');
|
||||
|
||||
final _nativeClose = ffi.DynamicLibrary.process()
|
||||
.lookupFunction<ffi.Int32 Function(ffi.Int32), int Function(int)>('close');
|
||||
final _nativeClose = ffi.DynamicLibrary.process().lookupFunction<ffi.Int32 Function(ffi.Int32), int Function(int)>('close');
|
||||
|
||||
final _ioctl = ffi.DynamicLibrary.process().lookupFunction<
|
||||
ffi.Int32 Function(ffi.Int32, ffi.UnsignedLong, ffi.Pointer<_Winsize>),
|
||||
int Function(int, int, ffi.Pointer<_Winsize>)>('ioctl');
|
||||
final _ioctl = ffi.DynamicLibrary.process()
|
||||
.lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.UnsignedLong, ffi.Pointer<_Winsize>), int Function(int, int, ffi.Pointer<_Winsize>)>('ioctl');
|
||||
|
||||
final _nativeKill = ffi.DynamicLibrary.process().lookupFunction<
|
||||
ffi.Int32 Function(ffi.Int32, ffi.Int32), int Function(int, int)>('kill');
|
||||
final _nativeKill = ffi.DynamicLibrary.process().lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.Int32), int Function(int, int)>('kill');
|
||||
|
||||
final _waitpid = ffi.DynamicLibrary.process().lookupFunction<
|
||||
ffi.Int32 Function(ffi.Int32, ffi.Pointer<ffi.Int32>, ffi.Int32),
|
||||
int Function(int, ffi.Pointer<ffi.Int32>, int)>('waitpid');
|
||||
final _waitpid = ffi.DynamicLibrary.process()
|
||||
.lookupFunction<ffi.Int32 Function(ffi.Int32, ffi.Pointer<ffi.Int32>, ffi.Int32), int Function(int, ffi.Pointer<ffi.Int32>, int)>('waitpid');
|
||||
|
||||
final _chdir = ffi.DynamicLibrary.process().lookupFunction<
|
||||
ffi.Int32 Function(ffi.Pointer<ffi.Char>),
|
||||
int Function(ffi.Pointer<ffi.Char>)>('chdir');
|
||||
final _chdir = ffi.DynamicLibrary.process().lookupFunction<ffi.Int32 Function(ffi.Pointer<ffi.Char>), int Function(ffi.Pointer<ffi.Char>)>('chdir');
|
||||
|
||||
final _exit_ = ffi.DynamicLibrary.process().lookupFunction<
|
||||
ffi.Void Function(ffi.Int32), void Function(int)>('_exit');
|
||||
final _exit_ = ffi.DynamicLibrary.process().lookupFunction<ffi.Void Function(ffi.Int32), void Function(int)>('_exit');
|
||||
|
||||
final int _kTiocsWinsz = Platform.isMacOS ? 0x80087467 : 0x5414;
|
||||
const _kSighup = 1;
|
||||
@@ -153,15 +139,11 @@ class NativePty {
|
||||
final envList = environment.entries.toList();
|
||||
final envpN = malloc<ffi.Pointer<ffi.Char>>(envList.length + 1);
|
||||
for (var i = 0; i < envList.length; i++) {
|
||||
envpN[i] = '${envList[i].key}=${envList[i].value}'
|
||||
.toNativeUtf8(allocator: malloc)
|
||||
.cast();
|
||||
envpN[i] = '${envList[i].key}=${envList[i].value}'.toNativeUtf8(allocator: malloc).cast();
|
||||
}
|
||||
envpN[envList.length] = ffi.nullptr;
|
||||
|
||||
final wdN = (workingDirectory ?? '/')
|
||||
.toNativeUtf8(allocator: malloc)
|
||||
.cast<ffi.Char>();
|
||||
final wdN = (workingDirectory ?? '/').toNativeUtf8(allocator: malloc).cast<ffi.Char>();
|
||||
final fdOut = calloc<ffi.Int32>();
|
||||
final ws = calloc<_Winsize>()
|
||||
..ref.wsRow = rows
|
||||
@@ -171,8 +153,7 @@ class NativePty {
|
||||
final pid = _forkpty(fdOut, ffi.nullptr, ffi.nullptr, ws);
|
||||
|
||||
if (pid == -1) {
|
||||
_freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN,
|
||||
fdOut, ws);
|
||||
_freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN, fdOut, ws);
|
||||
throw StateError('forkpty() failed');
|
||||
}
|
||||
|
||||
@@ -185,8 +166,7 @@ class NativePty {
|
||||
|
||||
// PARENT
|
||||
final fd = fdOut.value;
|
||||
_freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN,
|
||||
fdOut, ws);
|
||||
_freeAll(shellN, argvN, allArgs.length, envpN, envList.length, wdN, fdOut, ws);
|
||||
|
||||
final pty = NativePty._(fd, pid);
|
||||
pty._spawnReader();
|
||||
@@ -195,9 +175,13 @@ class NativePty {
|
||||
|
||||
static void _freeAll(
|
||||
ffi.Pointer shell,
|
||||
ffi.Pointer<ffi.Pointer<ffi.Char>> argv, int argc,
|
||||
ffi.Pointer<ffi.Pointer<ffi.Char>> envp, int envc,
|
||||
ffi.Pointer wd, ffi.Pointer fdOut, ffi.Pointer ws,
|
||||
ffi.Pointer<ffi.Pointer<ffi.Char>> argv,
|
||||
int argc,
|
||||
ffi.Pointer<ffi.Pointer<ffi.Char>> envp,
|
||||
int envc,
|
||||
ffi.Pointer wd,
|
||||
ffi.Pointer fdOut,
|
||||
ffi.Pointer ws,
|
||||
) {
|
||||
malloc.free(shell);
|
||||
for (var i = 0; i < argc; i++) malloc.free(argv[i]);
|
||||
@@ -229,12 +213,8 @@ class NativePty {
|
||||
static void _readLoop((SendPort, int) msg) {
|
||||
final (port, fd) = msg;
|
||||
final dl = ffi.DynamicLibrary.process();
|
||||
final rd = dl.lookupFunction<
|
||||
ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr),
|
||||
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 rd = dl.lookupFunction<ffi.IntPtr Function(ffi.Int32, ffi.Pointer<ffi.Void>, ffi.IntPtr), 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 pfd = calloc<_Pollfd>();
|
||||
@@ -276,9 +256,8 @@ class NativePty {
|
||||
final ws = calloc<_Winsize>()
|
||||
..ref.wsRow = rows
|
||||
..ref.wsCol = cols;
|
||||
final rc = _ioctl(_fd, _kTiocsWinsz, ws);
|
||||
_ioctl(_fd, _kTiocsWinsz, 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.
|
||||
_nativeKill(pid, 28); // SIGWINCH = 28 on macOS/Linux
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user