feat(ui): complete M2 Milestone - Desktop POSIX PTY FFI implementation and attach terminal tab integration
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
library mam_pty;
|
||||
|
||||
export 'src/pty_session.dart';
|
||||
@@ -0,0 +1,161 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi' as ffi;
|
||||
import 'dart:io';
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
// FFI Signatures for Linux libc
|
||||
typedef _posix_openpt_c = ffi.Int32 Function(ffi.Int32 flags);
|
||||
typedef _posix_openpt_dart = int Function(int flags);
|
||||
|
||||
typedef _grantpt_c = ffi.Int32 Function(ffi.Int32 fd);
|
||||
typedef _grantpt_dart = int Function(int fd);
|
||||
|
||||
typedef _unlockpt_c = ffi.Int32 Function(ffi.Int32 fd);
|
||||
typedef _unlockpt_dart = int Function(int fd);
|
||||
|
||||
typedef _ptsname_c = ffi.Pointer<ffi.Char> Function(ffi.Int32 fd);
|
||||
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_dart = int Function(int fd, int request, ffi.Pointer<ffi.Void> argp);
|
||||
|
||||
base class Winsize extends ffi.Struct {
|
||||
@ffi.Uint16()
|
||||
external int ws_row;
|
||||
@ffi.Uint16()
|
||||
external int ws_col;
|
||||
@ffi.Uint16()
|
||||
external int ws_xpixel;
|
||||
@ffi.Uint16()
|
||||
external int ws_ypixel;
|
||||
}
|
||||
|
||||
class PtySession {
|
||||
final int masterFd;
|
||||
final String slaveName;
|
||||
late final File _masterFile;
|
||||
late final RandomAccessFile _masterRaf;
|
||||
|
||||
final _stdoutController = StreamController<List<int>>();
|
||||
late final StreamSubscription _readSub;
|
||||
late final Process _process;
|
||||
bool _isClosed = false;
|
||||
|
||||
PtySession._({
|
||||
required this.masterFd,
|
||||
required this.slaveName,
|
||||
required RandomAccessFile raf,
|
||||
required File file,
|
||||
}) {
|
||||
_masterFile = file;
|
||||
_masterRaf = raf;
|
||||
_startReading();
|
||||
}
|
||||
|
||||
Stream<List<int>> get stdout => _stdoutController.stream;
|
||||
|
||||
static Future<PtySession> start(String executable, List<String> arguments, {
|
||||
String? workingDirectory,
|
||||
Map<String, String>? environment,
|
||||
}) async {
|
||||
final libc = ffi.DynamicLibrary.open('libc.so.6');
|
||||
|
||||
final posixOpenpt = libc.lookupFunction<_posix_openpt_c, _posix_openpt_dart>('posix_openpt');
|
||||
final grantpt = libc.lookupFunction<_grantpt_c, _grantpt_dart>('grantpt');
|
||||
final unlockpt = libc.lookupFunction<_unlockpt_c, _unlockpt_dart>('unlockpt');
|
||||
final ptsname = libc.lookupFunction<_ptsname_c, _ptsname_dart>('ptsname');
|
||||
|
||||
// O_RDWR = 2, O_NOCTTY = 0x00000400
|
||||
final fd = posixOpenpt(2 | 0x00000400);
|
||||
if (fd < 0) {
|
||||
throw OSError('Failed to open pseudo-terminal master');
|
||||
}
|
||||
|
||||
if (grantpt(fd) != 0) {
|
||||
throw OSError('Failed to grant pseudo-terminal slave permissions');
|
||||
}
|
||||
|
||||
if (unlockpt(fd) != 0) {
|
||||
throw OSError('Failed to unlock pseudo-terminal slave descriptor');
|
||||
}
|
||||
|
||||
final slavePtr = ptsname(fd);
|
||||
if (slavePtr == ffi.Pointer.fromAddress(0)) {
|
||||
throw OSError('Failed to get pseudo-terminal slave device name');
|
||||
}
|
||||
final slaveName = slavePtr.cast<Utf8>().toDartString();
|
||||
|
||||
final masterFile = File('/proc/self/fd/');
|
||||
final raf = masterFile.openSync(mode: FileMode.writeOnlyAppend);
|
||||
|
||||
final process = await Process.start(
|
||||
executable,
|
||||
arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
environment: environment,
|
||||
runInShell: false,
|
||||
mode: ProcessStartMode.normal,
|
||||
);
|
||||
|
||||
final session = PtySession._(
|
||||
masterFd: fd,
|
||||
slaveName: slaveName,
|
||||
raf: raf,
|
||||
file: masterFile,
|
||||
);
|
||||
session._process = process;
|
||||
return session;
|
||||
}
|
||||
|
||||
void _startReading() {
|
||||
final readStream = _masterFile.openRead();
|
||||
_readSub = readStream.listen(
|
||||
(data) {
|
||||
if (!_isClosed) _stdoutController.add(data);
|
||||
},
|
||||
onError: (err) {
|
||||
if (!_isClosed) _stdoutController.addError(err);
|
||||
},
|
||||
onDone: () {
|
||||
close();
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
void write(List<int> data) {
|
||||
if (_isClosed) return;
|
||||
try {
|
||||
_masterRaf.writeFromSync(data);
|
||||
_masterRaf.flushSync();
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
|
||||
void writeString(String str) {
|
||||
write(utf8.encode(str));
|
||||
}
|
||||
|
||||
void resize(int cols, int rows) {
|
||||
if (_isClosed) return;
|
||||
final libc = ffi.DynamicLibrary.open('libc.so.6');
|
||||
final ioctl = libc.lookupFunction<_ioctl_c, _ioctl_dart>('ioctl');
|
||||
|
||||
final size = calloc<Winsize>();
|
||||
size.ref.ws_col = cols;
|
||||
size.ref.ws_row = rows;
|
||||
|
||||
ioctl(masterFd, 0x5414, size.cast<ffi.Void>());
|
||||
calloc.free(size);
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (_isClosed) return;
|
||||
_isClosed = true;
|
||||
_readSub.cancel();
|
||||
_stdoutController.close();
|
||||
_masterRaf.closeSync();
|
||||
_process.kill(ProcessSignal.sigterm);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user