T-127: replace InProcessClient with socket loopback
test / unit + widget + golden + a11y (push) Failing after 2m16s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 29s
test / unit + widget + golden + a11y (push) Failing after 2m16s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 29s
Fourth slice of T-99. The UI's DaemonClient now talks to its own IpcServer through the same per-workspace Unix socket the C `clide` client uses — one transport, one wire contract, no second path through the dispatch tree. Changes: * lib/kernel/src/ipc/in_process.dart deleted. Nothing imports it. * DaemonClient.socketPath becomes mutable + new `reconnectAt(path)` method swaps an active client onto a different socket without restart. Project switch in main.dart uses it — the dispatcher + IpcServer are rebuilt for the new workspace, and the client reconnects to the new path. * main.dart's daemonClientFactory now builds a real DaemonClient pointed at workspaceSocketPath(workRoot); swapIpcServer kicks off server.start() then client.start() in sequence. * lib/test_app.dart's pane.spawn smoke test uses dispatcher.dispatch directly instead of InProcessClient — same coverage, no dead-end import. * DaemonClient client_test gets a reconnectAt round-trip test. T-128 (delete IsolateClient + Backend + backend_entry.dart) unblocked. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,13 +11,15 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
class DaemonClient extends ChangeNotifier {
|
||||
DaemonClient({
|
||||
required this.socketPath,
|
||||
required String socketPath,
|
||||
required Logger log,
|
||||
required DaemonBus events,
|
||||
}) : _log = log,
|
||||
}) : _socketPath = socketPath,
|
||||
_log = log,
|
||||
_events = events;
|
||||
|
||||
final String socketPath;
|
||||
String _socketPath;
|
||||
String get socketPath => _socketPath;
|
||||
final Logger _log;
|
||||
final DaemonBus _events;
|
||||
|
||||
@@ -47,6 +49,28 @@ class DaemonClient extends ChangeNotifier {
|
||||
_setConnected(false);
|
||||
}
|
||||
|
||||
/// Point the client at a different socket path and reconnect.
|
||||
/// Used on project switch — the workspace-derived socket path
|
||||
/// (D-70) changes when the user opens a different project, so the
|
||||
/// client follows. Cancels the reconnect timer, closes the live
|
||||
/// socket (failing in-flight requests with `disconnect`), updates
|
||||
/// the path, and re-arms the connect loop. Idempotent if the new
|
||||
/// path equals the current one.
|
||||
Future<void> reconnectAt(String newPath) async {
|
||||
if (newPath == _socketPath && _connected) return;
|
||||
_socketPath = newPath;
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
final s = _socket;
|
||||
_socket = null;
|
||||
await s?.close();
|
||||
_failPending('socket path changed');
|
||||
_setConnected(false);
|
||||
_disposed = false;
|
||||
_backoff = const Duration(milliseconds: 200);
|
||||
await _connect();
|
||||
}
|
||||
|
||||
Future<IpcResponse> request(
|
||||
String cmd, {
|
||||
Map<String, Object?> args = const {},
|
||||
@@ -72,12 +96,12 @@ class DaemonClient extends ChangeNotifier {
|
||||
Future<void> _connect() async {
|
||||
if (_disposed) return;
|
||||
try {
|
||||
final addr = InternetAddress(socketPath, type: InternetAddressType.unix);
|
||||
final addr = InternetAddress(_socketPath, type: InternetAddressType.unix);
|
||||
final socket = await Socket.connect(addr, 0);
|
||||
_socket = socket;
|
||||
_backoff = const Duration(milliseconds: 200);
|
||||
_setConnected(true);
|
||||
_log.info('ipc', 'connected to $socketPath');
|
||||
_log.info('ipc', 'connected to $_socketPath');
|
||||
socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
_handleLine,
|
||||
onDone: _handleDisconnect,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'package:clide/clide.dart';
|
||||
import 'package:clide/kernel/src/ipc/client.dart';
|
||||
|
||||
class InProcessClient extends DaemonClient {
|
||||
InProcessClient({
|
||||
required super.log,
|
||||
required super.events,
|
||||
required this.dispatcher,
|
||||
}) : super(socketPath: '');
|
||||
|
||||
DaemonDispatcher dispatcher;
|
||||
int _nextReqId = 0;
|
||||
|
||||
@override
|
||||
bool get isConnected => true;
|
||||
|
||||
@override
|
||||
Future<void> start() async {}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {}
|
||||
|
||||
@override
|
||||
Future<IpcResponse> request(String cmd, {Map<String, Object?> args = const {}}) {
|
||||
final id = '${_nextReqId++}';
|
||||
final req = IpcRequest(id: id, cmd: cmd, args: args);
|
||||
return dispatcher.dispatch(req);
|
||||
}
|
||||
}
|
||||
+30
-10
@@ -29,7 +29,6 @@ import 'package:clide/builtin/welcome/welcome.dart';
|
||||
import 'dart:io' show Directory, Platform;
|
||||
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/kernel/src/ipc/in_process.dart';
|
||||
import 'package:clide/src/daemon/dispatcher.dart';
|
||||
import 'package:clide/src/daemon/editor_commands.dart';
|
||||
import 'package:clide/src/daemon/files_commands.dart';
|
||||
@@ -40,6 +39,7 @@ import 'package:clide/src/editor/registry.dart' show EditorRegistry;
|
||||
import 'package:clide/src/git/client.dart';
|
||||
import 'package:clide/src/cli/argv_dispatch.dart';
|
||||
import 'package:clide/src/ipc/envelope.dart';
|
||||
import 'package:clide/src/ipc/paths.dart' show workspaceSocketPath;
|
||||
import 'package:clide/src/ipc/server.dart';
|
||||
import 'package:clide/src/panes/event_sink.dart';
|
||||
import 'package:clide/src/panes/registry.dart';
|
||||
@@ -78,11 +78,13 @@ Future<void> main() async {
|
||||
toolchain.applyResolved(resolveToolchainPaths());
|
||||
}
|
||||
|
||||
InProcessClient? ipcClient;
|
||||
DaemonClient? ipcClient;
|
||||
DaemonBus? daemonBus;
|
||||
// IPC socket server (T-99 / T-124, per D-70/71/72). One server per
|
||||
// workspace; restarted when the active project switches because the
|
||||
// socket path is workspace-derived.
|
||||
// socket path is workspace-derived. The local DaemonClient connects
|
||||
// back to it over the socket so all IPC — including from UI widgets
|
||||
// in the same process — goes through the wire contract (T-127).
|
||||
IpcServer? ipcServer;
|
||||
final ipcLog = Logger();
|
||||
|
||||
@@ -100,6 +102,14 @@ Future<void> main() async {
|
||||
await server.start();
|
||||
} catch (e, st) {
|
||||
ipcLog.error('ipc', 'server start failed', error: e, stackTrace: st);
|
||||
return;
|
||||
}
|
||||
// Point the in-process DaemonClient at the new socket. On first
|
||||
// boot (no client yet) the daemonClientFactory below kicks it
|
||||
// off; on project switch we just reconnect to the new path.
|
||||
final client = ipcClient;
|
||||
if (client != null) {
|
||||
await client.reconnectAt(server.socketPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,18 +143,28 @@ Future<void> main() async {
|
||||
daemonBus = events;
|
||||
final workRoot = FilesService.atCwd(events: _BusEventSink(events)).root;
|
||||
final dispatcher = buildDispatcher(events, toolchain, workRoot);
|
||||
ipcClient = InProcessClient(log: log, events: events, dispatcher: dispatcher);
|
||||
// Fire-and-forget: bring up the IPC socket server alongside.
|
||||
// Failure is logged, not fatal — the UI still works.
|
||||
unawaited(swapIpcServer(dispatcher, workRoot));
|
||||
return ipcClient!;
|
||||
// Build the client at the workspace's socket path. The
|
||||
// server is started below (swapIpcServer) which the
|
||||
// client will then auto-connect to via its reconnect
|
||||
// loop. autoStartDaemonClient:false means we own the
|
||||
// lifecycle here.
|
||||
final client = DaemonClient(
|
||||
socketPath: workspaceSocketPath(workRoot.path),
|
||||
log: log,
|
||||
events: events,
|
||||
);
|
||||
ipcClient = client;
|
||||
unawaited(() async {
|
||||
await swapIpcServer(dispatcher, workRoot);
|
||||
await client.start();
|
||||
}());
|
||||
return client;
|
||||
},
|
||||
onProjectOpen: kIsWeb
|
||||
? null
|
||||
: (path) async {
|
||||
if (ipcClient == null || daemonBus == null) return;
|
||||
if (daemonBus == null) return;
|
||||
final dispatcher = buildDispatcher(daemonBus!, toolchain, Directory(path));
|
||||
ipcClient!.dispatcher = dispatcher;
|
||||
await swapIpcServer(dispatcher, Directory(path));
|
||||
},
|
||||
);
|
||||
|
||||
+9
-4
@@ -29,7 +29,6 @@ import 'dart:ffi' as ffi;
|
||||
import 'package:ffi/ffi.dart' as pkg_ffi;
|
||||
import 'kernel/kernel.dart';
|
||||
import 'src/pty/ffi/libc.dart' as libc;
|
||||
import 'kernel/src/ipc/in_process.dart';
|
||||
import 'src/daemon/pane_commands.dart';
|
||||
import 'src/ipc/envelope.dart';
|
||||
import 'src/panes/event_sink.dart';
|
||||
@@ -325,19 +324,25 @@ class _ClideTestAppState extends State<ClideTestApp> {
|
||||
Future<void> _runTerminalTests(Toolchain tc, String workDir) async {
|
||||
_say('--- terminal ---');
|
||||
|
||||
// Test PTY via InProcessClient — same path as the real app.
|
||||
// Test PTY via the dispatcher directly — skip the socket
|
||||
// round-trip for the smoke test since it adds setup without
|
||||
// testing anything new for pane.spawn. The real app's path is
|
||||
// covered by the IPC server + client tests under test/ipc/.
|
||||
await _testAsync('pane.spawn via IPC', () async {
|
||||
final dispatcher = DaemonDispatcher();
|
||||
final bus = DaemonBus();
|
||||
final eventSink = _TestEventSink(bus);
|
||||
final paneRegistry = PaneRegistry(events: eventSink);
|
||||
registerPaneCommands(dispatcher, paneRegistry);
|
||||
final ipc = InProcessClient(log: Logger(), events: bus, dispatcher: dispatcher);
|
||||
|
||||
Future<IpcResponse> dispatch(String cmd, Map<String, Object?> args) {
|
||||
return dispatcher.dispatch(IpcRequest(id: 'tm-${DateTime.now().microsecondsSinceEpoch}', cmd: cmd, args: args));
|
||||
}
|
||||
|
||||
// Spawn a pane running /bin/echo.
|
||||
// Use interactive shell — fast-exiting commands lose output on macOS
|
||||
// because the slave closes before we can read the master.
|
||||
final spawnResp = await ipc.request('pane.spawn', args: {
|
||||
final spawnResp = await dispatch('pane.spawn', {
|
||||
'argv': [tc.shell],
|
||||
'kind': 'terminal',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user