fix(ui): resolve M2 blocking bugs with full POSIX fork/exec PTY spawn and tmux environment isolation

This commit is contained in:
2026-07-16 22:56:19 +09:00
parent b7901bcce5
commit f52f6eb2af
8 changed files with 474 additions and 322 deletions
@@ -16,129 +16,6 @@
"test" "test"
] ]
}, },
{
"name": "shelf_web_socket",
"version": "1.0.4",
"dependencies": [
"shelf",
"stream_channel",
"web_socket_channel"
]
},
{
"name": "shelf",
"version": "1.4.2",
"dependencies": [
"async",
"collection",
"http_parser",
"path",
"stack_trace",
"stream_channel"
]
},
{
"name": "http_parser",
"version": "4.1.2",
"dependencies": [
"collection",
"source_span",
"string_scanner",
"typed_data"
]
},
{
"name": "collection",
"version": "1.19.1",
"dependencies": []
},
{
"name": "typed_data",
"version": "1.4.0",
"dependencies": [
"collection"
]
},
{
"name": "stream_channel",
"version": "2.1.4",
"dependencies": [
"async"
]
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "ffi",
"version": "2.2.0",
"dependencies": []
},
{
"name": "source_span",
"version": "1.10.2",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "term_glyph",
"version": "1.2.2",
"dependencies": []
},
{
"name": "web_socket_channel",
"version": "2.4.5",
"dependencies": [
"async",
"crypto",
"stream_channel",
"web"
]
},
{
"name": "web",
"version": "0.5.1",
"dependencies": []
},
{
"name": "crypto",
"version": "3.0.7",
"dependencies": [
"typed_data"
]
},
{
"name": "async",
"version": "2.13.1",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "meta",
"version": "1.19.0",
"dependencies": []
},
{ {
"name": "test", "name": "test",
"version": "1.31.2", "version": "1.31.2",
@@ -170,6 +47,70 @@
"yaml" "yaml"
] ]
}, },
{
"name": "meta",
"version": "1.19.0",
"dependencies": []
},
{
"name": "shelf_web_socket",
"version": "1.0.4",
"dependencies": [
"shelf",
"stream_channel",
"web_socket_channel"
]
},
{
"name": "shelf",
"version": "1.4.2",
"dependencies": [
"async",
"collection",
"http_parser",
"path",
"stack_trace",
"stream_channel"
]
},
{
"name": "ffi",
"version": "2.2.0",
"dependencies": []
},
{
"name": "yaml",
"version": "3.1.3",
"dependencies": [
"collection",
"source_span",
"string_scanner"
]
},
{
"name": "webkit_inspection_protocol",
"version": "1.2.1",
"dependencies": [
"logging"
]
},
{
"name": "web_socket_channel",
"version": "2.4.5",
"dependencies": [
"async",
"crypto",
"stream_channel",
"web"
]
},
{
"name": "typed_data",
"version": "1.4.0",
"dependencies": [
"collection"
]
},
{ {
"name": "test_core", "name": "test_core",
"version": "0.6.19", "version": "0.6.19",
@@ -212,6 +153,101 @@
"term_glyph" "term_glyph"
] ]
}, },
{
"name": "stream_channel",
"version": "2.1.4",
"dependencies": [
"async"
]
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "source_span",
"version": "1.10.2",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "shelf_static",
"version": "1.1.3",
"dependencies": [
"convert",
"http_parser",
"mime",
"path",
"shelf"
]
},
{
"name": "shelf_packages_handler",
"version": "3.0.2",
"dependencies": [
"path",
"shelf",
"shelf_static"
]
},
{
"name": "pool",
"version": "1.5.2",
"dependencies": [
"async",
"stack_trace"
]
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "package_config",
"version": "3.0.0",
"dependencies": [
"meta"
]
},
{
"name": "node_preamble",
"version": "2.0.2",
"dependencies": []
},
{
"name": "matcher",
"version": "0.12.20",
"dependencies": [
"async",
"meta",
"stack_trace",
"term_glyph",
"test_api"
]
},
{
"name": "io",
"version": "1.0.5",
"dependencies": [
"meta",
"path",
"string_scanner"
]
},
{
"name": "http_multi_server",
"version": "3.2.2",
"dependencies": [
"async"
]
},
{ {
"name": "coverage", "name": "coverage",
"version": "1.15.1", "version": "1.15.1",
@@ -230,22 +266,99 @@
] ]
}, },
{ {
"name": "yaml", "name": "collection",
"version": "3.1.3", "version": "1.19.1",
"dependencies": []
},
{
"name": "boolean_selector",
"version": "2.1.2",
"dependencies": [ "dependencies": [
"collection",
"source_span", "source_span",
"string_scanner" "string_scanner"
] ]
}, },
{ {
"name": "cli_config", "name": "async",
"version": "0.2.0", "version": "2.13.1",
"dependencies": [ "dependencies": [
"args", "collection",
"meta"
]
},
{
"name": "analyzer",
"version": "14.1.0",
"dependencies": [
"_fe_analyzer_shared",
"collection",
"convert",
"crypto",
"glob",
"meta",
"package_config",
"path",
"pub_semver",
"source_span",
"watcher",
"yaml" "yaml"
] ]
}, },
{
"name": "http_parser",
"version": "4.1.2",
"dependencies": [
"collection",
"source_span",
"string_scanner",
"typed_data"
]
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "logging",
"version": "1.3.0",
"dependencies": []
},
{
"name": "web",
"version": "0.5.1",
"dependencies": []
},
{
"name": "crypto",
"version": "3.0.7",
"dependencies": [
"typed_data"
]
},
{
"name": "vm_service",
"version": "15.2.0",
"dependencies": []
},
{
"name": "source_maps",
"version": "0.10.13",
"dependencies": [
"source_span"
]
},
{
"name": "source_map_stack_trace",
"version": "2.1.2",
"dependencies": [
"path",
"source_maps",
"stack_trace"
]
},
{ {
"name": "glob", "name": "glob",
"version": "2.1.3", "version": "2.1.3",
@@ -266,146 +379,18 @@
] ]
}, },
{ {
"name": "shelf_packages_handler", "name": "args",
"version": "3.0.2", "version": "2.7.0",
"dependencies": [
"path",
"shelf",
"shelf_static"
]
},
{
"name": "pool",
"version": "1.5.2",
"dependencies": [
"async",
"stack_trace"
]
},
{
"name": "node_preamble",
"version": "2.0.2",
"dependencies": [] "dependencies": []
}, },
{ {
"name": "boolean_selector", "name": "term_glyph",
"version": "2.1.2", "version": "1.2.2",
"dependencies": [ "dependencies": []
"source_span",
"string_scanner"
]
}, },
{ {
"name": "source_map_stack_trace", "name": "mime",
"version": "2.1.2", "version": "2.0.0",
"dependencies": [
"path",
"source_maps",
"stack_trace"
]
},
{
"name": "source_maps",
"version": "0.10.13",
"dependencies": [
"source_span"
]
},
{
"name": "file",
"version": "7.0.1",
"dependencies": [
"meta",
"path"
]
},
{
"name": "webkit_inspection_protocol",
"version": "1.2.1",
"dependencies": [
"logging"
]
},
{
"name": "shelf_static",
"version": "1.1.3",
"dependencies": [
"convert",
"http_parser",
"mime",
"path",
"shelf"
]
},
{
"name": "io",
"version": "1.0.5",
"dependencies": [
"meta",
"path",
"string_scanner"
]
},
{
"name": "http_multi_server",
"version": "3.2.2",
"dependencies": [
"async"
]
},
{
"name": "package_config",
"version": "3.0.0",
"dependencies": [
"meta"
]
},
{
"name": "matcher",
"version": "0.12.20",
"dependencies": [
"async",
"meta",
"stack_trace",
"term_glyph",
"test_api"
]
},
{
"name": "analyzer",
"version": "14.1.0",
"dependencies": [
"_fe_analyzer_shared",
"collection",
"convert",
"crypto",
"glob",
"meta",
"package_config",
"path",
"pub_semver",
"source_span",
"watcher",
"yaml"
]
},
{
"name": "_fe_analyzer_shared",
"version": "105.0.0",
"dependencies": [
"meta"
]
},
{
"name": "pub_semver",
"version": "2.2.0",
"dependencies": [
"collection"
]
},
{
"name": "args",
"version": "2.7.0",
"dependencies": [] "dependencies": []
}, },
{ {
@@ -416,9 +401,12 @@
] ]
}, },
{ {
"name": "logging", "name": "cli_config",
"version": "1.3.0", "version": "0.2.0",
"dependencies": [] "dependencies": [
"args",
"yaml"
]
}, },
{ {
"name": "watcher", "name": "watcher",
@@ -429,14 +417,26 @@
] ]
}, },
{ {
"name": "mime", "name": "pub_semver",
"version": "2.0.0", "version": "2.2.0",
"dependencies": [] "dependencies": [
"collection"
]
}, },
{ {
"name": "vm_service", "name": "_fe_analyzer_shared",
"version": "15.2.0", "version": "105.0.0",
"dependencies": [] "dependencies": [
"meta"
]
},
{
"name": "file",
"version": "7.0.1",
"dependencies": [
"meta",
"path"
]
} }
], ],
"configVersion": 1 "configVersion": 1
@@ -20,6 +20,30 @@ typedef _ptsname_dart = ffi.Pointer<ffi.Char> Function(int fd);
typedef _ioctl_c = ffi.Int32 Function(ffi.Int32 fd, ffi.UnsignedLong request, ffi.Pointer<ffi.Void> argp); typedef _ioctl_c = ffi.Int32 Function(ffi.Int32 fd, ffi.UnsignedLong request, ffi.Pointer<ffi.Void> argp);
typedef _ioctl_dart = int Function(int fd, int request, ffi.Pointer<ffi.Void> argp); typedef _ioctl_dart = int Function(int fd, int request, ffi.Pointer<ffi.Void> argp);
typedef _fork_c = ffi.Int32 Function();
typedef _fork_dart = int Function();
typedef _setsid_c = ffi.Int32 Function();
typedef _setsid_dart = int Function();
typedef _open_c = ffi.Int32 Function(ffi.Pointer<ffi.Char> pathname, ffi.Int32 flags);
typedef _open_dart = int Function(ffi.Pointer<ffi.Char> pathname, int flags);
typedef _close_c = ffi.Int32 Function(ffi.Int32 fd);
typedef _close_dart = int Function(int fd);
typedef _dup2_c = ffi.Int32 Function(ffi.Int32 oldfd, ffi.Int32 newfd);
typedef _dup2_dart = int Function(int oldfd, int newfd);
typedef _execvp_c = ffi.Int32 Function(ffi.Pointer<ffi.Char> file, ffi.Pointer<ffi.Pointer<ffi.Char>> argv);
typedef _execvp_dart = int Function(ffi.Pointer<ffi.Char> file, ffi.Pointer<ffi.Pointer<ffi.Char>> argv);
typedef _read_c = ffi.IntPtr Function(ffi.Int32 fd, ffi.Pointer<ffi.Void> buf, ffi.IntPtr count);
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);
base class Winsize extends ffi.Struct { base class Winsize extends ffi.Struct {
@ffi.Uint16() @ffi.Uint16()
external int ws_row; external int ws_row;
@@ -34,22 +58,16 @@ base class Winsize extends ffi.Struct {
class PtySession { class PtySession {
final int masterFd; final int masterFd;
final String slaveName; final String slaveName;
late final File _masterFile; final int childPid;
late final RandomAccessFile _masterRaf;
final _stdoutController = StreamController<List<int>>(); final _stdoutController = StreamController<List<int>>();
late final StreamSubscription _readSub;
late final Process _process;
bool _isClosed = false; bool _isClosed = false;
PtySession._({ PtySession._({
required this.masterFd, required this.masterFd,
required this.slaveName, required this.slaveName,
required RandomAccessFile raf, required this.childPid,
required File file,
}) { }) {
_masterFile = file;
_masterRaf = raf;
_startReading(); _startReading();
} }
@@ -65,72 +83,137 @@ class PtySession {
final grantpt = libc.lookupFunction<_grantpt_c, _grantpt_dart>('grantpt'); final grantpt = libc.lookupFunction<_grantpt_c, _grantpt_dart>('grantpt');
final unlockpt = libc.lookupFunction<_unlockpt_c, _unlockpt_dart>('unlockpt'); final unlockpt = libc.lookupFunction<_unlockpt_c, _unlockpt_dart>('unlockpt');
final ptsname = libc.lookupFunction<_ptsname_c, _ptsname_dart>('ptsname'); final ptsname = libc.lookupFunction<_ptsname_c, _ptsname_dart>('ptsname');
final fork = libc.lookupFunction<_fork_c, _fork_dart>('fork');
final setsid = libc.lookupFunction<_setsid_c, _setsid_dart>('setsid');
final open = libc.lookupFunction<_open_c, _open_dart>('open');
final close = libc.lookupFunction<_close_c, _close_dart>('close');
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');
// O_RDWR = 2, O_NOCTTY = 0x00000400 // 1. Open master PTY (O_RDWR = 2, O_NOCTTY = 0x00000400)
final fd = posixOpenpt(2 | 0x00000400); final masterFd = posixOpenpt(2 | 0x00000400);
if (fd < 0) { if (masterFd < 0) {
throw OSError('Failed to open pseudo-terminal master'); throw OSError('Failed to open pseudo-terminal master');
} }
if (grantpt(fd) != 0) { if (grantpt(masterFd) != 0) {
close(masterFd);
throw OSError('Failed to grant pseudo-terminal slave permissions'); throw OSError('Failed to grant pseudo-terminal slave permissions');
} }
if (unlockpt(fd) != 0) { if (unlockpt(masterFd) != 0) {
close(masterFd);
throw OSError('Failed to unlock pseudo-terminal slave descriptor'); throw OSError('Failed to unlock pseudo-terminal slave descriptor');
} }
final slavePtr = ptsname(fd); final slavePtr = ptsname(masterFd);
if (slavePtr == ffi.Pointer.fromAddress(0)) { if (slavePtr == ffi.Pointer.fromAddress(0)) {
close(masterFd);
throw OSError('Failed to get pseudo-terminal slave device name'); throw OSError('Failed to get pseudo-terminal slave device name');
} }
final slaveName = slavePtr.cast<Utf8>().toDartString(); final slaveName = slavePtr.cast<Utf8>().toDartString();
final masterFile = File('/proc/self/fd/'); // 2. Resolve environment, isolating nested TMUX contexts (§6.7)
final raf = masterFile.openSync(mode: FileMode.writeOnlyAppend); final env = Map<String, String>.from(environment ?? Platform.environment);
env.remove('TMUX');
env.remove('TMUX_PANE');
final process = await Process.start( // 3. Fork child process
executable, final pid = fork();
arguments, if (pid < 0) {
workingDirectory: workingDirectory, close(masterFd);
environment: environment, throw OSError('Failed to fork child process');
runInShell: false, }
mode: ProcessStartMode.normal,
);
final session = PtySession._( if (pid == 0) {
masterFd: fd, // --- CHILD PROCESS BRANCH ---
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);
dup2(slaveFd, 1);
dup2(slaveFd, 2);
close(masterFd);
if (slaveFd > 2) {
close(slaveFd);
}
if (workingDirectory != null) {
Directory.current = workingDirectory;
}
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 ---
return PtySession._(
masterFd: masterFd,
slaveName: slaveName, slaveName: slaveName,
raf: raf, childPid: pid,
file: masterFile,
); );
session._process = process;
return session;
} }
void _startReading() { void _startReading() {
final readStream = _masterFile.openRead(); final libc = ffi.DynamicLibrary.open('libc.so.6');
_readSub = readStream.listen( final cRead = libc.lookupFunction<_read_c, _read_dart>('read');
(data) { final buffer = malloc<ffi.Uint8>(1024);
if (!_isClosed) _stdoutController.add(data);
}, Timer.periodic(const Duration(milliseconds: 10), (timer) {
onError: (err) { if (_isClosed) {
if (!_isClosed) _stdoutController.addError(err); timer.cancel();
}, malloc.free(buffer);
onDone: () { return;
}
final bytesRead = cRead(masterFd, buffer.cast<ffi.Void>(), 1024);
if (bytesRead > 0) {
final bytes = List<int>.generate(bytesRead, (i) => buffer[i]);
_stdoutController.add(bytes);
} else if (bytesRead < 0) {
final errno = ffi.DynamicLibrary.open('libc.so.6').lookup<ffi.Int32>('errno').value;
if (errno != 4 && errno != 11) {
close();
}
} else {
close(); close();
}, }
cancelOnError: false, });
);
} }
void write(List<int> data) { void write(List<int> data) {
if (_isClosed) return; if (_isClosed) return;
try { final libc = ffi.DynamicLibrary.open('libc.so.6');
_masterRaf.writeFromSync(data); final cWrite = libc.lookupFunction<_write_c, _write_dart>('write');
_masterRaf.flushSync();
} catch (_) { final buffer = malloc<ffi.Uint8>(data.length);
for (var i = 0; i < data.length; i++) {
buffer[i] = data[i];
} }
cWrite(masterFd, buffer.cast<ffi.Void>(), data.length);
malloc.free(buffer);
} }
void writeString(String str) { void writeString(String str) {
@@ -153,9 +236,16 @@ class PtySession {
void close() { void close() {
if (_isClosed) return; if (_isClosed) return;
_isClosed = true; _isClosed = true;
_readSub.cancel();
_stdoutController.close(); _stdoutController.close();
_masterRaf.closeSync();
_process.kill(ProcessSignal.sigterm); final libc = ffi.DynamicLibrary.open('libc.so.6');
final close = libc.lookupFunction<_close_c, _close_dart>('close');
close(masterFd);
final kill = libc.lookupFunction<
ffi.Int32 Function(ffi.Int32 pid, ffi.Int32 sig),
int Function(int pid, int sig)
>('kill');
kill(childPid, 15);
} }
} }
@@ -0,0 +1,27 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:mam_pty/mam_pty.dart';
import 'package:test/test.dart';
void main() {
test('PtySession runtime execution resolves process output', () async {
final pty = await PtySession.start('echo', ['hello-pty-ok']);
final completer = Completer<String>();
final buffer = StringBuffer();
final sub = pty.stdout.listen((data) {
buffer.write(utf8.decode(data));
}, onDone: () {
completer.complete(buffer.toString());
});
final result = await completer.future.timeout(const Duration(seconds: 3));
sub.cancel();
pty.close();
expect(result, contains('hello-pty-ok'));
print('PTY Runtime stdout verified: ' + result.trim());
});
}
+16
View File
@@ -0,0 +1,16 @@
{
"configVersion": 2,
"packages": [
{
"name": "multi_agent_mux_ui_root",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.0"
}
],
"generator": "pub",
"generatorVersion": "3.12.1",
"flutterRoot": "file:///home/godopu16/puenv/flutter",
"flutterVersion": "3.44.1",
"pubCache": "file:///home/godopu16/.pub-cache"
}
+14
View File
@@ -0,0 +1,14 @@
{
"roots": [
"multi_agent_mux_ui_root"
],
"packages": [
{
"name": "multi_agent_mux_ui_root",
"version": "1.0.0",
"dependencies": [],
"devDependencies": []
}
],
"configVersion": 1
}
+5
View File
@@ -0,0 +1,5 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages: {}
sdks:
dart: ">=3.0.0 <4.0.0"