From 94a25f4827dcf0056e176f3389b87bea200797bf Mon Sep 17 00:00:00 2001 From: chen08209 Date: Thu, 8 Oct 2026 16:09:04 +0800 Subject: [PATCH] fix(desktop): fix the system proxy, tray and Helper on Windows and Linux Changelog: Windows and Linux desktop fixes --- lib/common/linux_elevation.dart | 123 ++++ lib/common/preferences_store.dart | 111 +++ lib/common/system.dart | 71 +- lib/common/tray.dart | 4 +- lib/common/window.dart | 107 ++- lib/core/desktop/helper_client.dart | 26 +- lib/core/desktop/lifecycle.dart | 3 +- lib/core/desktop/rpc_client.dart | 20 +- lib/core/desktop/transport.dart | 246 +++---- lib/manager/window_manager.dart | 20 +- linux/CMakeLists.txt | 1 + linux/packaging/appimage/make_config.yaml | 2 + linux/packaging/deb/make_config.yaml | 2 + linux/packaging/rpm/make_config.yaml | 2 + linux/runner/CMakeLists.txt | 1 + linux/runner/main.cc | 9 + linux/runner/my_application.cc | 20 +- plugins/proxy/lib/proxy.dart | 6 +- plugins/proxy/lib/src/linux_proxy.dart | 14 +- plugins/proxy/lib/src/proxy_command.dart | 39 +- plugins/proxy/lib/src/windows_proxy.dart | 17 + plugins/proxy/test/proxy_test.dart | 93 ++- plugins/proxy/windows/CMakeLists.txt | 4 +- .../include/proxy/proxy_plugin_c_api.h | 3 + plugins/proxy/windows/proxy_plugin.cpp | 181 ++++- plugins/proxy/windows/proxy_plugin.h | 4 + plugins/proxy/windows/proxy_plugin_c_api.cpp | 4 + plugins/rust_api/lib/src/rust/api/ipc.dart | 46 +- .../rust_api/lib/src/rust/frb_generated.dart | 566 +++++++++++---- .../lib/src/rust/frb_generated.io.dart | 150 +++- .../lib/src/rust/frb_generated.web.dart | 142 +++- plugins/rust_api/rust/src/api/ipc.rs | 60 +- plugins/rust_api/rust/src/frb_generated.rs | 568 +++++++++++---- plugins/rust_api/rust/src/ipc/access.rs | 83 +++ plugins/rust_api/rust/src/ipc/frame.rs | 319 ++------- plugins/rust_api/rust/src/ipc/mod.rs | 22 +- plugins/rust_api/rust/src/ipc/outbox.rs | 165 +++++ plugins/rust_api/rust/src/ipc/platform.rs | 170 ----- plugins/rust_api/rust/src/ipc/queue.rs | 241 ------- plugins/rust_api/rust/src/ipc/server.rs | 672 ++++++++++++------ plugins/rust_api/rust/src/ipc/unsupported.rs | 32 +- plugins/rust_api/rust/src/script/mod.rs | 14 +- .../rust/tests/fixtures/profile_script.js | 4 +- .../tray/test/windows_menu_source_test.dart | 48 +- plugins/tray/windows/tray_plugin.cpp | 49 +- plugins/tray/windows/tray_plugin.h | 3 + plugins/window/windows/window_controller.cpp | 4 +- plugins/window/windows/window_style.h | 4 +- services/helper/src/service/hub.rs | 104 ++- services/helper/src/service/peer.rs | 77 +- test/common/preferences_store_test.dart | 161 +++++ test/common/preferences_test.dart | 2 - test/common/system_test.dart | 88 ++- test/common/tray_menu_test.dart | 15 +- test/common/window_test.dart | 117 +++ test/core/desktop/fakes.dart | 4 +- test/core/desktop/helper_client_test.dart | 7 + test/core/desktop/ipc_native_test.dart | 100 +++ test/core/desktop/transport_test.dart | 365 ++++++---- test/lint/platform_layering_test.dart | 1 - test/manager/window_manager_test.dart | 29 +- test/tool/packaging_config_test.dart | 18 + windows/packaging/exe/inno_setup.iss | 58 ++ windows/runner/main.cpp | 17 +- 64 files changed, 3947 insertions(+), 1711 deletions(-) create mode 100644 lib/common/linux_elevation.dart create mode 100644 lib/common/preferences_store.dart create mode 100644 plugins/proxy/lib/src/windows_proxy.dart create mode 100644 plugins/rust_api/rust/src/ipc/access.rs create mode 100644 plugins/rust_api/rust/src/ipc/outbox.rs delete mode 100644 plugins/rust_api/rust/src/ipc/platform.rs delete mode 100644 plugins/rust_api/rust/src/ipc/queue.rs create mode 100644 test/common/preferences_store_test.dart create mode 100644 test/core/desktop/ipc_native_test.dart diff --git a/lib/common/linux_elevation.dart b/lib/common/linux_elevation.dart new file mode 100644 index 00000000..030498fc --- /dev/null +++ b/lib/common/linux_elevation.dart @@ -0,0 +1,123 @@ +import 'dart:io'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; + +typedef InputProcessRunner = + Future Function( + String executable, + List arguments, + String input, + ); + +typedef PasswordPrompt = Future Function(); + +/// pkexec exits 127 when it cannot obtain an authorization at all, which is +/// what happens without a polkit agent, as under many tiling window managers; +/// 126 is the user dismissing the dialog and is final. +const _pkexecNotAuthorized = 127; + +class LinuxElevation { + @visibleForTesting + ProcessRunner run = Process.run; + + @visibleForTesting + InputProcessRunner runWithInput = _runWithInput; + + @visibleForTesting + PasswordPrompt askPassword = _askPassword; + + Future elevate(List command) async { + try { + final result = await run('pkexec', command); + if (result.exitCode != _pkexecNotAuthorized) { + return _succeeded('pkexec', result); + } + commonPrint.log( + 'pkexec could not ask for authorization, falling back to sudo: ' + '${result.stderr.toString().trim()}', + logLevel: LogLevel.warning, + ); + } on ProcessException catch (error) { + commonPrint.log( + 'pkexec is unavailable, falling back to sudo: ${compactError(error)}', + logLevel: LogLevel.warning, + ); + } + return _sudo(command); + } + + Future _sudo(List command) async { + try { + final cached = await run('sudo', ['-n', '--', ...command]); + if (cached.exitCode == 0) { + return true; + } + final probe = await run('sudo', ['-n', 'true']); + if (probe.exitCode == 0) { + return _succeeded('sudo', cached); + } + final password = await askPassword(); + if (password == null) { + return false; + } + final result = await runWithInput('sudo', [ + '-S', + '-p', + '', + '--', + ...command, + ], '$password\n'); + return _succeeded('sudo', result); + } on ProcessException catch (error) { + commonPrint.log( + 'sudo is unavailable: ${compactError(error)}', + logLevel: LogLevel.error, + ); + return false; + } + } + + bool _succeeded(String executable, ProcessResult result) { + if (result.exitCode != 0) { + commonPrint.log( + '$executable exited with ${result.exitCode}: ' + '${result.stderr.toString().trim()}', + logLevel: LogLevel.error, + ); + } + return result.exitCode == 0; + } + + static Future _runWithInput( + String executable, + List arguments, + String input, + ) async { + final process = await Process.start(executable, arguments); + final stdout = process.stdout.transform(systemEncoding.decoder).join(); + final stderr = process.stderr.transform(systemEncoding.decoder).join(); + try { + process.stdin.write(input); + await process.stdin.close(); + } on IOException { + // A sudo that needed no password may have exited before reading it. + } + return ProcessResult( + process.pid, + await process.exitCode, + await stdout, + await stderr, + ); + } + + static Future _askPassword() async { + await windowPort?.show(); + return dialogs.showPasswordInput( + title: currentAppLocalizations.sudoPasswordTitle, + ); + } +} + +final linuxElevation = LinuxElevation(); diff --git a/lib/common/preferences_store.dart b/lib/common/preferences_store.dart new file mode 100644 index 00000000..79ee0c88 --- /dev/null +++ b/lib/common/preferences_store.dart @@ -0,0 +1,111 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:fl_clash/common/file.dart'; +import 'package:fl_clash/common/print.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'package:shared_preferences_platform_interface/types.dart'; + +const _defaultPrefix = 'flutter.'; + +/// Stands in for the Windows and Linux stores, which truncate the file before +/// writing it (flutter/flutter#89211): same file and format, but each save +/// replaces it with a flushed sibling. +class AtomicFilePreferencesStore extends SharedPreferencesStorePlatform { + AtomicFilePreferencesStore(this._path); + + final Future _path; + Future>? _values; + Future _lastWrite = Future.value(true); + + Future> _read() { + return _values ??= _load().catchError((Object error, StackTrace stack) { + _values = null; + Error.throwWithStackTrace(error, stack); + }); + } + + Future> _load() async { + final file = File(await _path); + if (!await file.exists()) { + return {}; + } + final content = await file.readAsString(); + if (content.isEmpty) { + return {}; + } + final data = json.decode(content); + return data is Map ? Map.from(data) : {}; + } + + Future _update(void Function(Map values) change) async { + final values = await _read(); + change(values); + final content = json.encode(values); + return _lastWrite = _lastWrite.then((_) => _write(content)); + } + + Future _write(String content) async { + final file = File(await _path); + try { + await file.writeAsStringAtomically(content); + return true; + } catch (error) { + commonPrint.log( + 'Save preferences failed: ${compactError(error)}', + logLevel: LogLevel.warning, + ); + return false; + } + } + + @override + Future> getAll() { + return getAllWithParameters( + GetAllParameters(filter: PreferencesFilter(prefix: _defaultPrefix)), + ); + } + + @override + Future> getAllWithParameters( + GetAllParameters parameters, + ) async { + final filter = parameters.filter; + final values = await _read(); + return { + for (final entry in values.entries) + if (_matches(filter, entry.key)) entry.key: entry.value, + }; + } + + @override + Future setValue(String valueType, String key, Object value) { + return _update((values) => values[key] = value); + } + + @override + Future remove(String key) { + return _update((values) => values.remove(key)); + } + + @override + Future clear() { + return clearWithParameters( + ClearParameters(filter: PreferencesFilter(prefix: _defaultPrefix)), + ); + } + + @override + Future clearWithParameters(ClearParameters parameters) { + return _update( + (values) => + values.removeWhere((key, _) => _matches(parameters.filter, key)), + ); + } + + bool _matches(PreferencesFilter filter, String key) { + return key.startsWith(filter.prefix) && + (filter.allowList?.contains(key) ?? true); + } +} diff --git a/lib/common/system.dart b/lib/common/system.dart index 569178f4..291efdae 100644 --- a/lib/common/system.dart +++ b/lib/common/system.dart @@ -106,7 +106,8 @@ class System { @visibleForTesting static String linuxElevationShell(String corePath) { final path = _shellEscape(corePath); - return 'group=\$(id -g "\$PKEXEC_UID") && chown "root:\$group" $path && ' + return 'group=\$(id -g "\${PKEXEC_UID:-\$SUDO_UID}") && ' + 'chown "root:\$group" $path && ' 'chmod 4750 $path'; } @@ -248,28 +249,12 @@ class System { } return AuthorizeCode.success; } else if (system.isLinux) { - final ProcessResult result; - try { - result = await runProcess('pkexec', [ - '/bin/sh', - '-c', - linuxElevationShell(appPath.corePath), - ]); - } on ProcessException catch (error) { - commonPrint.log( - 'pkexec is unavailable: ${compactError(error)}', - logLevel: LogLevel.error, - ); - return AuthorizeCode.error; - } - if (result.exitCode != 0) { - commonPrint.log( - 'pkexec refused to elevate the Core: ${result.exitCode}', - logLevel: LogLevel.error, - ); - return AuthorizeCode.error; - } - return AuthorizeCode.success; + final elevated = await linuxElevation.elevate([ + '/bin/sh', + '-c', + linuxElevationShell(appPath.corePath), + ]); + return elevated ? AuthorizeCode.success : AuthorizeCode.error; } return AuthorizeCode.error; } @@ -289,6 +274,8 @@ final system = System(); class Windows { static Windows? _instance; + static const _swHide = 0; + static const _swShowNormal = 1; late DynamicLibrary _shell32; Windows._internal() { @@ -300,7 +287,7 @@ class Windows { return _instance!; } - bool runas(String command, String arguments) { + bool runas(String command, String arguments, {bool showWindow = true}) { final commandPtr = command.toNativeUtf16(); final argumentsPtr = arguments.toNativeUtf16(); final operationPtr = 'runas'.toNativeUtf16(); @@ -331,7 +318,7 @@ class Windows { commandPtr, argumentsPtr, nullptr, - 1, + showWindow ? _swShowNormal : _swHide, ); calloc.free(commandPtr); @@ -356,7 +343,8 @@ class Windows { Future registerService() { return registerHelperService( - () async => runas(appPath.helperPath, installArguments(pid)), + () async => + runas(appPath.helperPath, installArguments(pid), showWindow: false), ); } } @@ -433,9 +421,6 @@ final windows = system.isWindows ? Windows() : null; class Linux { static Linux? _instance; - @visibleForTesting - ProcessRunner runProcess = Process.run; - Linux._internal(); factory Linux() { @@ -447,31 +432,11 @@ class Linux { return registerHelperService(installService); } - /// pkexec raises the system polkit prompt and names the requesting user to - /// the installer, which is where the unit takes the account it grants the - /// Helper socket to. + /// pkexec and sudo both name the requesting user to the installer, which is + /// where the unit takes the account it grants the Helper socket to. @visibleForTesting - Future installService() async { - try { - final result = await runProcess('pkexec', [ - appPath.helperPath, - 'install', - ]); - if (result.exitCode == 0) { - return true; - } - commonPrint.log( - 'pkexec helper install exited with ${result.exitCode}: ' - '${result.stderr.toString().trim()}', - logLevel: LogLevel.error, - ); - } catch (error) { - commonPrint.log( - 'pkexec is unavailable: ${compactError(error)}', - logLevel: LogLevel.error, - ); - } - return false; + Future installService() { + return linuxElevation.elevate([appPath.helperPath, 'install']); } } diff --git a/lib/common/tray.dart b/lib/common/tray.dart index a371404f..4e1c879c 100644 --- a/lib/common/tray.dart +++ b/lib/common/tray.dart @@ -211,7 +211,9 @@ class AppTray implements TrayPort { if (delay == null) { return null; } - return delay > 0 ? '$delay' : currentAppLocalizations.timeout; + return delay > 0 + ? '$delay' + : delayFailureText(delay, currentAppLocalizations); } List _buildGroupMenu({ diff --git a/lib/common/window.dart b/lib/common/window.dart index 8e82dfe4..1e09bc36 100755 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/config.dart'; +import 'package:flutter/foundation.dart'; import 'package:material_ui/material_ui.dart'; import 'package:screen_retriever/screen_retriever.dart'; import 'package:window/window.dart'; @@ -31,11 +32,6 @@ class Window implements WindowPort { } Future init(int version, WindowProps props) async { - final acquire = await singleInstanceLock.acquire(); - if (!acquire) { - commonPrint.log('another instance owns the data directory, exiting'); - exit(0); - } if (!safeModeBuild) { if (system.isWindows) { for (final scheme in protocolSchemes) { @@ -61,39 +57,26 @@ class Window implements WindowPort { } Future _windowPosition(WindowProps props) async { - if (_supportsPosition) { - final left = props.left; - final top = props.top; - if (left == null || top == null) { - await desktopWindow.setAlignment(Alignment.center); - } else { - final size = props.size; - final right = left + size.width; - final bottom = top + size.height; - final displays = await screenRetriever.getAllDisplays(); - final isPositionValid = displays.any((display) { - final visiblePosition = display.visiblePosition; - if (visiblePosition == null) { - return false; - } - final displayBounds = Rect.fromLTWH( - visiblePosition.dx, - visiblePosition.dy, - display.size.width, - display.size.height, + if (!_supportsPosition) { + return; + } + final position = props.left == null || props.top == null + ? null + : restoredWindowPosition( + props, + displays: await screenRetriever.getAllDisplays(), + currentScale: system.isWindows ? _windowScale : null, ); - return displayBounds.contains(Offset(left, top)) || - displayBounds.contains(Offset(right, bottom)); - }); - if (isPositionValid) { - await desktopWindow.setPosition(Offset(left, top)); - } else { - await desktopWindow.setAlignment(Alignment.center); - } - } + if (position == null) { + await desktopWindow.setAlignment(Alignment.center); + } else { + await desktopWindow.setPosition(position); } } + double get _windowScale => + PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 1; + @override Future captureNormalGeometry(WindowProps current) async { final states = await Future.wait([ @@ -119,6 +102,9 @@ class Window implements WindowPort { height: bounds.height, left: hasValidPosition ? bounds.left : current.left, top: hasValidPosition ? bounds.top : current.top, + scale: hasValidPosition && system.isWindows + ? _windowScale + : current.scale, ); } @@ -163,9 +149,12 @@ class Window implements WindowPort { /// Every desktop runner leaves the window hidden until [init] reveals it, so /// a failure before that point would leave the error screen with no window. - Future showInitFailure() async { + Future showInitFailure({required AsyncCallback onExit}) async { + desktopWindow.addListener(_InitFailureWindowListener(onExit)); try { await desktopWindow.ensureInitialized(); + await desktopWindow.setPreventClose(true); + await desktopWindow.setTitleBarStyle(TitleBarStyle.normal); if (await desktopWindow.isVisible()) { return; } @@ -200,9 +189,7 @@ class Window implements WindowPort { } Future _isWindowVisible() async { - final value = await desktopWindow.isVisible(); - commonPrint.log('window visible check: $value'); - return value; + return desktopWindow.isVisible(); } @override @@ -216,6 +203,50 @@ class Window implements WindowPort { } } +/// On Windows the plugin scales by the window's monitor and screen_retriever by +/// each display's own, so only physical pixels compare across mixed scales. +@visibleForTesting +Offset? restoredWindowPosition( + WindowProps props, { + required List displays, + double? currentScale, +}) { + final savedScale = currentScale == null ? 1.0 : props.scale ?? currentScale; + final topLeft = Offset(props.left!, props.top!) * savedScale; + final bottomRight = + topLeft + props.size.bottomRight(Offset.zero) * savedScale; + final isVisible = displays.any((display) { + final position = display.visiblePosition; + if (position == null) { + return false; + } + final scale = currentScale == null + ? 1.0 + : display.scaleFactor?.toDouble() ?? 1.0; + final bounds = position & display.size; + final physical = Rect.fromLTRB( + bounds.left * scale, + bounds.top * scale, + bounds.right * scale, + bounds.bottom * scale, + ); + return physical.contains(topLeft) || physical.contains(bottomRight); + }); + return isVisible ? topLeft / (currentScale ?? 1.0) : null; +} + +class _InitFailureWindowListener with WindowListener { + final AsyncCallback onExit; + + _InitFailureWindowListener(this.onExit); + + @override + void onWindowClose() => unawaited(onExit()); + + @override + void onWindowShouldTerminate() => unawaited(onExit()); +} + /// Serializes visibility requests so a burst of hotkey toggles lands in /// order, and holds back the Dock-hiding activation policy switch while a /// preceding regular switch settles: flipping regular → accessory → regular diff --git a/lib/core/desktop/helper_client.dart b/lib/core/desktop/helper_client.dart index 9b6afea2..eb18befb 100644 --- a/lib/core/desktop/helper_client.dart +++ b/lib/core/desktop/helper_client.dart @@ -8,6 +8,7 @@ import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/common/print.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:path/path.dart' as p; +import 'package:win32_registry/win32_registry.dart'; import 'core_manifest.dart'; import 'launcher.dart'; @@ -54,7 +55,7 @@ final class HelperClient { final Dio _dio; final String Function() _expectedHelperPath; final Future Function() _readCoreSha256; - final String baseUrl; + final String Function() _baseUrl; String? _coreSha256Cache; HelperClient({ @@ -63,7 +64,7 @@ final class HelperClient { Future Function()? readCoreSha256, String? baseUrl, }) : _dio = dio ?? _createDio(), - baseUrl = baseUrl ?? _defaultBaseUrl(), + _baseUrl = baseUrl == null ? _defaultBaseUrl : (() => baseUrl), _expectedHelperPath = expectedHelperPath ?? _defaultHelperPath, _readCoreSha256 = readCoreSha256 ?? _readBundledCoreSha256; @@ -111,10 +112,29 @@ final class HelperClient { ); } + String get baseUrl => _baseUrl(); + static String _defaultBaseUrl() { return Platform.isLinux ? 'http://$appHelperService' - : 'http://$localhost:$helperPort'; + : 'http://$localhost:${_readHelperPort()}'; + } + + /// Helpers installed before the port was published listen on [helperPort]. + static int _readHelperPort() { + if (!Platform.isWindows) { + return helperPort; + } + try { + final key = LOCAL_MACHINE.open(helperPortKey); + try { + return key.getInt(helperPortValue) ?? helperPort; + } finally { + key.close(); + } + } catch (_) { + return helperPort; + } } static Future _readBundledCoreSha256() async { diff --git a/lib/core/desktop/lifecycle.dart b/lib/core/desktop/lifecycle.dart index 31747f3d..ccb34d18 100644 --- a/lib/core/desktop/lifecycle.dart +++ b/lib/core/desktop/lifecycle.dart @@ -710,11 +710,12 @@ final class DesktopCoreLifecycle implements DesktopCoreLifecycleController { return; } switch (event) { - case TransportDisconnected(:final generation) + case TransportDisconnected(:final generation, :final error) when generation == running.session.connectionGeneration: _handleUnexpectedDisconnect( running.session, code: 'unexpected_disconnect', + cause: error, ); case TransportFailed(:final error, :final stackTrace): _handleUnexpectedDisconnect( diff --git a/lib/core/desktop/rpc_client.dart b/lib/core/desktop/rpc_client.dart index c9dd7e92..76f106a3 100644 --- a/lib/core/desktop/rpc_client.dart +++ b/lib/core/desktop/rpc_client.dart @@ -18,6 +18,9 @@ const _maxExtensionGrace = Duration(seconds: 30); const _connectTimeout = Duration(seconds: 10); +final _requestEncoder = JsonUtf8Encoder(); +final _frameDecoder = utf8.decoder.fuse(json.decoder); + abstract interface class CoreRpcChannel { Future invoke({ required CoreMethod method, @@ -83,17 +86,14 @@ final class CoreRpcClient implements CoreRpcChannel { if (completer.isCompleted) { return await completer.future as T?; } - final sendTimeout = requestTimeout - stopwatch.elapsed; - if (sendTimeout <= Duration.zero) { + if (stopwatch.elapsed >= requestTimeout) { throw TimeoutException('Core method ${method.name} timed out'); } - await transport - .send( - json.encode( - CoreMethodCall(id: id, method: method, arguments: arguments), - ), - ) - .timeout(sendTimeout); + transport.send( + _requestEncoder.convert( + CoreMethodCall(id: id, method: method, arguments: arguments), + ), + ); return await _awaitResponse( method: method, completer: completer, @@ -170,7 +170,7 @@ final class CoreRpcClient implements CoreRpcChannel { void _handleFrame(Uint8List frame) { try { - final decoded = json.decode(utf8.decode(frame)); + final decoded = _frameDecoder.convert(frame); if (decoded is! Map) { throw const FormatException('Core transport data is not an object'); } diff --git a/lib/core/desktop/transport.dart b/lib/core/desktop/transport.dart index 9bd4e8b2..7fad1b0b 100644 --- a/lib/core/desktop/transport.dart +++ b/lib/core/desktop/transport.dart @@ -1,20 +1,11 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:typed_data'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:rust_api/rust_api.dart'; -const _typeReady = 0x00; -const _typeConnected = 0x01; -const _typeDisconnected = 0x02; -const _typeData = 0x03; -const _typeError = 0x04; - -typedef IpcServerStarter = Stream Function(String address); -typedef IpcMessageSender = Future Function(List data); -typedef IpcServerStopper = Future Function(); +typedef IpcServerBinder = Future Function(String address); enum DesktopTransportState { idle, starting, ready, connected, failed, closed } @@ -35,8 +26,9 @@ final class TransportConnected extends DesktopTransportEvent { final class TransportDisconnected extends DesktopTransportEvent { final int generation; + final Object? error; - const TransportDisconnected(this.generation); + const TransportDisconnected(this.generation, {this.error}); } final class TransportFailed extends DesktopTransportEvent { @@ -59,7 +51,7 @@ abstract interface class DesktopCoreTransport { Future waitUntilConnected(Duration timeout); - Future send(String message); + void send(List frame); Future close(); } @@ -90,6 +82,13 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport { ); } + Future _unbind() async { + await _eventSubscription?.cancel(); + await _frameSubscription?.cancel(); + _eventSubscription = null; + _frameSubscription = null; + } + @override String get address => _transport.address; @@ -107,8 +106,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport { throw StateError('IPC transport binding is closed'); } final previous = _transport; - if (previous.state == DesktopTransportState.connected && - !_eventController.isClosed) { + if (previous.state == DesktopTransportState.connected) { _eventController.add( TransportFailed( StateError('Connected IPC transport was replaced'), @@ -116,22 +114,12 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport { ), ); } - await _eventSubscription?.cancel(); - await _frameSubscription?.cancel(); - _eventSubscription = null; - _frameSubscription = null; - Object? closeError; - StackTrace? closeStackTrace; + await _unbind(); try { await previous.close(); - } catch (error, stackTrace) { - closeError = error; - closeStackTrace = stackTrace; - } - _transport = next; - _bind(next); - if (closeError != null) { - Error.throwWithStackTrace(closeError, closeStackTrace!); + } finally { + _transport = next; + _bind(next); } } @@ -144,7 +132,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport { } @override - Future send(String message) => _transport.send(message); + void send(List frame) => _transport.send(frame); @override Future close() { @@ -152,10 +140,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport { } Future _close() async { - await _eventSubscription?.cancel(); - await _frameSubscription?.cancel(); - _eventSubscription = null; - _frameSubscription = null; + await _unbind(); try { await _transport.close(); } finally { @@ -165,21 +150,18 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport { } } -final class IPCCoreTransport implements DesktopCoreTransport { +final class IpcCoreTransport implements DesktopCoreTransport { @override final String address; - final Duration readyTimeout; - final IpcServerStarter _startServer; - final IpcMessageSender _sendMessage; - final IpcServerStopper _stopServer; - + final IpcServerBinder _bind; final StreamController _eventController = StreamController.broadcast(); final StreamController _frameController = StreamController.broadcast(); - StreamSubscription? _subscription; + IpcServer? _server; + StreamSubscription? _subscription; Future? _openOperation; Future? _closeOperation; DesktopTransportState _state = DesktopTransportState.idle; @@ -187,22 +169,11 @@ final class IPCCoreTransport implements DesktopCoreTransport { TransportFailed? _failure; int _connectionGeneration = 0; - IPCCoreTransport({ - required this.address, - this.readyTimeout = const Duration(seconds: 10), - IpcServerStarter? startServer, - IpcMessageSender? sendMessage, - IpcServerStopper? stopServer, - }) : _startServer = startServer ?? _restartIpcServer, - _sendMessage = sendMessage ?? _sendIpcMessage, - _stopServer = stopServer ?? stopIpcServer; + IpcCoreTransport({required this.address, IpcServerBinder? bind}) + : _bind = bind ?? _bindIpcServer; - static Stream _restartIpcServer(String address) { - return restartIpcServer(name: address); - } - - static Future _sendIpcMessage(List data) { - return sendIpcMessage(data: data); + static Future _bindIpcServer(String address) { + return IpcServer.bind(address: address); } @override @@ -216,134 +187,103 @@ final class IPCCoreTransport implements DesktopCoreTransport { @override Future open() { - if (_state == DesktopTransportState.ready || - _state == DesktopTransportState.connected) { - return Future.value(); - } if (_state == DesktopTransportState.closed) { return Future.error(StateError('IPC transport is closed')); } + final failure = _failure; + if (failure != null) { + return Future.error(failure.error, failure.stackTrace); + } return _openOperation ??= _open(); } Future _open() async { _state = DesktopTransportState.starting; - final readiness = events.firstWhere( - (event) => event is TransportReady || event is TransportFailed, - ); try { - _subscription = _startServer(address).listen( - _handleFrame, - onError: _handleStreamError, - onDone: _handleStreamDone, - cancelOnError: false, + final server = await _bind(address); + if (_state == DesktopTransportState.closed) { + await server.close(); + throw StateError('IPC transport is closed'); + } + _server = server; + _state = DesktopTransportState.ready; + commonPrint.log('IPC Ready'); + _eventController.add(const TransportReady()); + _subscription = server.events().listen( + _handleEvent, + onError: _fail, + onDone: _handleDone, ); - final event = await readiness.timeout(readyTimeout); - if (event case TransportFailed(:final error, :final stackTrace)) { - Error.throwWithStackTrace(error, stackTrace); - } } catch (error, stackTrace) { - if (_state != DesktopTransportState.failed && - _state != DesktopTransportState.closed) { - _fail(error, stackTrace); - } - await _subscription?.cancel(); - _subscription = null; + _fail(error, stackTrace); rethrow; } } - void _handleFrame(Uint8List data) { - if (data.isEmpty || _state == DesktopTransportState.closed) { + bool get _isTerminal => + _state == DesktopTransportState.failed || + _state == DesktopTransportState.closed; + + void _handleEvent(IpcEvent event) { + if (_isTerminal) { return; } - final type = data[0]; - final payload = data.length > 1 - ? Uint8List.sublistView(data, 1) - : Uint8List(0); - switch (type) { - case _typeReady: - commonPrint.log('IPC Ready'); - _failure = null; - _state = DesktopTransportState.ready; - _eventController.add(const TransportReady()); - case _typeConnected: - final processId = _decodeConnectedProcessId(payload); - if (payload.isNotEmpty && processId == null) { - _fail(StateError('Invalid IPC connected frame'), StackTrace.current); - return; - } - commonPrint.log( - 'IPC Connected${processId == null ? '' : ': $processId'}', - ); - _failure = null; - _connectionGeneration++; - _connection = TransportConnected( - pid: processId, - generation: _connectionGeneration, - ); - _state = DesktopTransportState.connected; - _eventController.add(_connection!); - case _typeDisconnected: - _disconnect(); - case _typeData: - if (!_frameController.isClosed) { - _frameController.add(payload); - } - case _typeError: - final message = utf8.decode(payload, allowMalformed: true); - _fail(StateError('IPC error: $message'), StackTrace.current); - default: - commonPrint.log( - 'IPC unknown frame type: $type', - logLevel: LogLevel.warning, - ); + switch (event.kind) { + case IpcEventKind.connected: + _connect(event.pid); + case IpcEventKind.message: + _frameController.add(event.payload); + case IpcEventKind.disconnected: + _disconnect(event.error); + case IpcEventKind.failed: + _fail(StateError('IPC error: ${event.error}'), StackTrace.current); } } - int? _decodeConnectedProcessId(Uint8List payload) { - if (payload.isEmpty) { - return null; - } - if (payload.length != Uint32List.bytesPerElement) { - return null; - } - final processId = ByteData.sublistView(payload).getUint32(0, Endian.little); - return processId == 0 ? null : processId; + void _connect(int? pid) { + commonPrint.log('IPC Connected${pid == null ? '' : ': $pid'}'); + final connection = TransportConnected( + pid: pid, + generation: ++_connectionGeneration, + ); + _connection = connection; + _state = DesktopTransportState.connected; + _eventController.add(connection); } - void _disconnect() { + void _disconnect(String? error) { final connection = _connection; if (connection == null) { return; } - commonPrint.log('IPC Disconnected'); + commonPrint.log( + 'IPC Disconnected${error == null ? '' : ': $error'}', + logLevel: error == null ? LogLevel.info : LogLevel.warning, + ); _connection = null; _state = DesktopTransportState.ready; - _eventController.add(TransportDisconnected(connection.generation)); + _eventController.add( + TransportDisconnected( + connection.generation, + error: error == null ? null : StateError('IPC error: $error'), + ), + ); } - void _handleStreamError(Object error, StackTrace stackTrace) { - _fail(error, stackTrace); - } - - void _handleStreamDone() { - if (_state == DesktopTransportState.closed) { - return; - } - _disconnect(); + void _handleDone() { _fail(StateError('IPC server stopped unexpectedly'), StackTrace.current); } void _fail(Object error, StackTrace stackTrace) { - if (_state == DesktopTransportState.failed || - _state == DesktopTransportState.closed) { + if (_isTerminal) { return; } commonPrint.log('IPC error: $error', logLevel: LogLevel.debug); + final failure = TransportFailed(error, stackTrace); + _connection = null; + _failure = failure; _state = DesktopTransportState.failed; - _failure = TransportFailed(error, stackTrace); - _eventController.add(_failure!); + _eventController.add(failure); } @override @@ -371,11 +311,15 @@ final class IPCCoreTransport implements DesktopCoreTransport { } @override - Future send(String message) { + void send(List frame) { if (_state == DesktopTransportState.closed) { - return Future.error(StateError('IPC transport is closed')); + throw StateError('IPC transport is closed'); } - return _sendMessage(utf8.encode(message)); + final server = _server; + if (server == null) { + throw StateError('IPC transport is not open'); + } + server.send(message: frame); } @override @@ -384,16 +328,12 @@ final class IPCCoreTransport implements DesktopCoreTransport { } Future _close() async { - if (_state == DesktopTransportState.closed) { - return; - } _state = DesktopTransportState.closed; _connection = null; try { - await _stopServer(); + await _server?.close(); } finally { await _subscription?.cancel(); - _subscription = null; await _eventController.close(); await _frameController.close(); } diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index 92315a88..4936a393 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/launch.dart'; @@ -16,7 +17,11 @@ const _windowGeometryDelay = Duration(milliseconds: 120); class WindowManager extends ConsumerStatefulWidget { final Widget child; - const WindowManager({super.key, required this.child}); + /// A request to quit from outside the window, such as SIGTERM from `kill` + /// or the session ending; exits like the tray's quit. + final Stream? terminateSignals; + + const WindowManager({super.key, this.terminateSignals, required this.child}); @override ConsumerState createState() => _WindowContainerState(); @@ -25,6 +30,7 @@ class WindowManager extends ConsumerStatefulWidget { class _WindowContainerState extends ConsumerState with WindowListener { Timer? _windowGeometryTimer; + StreamSubscription? _terminateSubscription; int _windowGeometryRevision = 0; int _windowBlurRevision = 0; @@ -52,6 +58,9 @@ class _WindowContainerState extends ConsumerState } }, fireImmediately: true); desktopWindow.addListener(this); + _terminateSubscription = widget.terminateSignals?.listen( + (_) => unawaited(onWindowShouldTerminate()), + ); } Future _applyWindowBlur(WindowBlurRequest request) async { @@ -86,12 +95,6 @@ class _WindowContainerState extends ConsumerState super.onWindowClose(); } - @override - void onWindowFocus() { - super.onWindowFocus(); - commonPrint.log('focus'); - } - /// Another launch, or a Dock reopen, asked for the window. @override void onWindowActivate() { @@ -182,13 +185,11 @@ class _WindowContainerState extends ConsumerState void onWindowMinimize() async { _invalidateWindowGeometryCapture(); ref.read(storeActionProvider.notifier).savePreferencesDebounce(); - commonPrint.log('minimize'); super.onWindowMinimize(); } @override void onWindowRestore() { - commonPrint.log('restore'); super.onWindowRestore(); _scheduleWindowGeometryCapture(); } @@ -196,6 +197,7 @@ class _WindowContainerState extends ConsumerState @override void dispose() { _invalidateWindowGeometryCapture(); + unawaited(_terminateSubscription?.cancel()); desktopWindow.removeListener(this); super.dispose(); } diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 07358bb2..ce3347ee 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -53,6 +53,7 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") diff --git a/linux/packaging/appimage/make_config.yaml b/linux/packaging/appimage/make_config.yaml index 88680848..aaca7cde 100644 --- a/linux/packaging/appimage/make_config.yaml +++ b/linux/packaging/appimage/make_config.yaml @@ -20,4 +20,6 @@ categories: startup_notify: true +startup_wm_class: com.follow.clash + include: [] \ No newline at end of file diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index bc06a385..6896b1b1 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -39,3 +39,5 @@ categories: - Network startup_notify: true + +startup_wm_class: com.follow.clash diff --git a/linux/packaging/rpm/make_config.yaml b/linux/packaging/rpm/make_config.yaml index 5a4f1e92..e8930c1a 100644 --- a/linux/packaging/rpm/make_config.yaml +++ b/linux/packaging/rpm/make_config.yaml @@ -46,3 +46,5 @@ generic_name: FlClash group: Applications/Internet startup_notify: true + +startup_wm_class: com.follow.clash diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt index e97dabc7..fbd501e2 100644 --- a/linux/runner/CMakeLists.txt +++ b/linux/runner/CMakeLists.txt @@ -22,5 +22,6 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::X11) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc index e7c5c543..7cfa072d 100644 --- a/linux/runner/main.cc +++ b/linux/runner/main.cc @@ -1,6 +1,15 @@ #include "my_application.h" +#ifdef GDK_WINDOWING_X11 +#include +#endif + int main(int argc, char** argv) { +#ifdef GDK_WINDOWING_X11 + // libX11 enables its own locking only from 1.8 on; on older releases the + // tray indicator reaching X11 from another thread aborts the process. + XInitThreads(); +#endif g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 270e1d6e..d146c22a 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -55,6 +55,21 @@ static void my_application_activate(GApplication* application) { gtk_window_set_default_size(window, 680, 580); + // X11 panels that do not match the window to a desktop entry read its icon; + // GDK leaves one over 512x512 out of _NET_WM_ICON without a word. + g_autofree gchar* executable = g_file_read_link("/proc/self/exe", nullptr); + if (executable != nullptr) { + g_autofree gchar* directory = g_path_get_dirname(executable); + g_autofree gchar* path = g_build_filename( + directory, "data", "flutter_assets", "assets", "images", "icon.png", + nullptr); + g_autoptr(GdkPixbuf) icon = + gdk_pixbuf_new_from_file_at_size(path, 256, 256, nullptr); + if (icon != nullptr) { + gtk_window_set_icon(window, icon); + } + } + g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); @@ -97,8 +112,9 @@ static void my_application_before_emit(GApplication* application, GVariant* plat } // Implements GApplication::after_emit. A relaunch reaches the primary as a -// command-line signal that the gtk plugin consumes first, so the window is -// raised here, while the launcher's activation token is still current. +// command-line signal whose activate returns early on the existing window, so +// the window is raised here, while the launcher's activation token is still +// current. static void my_application_after_emit(GApplication* application, GVariant* platform_data) { MyApplication* self = MY_APPLICATION(application); if (self->had_window_before_emit) { diff --git a/plugins/proxy/lib/proxy.dart b/plugins/proxy/lib/proxy.dart index bc8a83f9..92b1aab2 100644 --- a/plugins/proxy/lib/proxy.dart +++ b/plugins/proxy/lib/proxy.dart @@ -4,6 +4,7 @@ import 'proxy_platform_interface.dart'; import 'src/linux_proxy.dart'; import 'src/macos_proxy.dart'; import 'src/proxy_command.dart'; +import 'src/windows_proxy.dart'; export 'src/proxy_command.dart' show ProxyExecutableChecker, ProxyProcessRunner; @@ -41,7 +42,10 @@ class Proxy { desktop: Platform.environment['XDG_CURRENT_DESKTOP'], homeDir: Platform.environment['HOME'], ), - 'windows' => await ProxyPlatform.instance.startProxy(port, bypassDomain), + 'windows' => await ProxyPlatform.instance.startProxy( + port, + windowsBypassList(bypassDomain), + ), String() => false, }; } diff --git a/plugins/proxy/lib/src/linux_proxy.dart b/plugins/proxy/lib/src/linux_proxy.dart index 0b053171..d8f5f75b 100644 --- a/plugins/proxy/lib/src/linux_proxy.dart +++ b/plugins/proxy/lib/src/linux_proxy.dart @@ -325,7 +325,7 @@ class LinuxProxyCommands { 'Proxy Settings', '--key', '${type.name}Proxy', - '${type.name}://$proxyHost:$port', + '${type == _ProxyType.socks ? 'socks' : 'http'}://$proxyHost:$port', ]), ProxyCommand(executable, [ '--file', @@ -336,6 +336,7 @@ class LinuxProxyCommands { 'ProxyType', '1', ]), + _kdeReparseConfiguration, ]; } @@ -353,9 +354,20 @@ class LinuxProxyCommands { 'ProxyType', '0', ]), + _kdeReparseConfiguration, ]; } + /// Running KIO programs keep the settings they read at startup until this + /// signal, which is what the KDE proxy settings page sends after saving. + static final _kdeReparseConfiguration = ProxyCommand('dbus-send', [ + '--session', + '--type=signal', + '/KIO/Scheduler', + 'org.kde.KIO.Scheduler.reparseSlaveConfiguration', + 'string:', + ], optional: true); + static String _formatGSettingsStringList(List values) { final escaped = values.map((value) { return value.replaceAll(r'\', r'\\').replaceAll("'", r"\'"); diff --git a/plugins/proxy/lib/src/proxy_command.dart b/plugins/proxy/lib/src/proxy_command.dart index 7f305e35..401e0bcd 100644 --- a/plugins/proxy/lib/src/proxy_command.dart +++ b/plugins/proxy/lib/src/proxy_command.dart @@ -18,9 +18,14 @@ class ProxyCommand { final String executable; final List args; final bool runInShell; + final bool optional; - ProxyCommand(this.executable, List args, {this.runInShell = false}) - : args = List.unmodifiable(args); + ProxyCommand( + this.executable, + List args, { + this.runInShell = false, + this.optional = false, + }) : args = List.unmodifiable(args); } class ProxyCommandRunner { @@ -38,21 +43,25 @@ class ProxyCommandRunner { Future run(Iterable commands) async { var executed = false; - try { - for (final command in commands) { - executed = true; - final result = await process( - command.executable, - command.args, - runInShell: command.runInShell, - ); - if (result.exitCode != 0) { - return false; - } + for (final command in commands) { + executed = true; + if (!await _succeeds(command) && !command.optional) { + return false; } - } on ProcessException { - return false; } return executed; } + + Future _succeeds(ProxyCommand command) async { + try { + final result = await process( + command.executable, + command.args, + runInShell: command.runInShell, + ); + return result.exitCode == 0; + } on ProcessException { + return false; + } + } } diff --git a/plugins/proxy/lib/src/windows_proxy.dart b/plugins/proxy/lib/src/windows_proxy.dart new file mode 100644 index 00000000..4a0bfc7e --- /dev/null +++ b/plugins/proxy/lib/src/windows_proxy.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +/// WinINet rejects the whole list over one bare IPv6 literal, so the system +/// proxy is not set at all, and its `` token for single-label intranet +/// hosts like `nas` has no cross-platform pattern. Loopback needs no entry: +/// WinINet never proxies it. +List windowsBypassList(List bypassDomain) { + final entries = {}; + for (final domain in bypassDomain) { + final entry = domain.trim(); + if (entry.isEmpty) continue; + final isBareIPv6 = + InternetAddress.tryParse(entry)?.type == InternetAddressType.IPv6; + entries.add(isBareIPv6 ? '[$entry]' : entry); + } + return {...entries, ''}.toList(); +} diff --git a/plugins/proxy/test/proxy_test.dart b/plugins/proxy/test/proxy_test.dart index 31265f39..c30bd9f7 100644 --- a/plugins/proxy/test/proxy_test.dart +++ b/plugins/proxy/test/proxy_test.dart @@ -7,6 +7,7 @@ import 'package:proxy/proxy_method_channel.dart'; import 'package:proxy/src/linux_proxy.dart'; import 'package:proxy/src/macos_proxy.dart'; import 'package:proxy/src/proxy_command.dart'; +import 'package:proxy/src/windows_proxy.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -68,6 +69,30 @@ void main() { expect(await runner.run([ProxyCommand('missing', const [])]), isFalse); }); + + test('carries on past an optional command that fails', () async { + final started = []; + final runner = ProxyCommandRunner(( + executable, + arguments, { + runInShell = false, + }) async { + started.add(executable); + if (executable == 'missing') { + throw ProcessException(executable, arguments); + } + return ProcessResult(0, executable == 'refused' ? 1 : 0, '', ''); + }); + + final result = await runner.run([ + ProxyCommand('missing', const [], optional: true), + ProxyCommand('refused', const [], optional: true), + ProxyCommand('last', const []), + ]); + + expect(result, isTrue); + expect(started, ['missing', 'refused', 'last']); + }); }); group('Linux proxy command builders', () { @@ -104,7 +129,7 @@ void main() { 'kwriteconfig6', 'kwriteconfig5', ]); - expect(executedCommands, everyElement('kwriteconfig5')); + expect(executedCommands.toSet(), {'kwriteconfig5', 'dbus-send'}); }, ); @@ -237,6 +262,52 @@ void main() { expect(commands.map((command) => command.executable).toSet(), { 'kwriteconfig6', + 'dbus-send', + }); + }); + + test('points every KDE proxy at the plain HTTP or SOCKS listener and ' + 'tells running KIO programs to reload', () { + for (final commands in [ + LinuxProxyCommands.buildStart( + port: 7890, + bypassDomain: ['localhost'], + desktop: 'KDE', + homeDir: '/home/user', + availableExecutables: {'kwriteconfig6'}, + ), + LinuxProxyCommands.buildStop( + desktop: 'KDE', + homeDir: '/home/user', + availableExecutables: {'kwriteconfig6'}, + ), + ]) { + final reload = commands.last; + expect(reload.executable, 'dbus-send'); + expect(reload.args, [ + '--session', + '--type=signal', + '/KIO/Scheduler', + 'org.kde.KIO.Scheduler.reparseSlaveConfiguration', + 'string:', + ]); + expect(reload.optional, isTrue); + } + final proxies = { + for (final command in LinuxProxyCommands.buildStart( + port: 7890, + bypassDomain: ['localhost'], + desktop: 'KDE', + homeDir: '/home/user', + availableExecutables: {'kwriteconfig6'}, + )) + if (command.args.length > 6 && command.args[5].endsWith('Proxy')) + command.args[5]: command.args[6], + }; + expect(proxies, { + 'httpProxy': 'http://127.0.0.1:7890', + 'httpsProxy': 'http://127.0.0.1:7890', + 'socksProxy': 'socks://127.0.0.1:7890', }); }); @@ -253,6 +324,7 @@ void main() { expect(commands.map((command) => command.executable).toSet(), { 'kwriteconfig5', + 'dbus-send', }); }, ); @@ -268,6 +340,7 @@ void main() { expect(commands.map((command) => command.executable).toSet(), { 'kwriteconfig5', + 'dbus-send', }); }); @@ -357,6 +430,24 @@ USB 10/100/1000 LAN }); }); + group('windowsBypassList', () { + test('adds the intranet token once', () { + expect(windowsBypassList(['localhost', '127.*', '']), [ + 'localhost', + '127.*', + '', + ]); + expect(windowsBypassList(['localhost']), ['localhost', '']); + }); + + test('brackets bare IPv6 literals and drops blank entries', () { + expect( + windowsBypassList([' ::1 ', 'fe80::1', '', '[fd00::1]', '*.lan']), + ['[::1]', '[fe80::1]', '[fd00::1]', '*.lan', ''], + ); + }); + }); + group('MethodChannelProxy', () { final proxy = MethodChannelProxy(); diff --git a/plugins/proxy/windows/CMakeLists.txt b/plugins/proxy/windows/CMakeLists.txt index 9e56237f..74ae0911 100644 --- a/plugins/proxy/windows/CMakeLists.txt +++ b/plugins/proxy/windows/CMakeLists.txt @@ -46,7 +46,7 @@ target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) # dependencies here. target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin wininet rasapi32) +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin wininet rasapi32 iphlpapi) # List of absolute paths to libraries that should be bundled with the plugin. # This list could contain prebuilt libraries, or libraries created by an @@ -86,7 +86,7 @@ add_executable(${TEST_RUNNER} ) apply_standard_settings(${TEST_RUNNER}) target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin wininet rasapi32) +target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin wininet rasapi32 iphlpapi) target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) # flutter_wrapper_plugin has link dependencies on the Flutter DLL. add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD diff --git a/plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h b/plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h index 3eca0100..350a5e1f 100644 --- a/plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h +++ b/plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h @@ -16,6 +16,9 @@ extern "C" { FLUTTER_PLUGIN_EXPORT void ProxyPluginCApiRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); +// Safe pre-engine. +FLUTTER_PLUGIN_EXPORT bool ProxyPluginClearStaleProxy(); + #if defined(__cplusplus) } // extern "C" #endif diff --git a/plugins/proxy/windows/proxy_plugin.cpp b/plugins/proxy/windows/proxy_plugin.cpp index c6cf5081..2469d4a0 100644 --- a/plugins/proxy/windows/proxy_plugin.cpp +++ b/plugins/proxy/windows/proxy_plugin.cpp @@ -1,3 +1,8 @@ +// The IPv6 listener table needs winsock2.h, which has to come before the +// windows.h that proxy_plugin.h pulls in. +#include +#include + #include "proxy_plugin.h" // This must be included before many other Windows headers. @@ -6,12 +11,17 @@ #include #include #include +#include +#include #include +#include +#include #include #include #pragma comment(lib, "wininet") #pragma comment(lib, "Rasapi32") +#pragma comment(lib, "iphlpapi") #include #include @@ -24,6 +34,7 @@ namespace constexpr int kMinProxyPort = 1; constexpr int kMaxProxyPort = 65535; +constexpr wchar_t kProxyServerPrefix[] = L"127.0.0.1:"; std::wstring Utf8ToWide(const std::string& value) { @@ -80,9 +91,9 @@ bool SetOptionsForConnection( sizeof(list)) != FALSE; } -bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list) +bool ForEachConnection(const std::function& visit) { - bool success = SetOptionsForConnection(list, nullptr); + bool success = visit(nullptr); DWORD size = 0; DWORD count = 0; @@ -99,7 +110,7 @@ bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list) { for (DWORD i = 0; i < count; i++) { - success = SetOptionsForConnection(list, entries[i].szEntryName) && success; + success = visit(entries[i].szEntryName) && success; } } else @@ -115,6 +126,14 @@ bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list) return success; } +bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list) +{ + return ForEachConnection([&list](LPTSTR connection) + { + return SetOptionsForConnection(list, connection); + }); +} + bool NotifySettingsChanged() { const bool changed = InternetSetOption( @@ -126,7 +145,7 @@ bool NotifySettingsChanged() bool startProxy(const int port, const flutter::EncodableList& bypassDomain) { - auto url = Utf8ToWide("127.0.0.1:" + std::to_string(port)); + auto url = kProxyServerPrefix + std::to_wstring(port); auto bypassList = BuildBypassList(bypassDomain); std::vector options(3); @@ -166,11 +185,165 @@ bool stopProxy() return optionsApplied && settingsNotified; } +std::optional ProxyPort(const std::wstring& server) +{ + const std::wstring prefix = kProxyServerPrefix; + if (server.size() <= prefix.size() || server.size() > prefix.size() + 5 || + server.compare(0, prefix.size(), prefix) != 0) + { + return std::nullopt; + } + int port = 0; + for (size_t i = prefix.size(); i < server.size(); i++) + { + if (server[i] < L'0' || server[i] > L'9') + { + return std::nullopt; + } + port = port * 10 + (server[i] - L'0'); + } + if (port < kMinProxyPort || port > kMaxProxyPort) + { + return std::nullopt; + } + return port; +} + +template +bool TableHasPort(const std::vector& buffer, int port) +{ + const auto* table = reinterpret_cast(buffer.data()); + for (DWORD i = 0; i < table->dwNumEntries; i++) + { + const auto localPort = + _byteswap_ushort(static_cast(table->table[i].dwLocalPort)); + if (localPort == port) + { + return true; + } + } + return false; +} + +std::optional HasTcpListener(ULONG family, int port) +{ + std::vector buffer; + DWORD size = 0; + DWORD result = ERROR_INSUFFICIENT_BUFFER; + for (int attempt = 0; attempt < 3 && result == ERROR_INSUFFICIENT_BUFFER; + attempt++) + { + buffer.resize(size); + result = GetExtendedTcpTable( + buffer.empty() ? nullptr : buffer.data(), &size, FALSE, family, + TCP_TABLE_OWNER_PID_LISTENER, 0); + } + if (result != NO_ERROR) + { + return std::nullopt; + } + return family == AF_INET + ? TableHasPort(buffer, port) + : TableHasPort(buffer, port); +} + +// A lookup that fails counts as listening, so a live proxy is never cleared. +bool IsPortListening(int port) +{ + for (const ULONG family : {static_cast(AF_INET), + static_cast(AF_INET6)}) + { + if (HasTcpListener(family, port).value_or(true)) + { + return true; + } + } + return false; +} + +// taskkill /f returns once TerminateProcess has been issued, which is before +// the killed Core has released its listening socket. +bool IsPortReleased(int port) +{ + for (int attempt = 0; attempt < 20; attempt++) + { + if (!IsPortListening(port)) + { + return true; + } + Sleep(100); + } + return false; +} + +bool ClearStaleProxyForConnection( + LPTSTR connection, + std::map& releasedPorts) +{ + std::vector options(2); + options[0].dwOption = INTERNET_PER_CONN_FLAGS; + options[1].dwOption = INTERNET_PER_CONN_PROXY_SERVER; + + INTERNET_PER_CONN_OPTION_LIST list = {}; + list.dwSize = sizeof(list); + list.pszConnection = connection; + list.dwOptionCount = static_cast(options.size()); + list.pOptions = options.data(); + + DWORD size = sizeof(list); + if (InternetQueryOption( + nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, &size) == + FALSE) + { + return false; + } + const DWORD flags = options[0].Value.dwValue; + std::wstring server; + if (options[1].Value.pszValue != nullptr) + { + server = options[1].Value.pszValue; + GlobalFree(options[1].Value.pszValue); + } + + const auto port = ProxyPort(server); + if ((flags & PROXY_TYPE_PROXY) == 0 || !port) + { + return true; + } + auto [released, unchecked] = releasedPorts.try_emplace(*port, false); + if (unchecked) + { + released->second = IsPortReleased(*port); + } + if (!released->second) + { + return true; + } + INTERNET_PER_CONN_OPTION option = {}; + option.dwOption = INTERNET_PER_CONN_FLAGS; + option.Value.dwValue = (flags & ~PROXY_TYPE_PROXY) | PROXY_TYPE_DIRECT; + list.dwOptionCount = 1; + list.pOptions = &option; + return SetOptionsForConnection(list, connection); +} + } // namespace namespace proxy { + bool ClearStaleProxy() + { + std::map releasedPorts; + const bool cleared = ForEachConnection( + [&releasedPorts](LPTSTR connection) + { + return ClearStaleProxyForConnection(connection, releasedPorts); + }); + const bool settingsNotified = NotifySettingsChanged(); + return cleared && settingsNotified; + } + // static void ProxyPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows *registrar) diff --git a/plugins/proxy/windows/proxy_plugin.h b/plugins/proxy/windows/proxy_plugin.h index 7a2252a1..083dc8de 100644 --- a/plugins/proxy/windows/proxy_plugin.h +++ b/plugins/proxy/windows/proxy_plugin.h @@ -11,6 +11,10 @@ namespace proxy { +// Turns off a system proxy that still points at FlClash's address while +// nothing listens on its port any more, the state a killed app leaves behind. +bool ClearStaleProxy(); + class ProxyPlugin : public flutter::Plugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); diff --git a/plugins/proxy/windows/proxy_plugin_c_api.cpp b/plugins/proxy/windows/proxy_plugin_c_api.cpp index 4f398777..3ff5ea5b 100644 --- a/plugins/proxy/windows/proxy_plugin_c_api.cpp +++ b/plugins/proxy/windows/proxy_plugin_c_api.cpp @@ -10,3 +10,7 @@ void ProxyPluginCApiRegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); } + +bool ProxyPluginClearStaleProxy() { + return proxy::ClearStaleProxy(); +} diff --git a/plugins/rust_api/lib/src/rust/api/ipc.dart b/plugins/rust_api/lib/src/rust/api/ipc.dart index 0d53811d..82c7d24f 100644 --- a/plugins/rust_api/lib/src/rust/api/ipc.dart +++ b/plugins/rust_api/lib/src/rust/api/ipc.dart @@ -6,10 +6,46 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -Stream restartIpcServer({required String name}) => - RustLib.instance.api.crateApiIpcRestartIpcServer(name: name); +// These functions are ignored because they are not marked as `pub`: `bridge` -Future stopIpcServer() => RustLib.instance.api.crateApiIpcStopIpcServer(); +// Rust type: RustOpaqueMoi> +abstract class IpcServer implements RustOpaqueInterface { + static Future bind({required String address}) => + RustLib.instance.api.crateApiIpcIpcServerBind(address: address); -Future sendIpcMessage({required List data}) => - RustLib.instance.api.crateApiIpcSendIpcMessage(data: data); + Future close(); + + Stream events(); + + void send({required List message}); +} + +class IpcEvent { + final IpcEventKind kind; + final int? pid; + final Uint8List payload; + final String? error; + + const IpcEvent({ + required this.kind, + this.pid, + required this.payload, + this.error, + }); + + @override + int get hashCode => + kind.hashCode ^ pid.hashCode ^ payload.hashCode ^ error.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is IpcEvent && + runtimeType == other.runtimeType && + kind == other.kind && + pid == other.pid && + payload == other.payload && + error == other.error; +} + +enum IpcEventKind { connected, message, disconnected, failed } diff --git a/plugins/rust_api/lib/src/rust/frb_generated.dart b/plugins/rust_api/lib/src/rust/frb_generated.dart index 8bc619ac..77561aae 100644 --- a/plugins/rust_api/lib/src/rust/frb_generated.dart +++ b/plugins/rust_api/lib/src/rust/frb_generated.dart @@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.13.0'; @override - int get rustContentHash => -1283005629; + int get rustContentHash => 736531895; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -81,6 +81,17 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { + Future crateApiIpcIpcServerBind({required String address}); + + Future crateApiIpcIpcServerClose({required IpcServer that}); + + Stream crateApiIpcIpcServerEvents({required IpcServer that}); + + void crateApiIpcIpcServerSend({ + required IpcServer that, + required List message, + }); + List crateApiEditorRopeBridgeCachedLinesRange({ required RopeBridge that, required BigInt startLine, @@ -154,18 +165,20 @@ abstract class RustLibApi extends BaseApi { Future crateApiInitInitApp(); - Stream crateApiIpcRestartIpcServer({required String name}); - - Future crateApiIpcSendIpcMessage({required List data}); - Future> crateApiHotkeySetHotKeys({ required List specs, }); - Future crateApiIpcStopIpcServer(); - Future> crateApiEditorWordsExtract({required RopeBridge rope}); + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_IpcServer; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_IpcServer; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_IpcServerPtr; + RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_RopeBridge; @@ -183,6 +196,136 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required super.portManager, }); + @override + Future crateApiIpcIpcServerBind({required String address}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(address, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 1, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiIpcIpcServerBindConstMeta, + argValues: [address], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiIpcIpcServerBindConstMeta => + const TaskConstMeta(debugName: "IpcServer_bind", argNames: ["address"]); + + @override + Future crateApiIpcIpcServerClose({required IpcServer that}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + that, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 2, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiIpcIpcServerCloseConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiIpcIpcServerCloseConstMeta => + const TaskConstMeta(debugName: "IpcServer_close", argNames: ["that"]); + + @override + Stream crateApiIpcIpcServerEvents({required IpcServer that}) { + final sink = RustStreamSink(); + unawaited( + handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + that, + serializer, + ); + sse_encode_StreamSink_ipc_event_Sse(sink, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 3, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiIpcIpcServerEventsConstMeta, + argValues: [that, sink], + apiImpl: this, + ), + ), + ); + return sink.stream; + } + + TaskConstMeta get kCrateApiIpcIpcServerEventsConstMeta => const TaskConstMeta( + debugName: "IpcServer_events", + argNames: ["that", "sink"], + ); + + @override + void crateApiIpcIpcServerSend({ + required IpcServer that, + required List message, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(message, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiIpcIpcServerSendConstMeta, + argValues: [that, message], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiIpcIpcServerSendConstMeta => const TaskConstMeta( + debugName: "IpcServer_send", + argNames: ["that", "message"], + ); + @override List crateApiEditorRopeBridgeCachedLinesRange({ required RopeBridge that, @@ -199,7 +342,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_usize(startLine, serializer); sse_encode_usize(endLine, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_String, @@ -232,7 +375,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_usize(charIdx, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; }, codec: SseCodec( decodeSuccessData: sse_decode_usize, @@ -258,7 +401,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(initialText, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; }, codec: SseCodec( decodeSuccessData: @@ -288,7 +431,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -320,7 +463,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_usize(charIdx, serializer); sse_encode_String(text, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -349,7 +492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; }, codec: SseCodec( decodeSuccessData: sse_decode_usize, @@ -378,7 +521,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; }, codec: SseCodec( decodeSuccessData: sse_decode_usize, @@ -411,7 +554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_usize(lineIdx, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -444,7 +587,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_usize(lineIdx, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; }, codec: SseCodec( decodeSuccessData: sse_decode_usize, @@ -479,7 +622,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_usize(start, serializer); sse_encode_usize(end, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -514,7 +657,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_usize(start, serializer); sse_encode_usize(end, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -547,7 +690,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 12, + funcId: 16, port: port_, ); }, @@ -585,7 +728,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 17, port: port_, ); }, @@ -620,7 +763,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_usize(targetOffset, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_box_autoadd_usize, @@ -657,7 +800,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_usize(firstVisible, serializer); sse_encode_usize(lastVisible, serializer); sse_encode_usize(tabSize, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_guide_block, @@ -688,7 +831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 20, port: port_, ); }, @@ -717,7 +860,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 17, + funcId: 21, port: port_, ); }, @@ -735,70 +878,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiInitInitAppConstMeta => const TaskConstMeta(debugName: "init_app", argNames: []); - @override - Stream crateApiIpcRestartIpcServer({required String name}) { - final sink = RustStreamSink(); - unawaited( - handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(name, serializer); - sse_encode_StreamSink_list_prim_u_8_strict_Sse(sink, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 18, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_String, - ), - constMeta: kCrateApiIpcRestartIpcServerConstMeta, - argValues: [name, sink], - apiImpl: this, - ), - ), - ); - return sink.stream; - } - - TaskConstMeta get kCrateApiIpcRestartIpcServerConstMeta => - const TaskConstMeta( - debugName: "restart_ipc_server", - argNames: ["name", "sink"], - ); - - @override - Future crateApiIpcSendIpcMessage({required List data}) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_prim_u_8_loose(data, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 19, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_String, - ), - constMeta: kCrateApiIpcSendIpcMessageConstMeta, - argValues: [data], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiIpcSendIpcMessageConstMeta => - const TaskConstMeta(debugName: "send_ipc_message", argNames: ["data"]); - @override Future> crateApiHotkeySetHotKeys({ required List specs, @@ -811,7 +890,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 22, port: port_, ); }, @@ -829,33 +908,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiHotkeySetHotKeysConstMeta => const TaskConstMeta(debugName: "set_hot_keys", argNames: ["specs"]); - @override - Future crateApiIpcStopIpcServer() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 21, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_String, - ), - constMeta: kCrateApiIpcStopIpcServerConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiIpcStopIpcServerConstMeta => - const TaskConstMeta(debugName: "stop_ipc_server", argNames: []); - @override Future> crateApiEditorWordsExtract({required RopeBridge rope}) { return handler.executeNormal( @@ -869,7 +921,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 23, port: port_, ); }, @@ -887,6 +939,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiEditorWordsExtractConstMeta => const TaskConstMeta(debugName: "words_extract", argNames: ["rope"]); + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_IpcServer => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_IpcServer => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer; + RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_RopeBridge => wire .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge; @@ -901,6 +961,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return AnyhowException(raw as String); } + @protected + IpcServer + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return IpcServerImpl.frbInternalDcoDecode(raw as List); + } + @protected RopeBridge dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -910,6 +979,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return RopeBridgeImpl.frbInternalDcoDecode(raw as List); } + @protected + IpcServer + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return IpcServerImpl.frbInternalDcoDecode(raw as List); + } + @protected RopeBridge dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -919,6 +997,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return RopeBridgeImpl.frbInternalDcoDecode(raw as List); } + @protected + IpcServer + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return IpcServerImpl.frbInternalDcoDecode(raw as List); + } + @protected RopeBridge dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -929,9 +1016,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - RustStreamSink dco_decode_StreamSink_list_prim_u_8_strict_Sse( - dynamic raw, - ) { + RustStreamSink dco_decode_StreamSink_ipc_event_Sse(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs throw UnimplementedError(); } @@ -948,6 +1033,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as String; } + @protected + int dco_decode_box_autoadd_u_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + @protected BigInt dco_decode_box_autoadd_usize(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1005,6 +1096,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as int; } + @protected + IpcEvent dco_decode_ipc_event(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 4) + throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + return IpcEvent( + kind: dco_decode_ipc_event_kind(arr[0]), + pid: dco_decode_opt_box_autoadd_u_32(arr[1]), + payload: dco_decode_list_prim_u_8_strict(arr[2]), + error: dco_decode_opt_String(arr[3]), + ); + } + + @protected + IpcEventKind dco_decode_ipc_event_kind(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return IpcEventKind.values[raw as int]; + } + @protected List dco_decode_list_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1053,6 +1164,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (raw as List).map(dco_decode_rust_fold_range).toList(); } + @protected + String? dco_decode_opt_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_String(raw); + } + + @protected + int? dco_decode_opt_box_autoadd_u_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_u_32(raw); + } + @protected BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1102,6 +1225,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return AnyhowException(inner); } + @protected + IpcServer + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return IpcServerImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected RopeBridge sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -1114,6 +1249,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + IpcServer + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return IpcServerImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected RopeBridge sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -1126,6 +1273,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + IpcServer + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return IpcServerImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected RopeBridge sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -1139,7 +1298,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - RustStreamSink sse_decode_StreamSink_list_prim_u_8_strict_Sse( + RustStreamSink sse_decode_StreamSink_ipc_event_Sse( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1161,6 +1320,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return utf8.decoder.convert(inner); } + @protected + int sse_decode_box_autoadd_u_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_u_32(deserializer)); + } + @protected BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1212,6 +1377,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getInt32(); } + @protected + IpcEvent sse_decode_ipc_event(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_kind = sse_decode_ipc_event_kind(deserializer); + var var_pid = sse_decode_opt_box_autoadd_u_32(deserializer); + var var_payload = sse_decode_list_prim_u_8_strict(deserializer); + var var_error = sse_decode_opt_String(deserializer); + return IpcEvent( + kind: var_kind, + pid: var_pid, + payload: var_payload, + error: var_error, + ); + } + + @protected + IpcEventKind sse_decode_ipc_event_kind(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return IpcEventKind.values[inner]; + } + @protected List sse_decode_list_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1304,6 +1491,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + String? sse_decode_opt_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_String(deserializer)); + } else { + return null; + } + } + + @protected + int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_u_32(deserializer)); + } else { + return null; + } + } + @protected BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1361,6 +1570,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.message, serializer); } + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as IpcServerImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -1374,6 +1596,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as IpcServerImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -1387,6 +1622,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as IpcServerImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -1401,15 +1649,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - void sse_encode_StreamSink_list_prim_u_8_strict_Sse( - RustStreamSink self, + void sse_encode_StreamSink_ipc_event_Sse( + RustStreamSink self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_String( self.setupAndSerialize( codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeSuccessData: sse_decode_ipc_event, decodeErrorData: sse_decode_AnyhowException, ), ), @@ -1440,6 +1688,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); } + @protected + void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_32(self, serializer); + } + @protected void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1488,6 +1742,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putInt32(self); } + @protected + void sse_encode_ipc_event(IpcEvent self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_ipc_event_kind(self.kind, serializer); + sse_encode_opt_box_autoadd_u_32(self.pid, serializer); + sse_encode_list_prim_u_8_strict(self.payload, serializer); + sse_encode_opt_String(self.error, serializer); + } + + @protected + void sse_encode_ipc_event_kind(IpcEventKind self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + @protected void sse_encode_list_String(List self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1579,6 +1848,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_String(self, serializer); + } + } + + @protected + void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_u_32(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_usize( BigInt? self, @@ -1632,6 +1921,35 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } +@sealed +class IpcServerImpl extends RustOpaque implements IpcServer { + // Not to be used by end users + IpcServerImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + IpcServerImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_IpcServer, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_IpcServer, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_IpcServerPtr, + ); + + Future close() => + RustLib.instance.api.crateApiIpcIpcServerClose(that: this); + + Stream events() => + RustLib.instance.api.crateApiIpcIpcServerEvents(that: this); + + void send({required List message}) => RustLib.instance.api + .crateApiIpcIpcServerSend(that: this, message: message); +} + @sealed class RopeBridgeImpl extends RustOpaque implements RopeBridge { // Not to be used by end users diff --git a/plugins/rust_api/lib/src/rust/frb_generated.io.dart b/plugins/rust_api/lib/src/rust/frb_generated.io.dart index d2ece6d0..6f343d9c 100644 --- a/plugins/rust_api/lib/src/rust/frb_generated.io.dart +++ b/plugins/rust_api/lib/src/rust/frb_generated.io.dart @@ -21,6 +21,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { required super.portManager, }); + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_IpcServerPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServerPtr; + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RopeBridgePtr => wire ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridgePtr; @@ -28,18 +32,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException dco_decode_AnyhowException(dynamic raw); + @protected + IpcServer + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ); + @protected RopeBridge dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dynamic raw, ); + @protected + IpcServer + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ); + @protected RopeBridge dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dynamic raw, ); + @protected + IpcServer + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ); + @protected RopeBridge dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -47,9 +69,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - RustStreamSink dco_decode_StreamSink_list_prim_u_8_strict_Sse( - dynamic raw, - ); + RustStreamSink dco_decode_StreamSink_ipc_event_Sse(dynamic raw); @protected RustStreamSink dco_decode_StreamSink_u_32_Sse(dynamic raw); @@ -57,6 +77,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String dco_decode_String(dynamic raw); + @protected + int dco_decode_box_autoadd_u_32(dynamic raw); + @protected BigInt dco_decode_box_autoadd_usize(dynamic raw); @@ -75,6 +98,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_i_32(dynamic raw); + @protected + IpcEvent dco_decode_ipc_event(dynamic raw); + + @protected + IpcEventKind dco_decode_ipc_event_kind(dynamic raw); + @protected List dco_decode_list_String(dynamic raw); @@ -99,6 +128,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_rust_fold_range(dynamic raw); + @protected + String? dco_decode_opt_String(dynamic raw); + + @protected + int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); @@ -120,18 +155,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + @protected + IpcServer + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ); + @protected RopeBridge sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( SseDeserializer deserializer, ); + @protected + IpcServer + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ); + @protected RopeBridge sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( SseDeserializer deserializer, ); + @protected + IpcServer + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ); + @protected RopeBridge sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -139,7 +192,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - RustStreamSink sse_decode_StreamSink_list_prim_u_8_strict_Sse( + RustStreamSink sse_decode_StreamSink_ipc_event_Sse( SseDeserializer deserializer, ); @@ -151,6 +204,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String sse_decode_String(SseDeserializer deserializer); + @protected + int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); @@ -169,6 +225,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_i_32(SseDeserializer deserializer); + @protected + IpcEvent sse_decode_ipc_event(SseDeserializer deserializer); + + @protected + IpcEventKind sse_decode_ipc_event_kind(SseDeserializer deserializer); + @protected List sse_decode_list_String(SseDeserializer deserializer); @@ -199,6 +261,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + + @protected + int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); @@ -226,6 +294,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -233,6 +308,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -240,6 +322,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ); + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -248,8 +337,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - void sse_encode_StreamSink_list_prim_u_8_strict_Sse( - RustStreamSink self, + void sse_encode_StreamSink_ipc_event_Sse( + RustStreamSink self, SseSerializer serializer, ); @@ -262,6 +351,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_String(String self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); @@ -283,6 +375,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_32(int self, SseSerializer serializer); + @protected + void sse_encode_ipc_event(IpcEvent self, SseSerializer serializer); + + @protected + void sse_encode_ipc_event_kind(IpcEventKind self, SseSerializer serializer); + @protected void sse_encode_list_String(List self, SseSerializer serializer); @@ -325,6 +423,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + + @protected + void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); @@ -361,6 +465,40 @@ class RustLibWire implements BaseWire { RustLibWire(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServerPtr = + _lookup)>>( + 'frbgen_rust_api_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServerPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServerPtr = + _lookup)>>( + 'frbgen_rust_api_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServerPtr + .asFunction)>(); + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( ffi.Pointer ptr, diff --git a/plugins/rust_api/lib/src/rust/frb_generated.web.dart b/plugins/rust_api/lib/src/rust/frb_generated.web.dart index 3bcd736d..983b4b35 100644 --- a/plugins/rust_api/lib/src/rust/frb_generated.web.dart +++ b/plugins/rust_api/lib/src/rust/frb_generated.web.dart @@ -23,6 +23,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { required super.portManager, }); + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_IpcServerPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer; + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RopeBridgePtr => wire .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge; @@ -30,18 +34,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException dco_decode_AnyhowException(dynamic raw); + @protected + IpcServer + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ); + @protected RopeBridge dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dynamic raw, ); + @protected + IpcServer + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ); + @protected RopeBridge dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dynamic raw, ); + @protected + IpcServer + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + dynamic raw, + ); + @protected RopeBridge dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -49,9 +71,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - RustStreamSink dco_decode_StreamSink_list_prim_u_8_strict_Sse( - dynamic raw, - ); + RustStreamSink dco_decode_StreamSink_ipc_event_Sse(dynamic raw); @protected RustStreamSink dco_decode_StreamSink_u_32_Sse(dynamic raw); @@ -59,6 +79,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String dco_decode_String(dynamic raw); + @protected + int dco_decode_box_autoadd_u_32(dynamic raw); + @protected BigInt dco_decode_box_autoadd_usize(dynamic raw); @@ -77,6 +100,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_i_32(dynamic raw); + @protected + IpcEvent dco_decode_ipc_event(dynamic raw); + + @protected + IpcEventKind dco_decode_ipc_event_kind(dynamic raw); + @protected List dco_decode_list_String(dynamic raw); @@ -101,6 +130,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_rust_fold_range(dynamic raw); + @protected + String? dco_decode_opt_String(dynamic raw); + + @protected + int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); @@ -122,18 +157,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + @protected + IpcServer + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ); + @protected RopeBridge sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( SseDeserializer deserializer, ); + @protected + IpcServer + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ); + @protected RopeBridge sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( SseDeserializer deserializer, ); + @protected + IpcServer + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + SseDeserializer deserializer, + ); + @protected RopeBridge sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -141,7 +194,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - RustStreamSink sse_decode_StreamSink_list_prim_u_8_strict_Sse( + RustStreamSink sse_decode_StreamSink_ipc_event_Sse( SseDeserializer deserializer, ); @@ -153,6 +206,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String sse_decode_String(SseDeserializer deserializer); + @protected + int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); @@ -171,6 +227,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_i_32(SseDeserializer deserializer); + @protected + IpcEvent sse_decode_ipc_event(SseDeserializer deserializer); + + @protected + IpcEventKind sse_decode_ipc_event_kind(SseDeserializer deserializer); + @protected List sse_decode_list_String(SseDeserializer deserializer); @@ -201,6 +263,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + + @protected + int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); @@ -228,6 +296,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -235,6 +310,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -242,6 +324,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + IpcServer self, + SseSerializer serializer, + ); + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( @@ -250,8 +339,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - void sse_encode_StreamSink_list_prim_u_8_strict_Sse( - RustStreamSink self, + void sse_encode_StreamSink_ipc_event_Sse( + RustStreamSink self, SseSerializer serializer, ); @@ -264,6 +353,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_String(String self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); @@ -285,6 +377,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_32(int self, SseSerializer serializer); + @protected + void sse_encode_ipc_event(IpcEvent self, SseSerializer serializer); + + @protected + void sse_encode_ipc_event_kind(IpcEventKind self, SseSerializer serializer); + @protected void sse_encode_list_String(List self, SseSerializer serializer); @@ -327,6 +425,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + + @protected + void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); @@ -354,6 +458,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { class RustLibWire implements BaseWire { RustLibWire.fromExternalLibrary(ExternalLibrary lib); + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ptr, + ); + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( int ptr, @@ -377,6 +497,16 @@ external RustLibWasmModule get wasmModule; @JS() @anonymous extension type RustLibWasmModule._(JSObject _) implements JSObject { + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + int ptr, + ); + external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( int ptr, diff --git a/plugins/rust_api/rust/src/api/ipc.rs b/plugins/rust_api/rust/src/api/ipc.rs index fa373d77..e66873cf 100644 --- a/plugins/rust_api/rust/src/api/ipc.rs +++ b/plugins/rust_api/rust/src/api/ipc.rs @@ -3,17 +3,59 @@ use crate::ipc; use flutter_rust_bridge::for_generated::SseCodec; use flutter_rust_bridge::frb; -#[frb] -pub fn restart_ipc_server(name: String, sink: StreamSink, SseCodec>) -> Result<(), String> { - ipc::restart_server(name, sink) +pub enum IpcEventKind { + Connected, + Message, + Disconnected, + Failed, } -#[frb] -pub fn stop_ipc_server() -> Result<(), String> { - ipc::stop_server() +pub struct IpcEvent { + pub kind: IpcEventKind, + pub pid: Option, + pub payload: Vec, + pub error: Option, } -#[frb] -pub fn send_ipc_message(data: Vec) -> Result<(), String> { - ipc::send_message(data) +fn bridge(event: ipc::Event) -> IpcEvent { + let (kind, pid, payload, error) = match event { + ipc::Event::Connected { pid } => (IpcEventKind::Connected, pid, Vec::new(), None), + ipc::Event::Message(payload) => (IpcEventKind::Message, None, payload, None), + ipc::Event::Disconnected { error } => (IpcEventKind::Disconnected, None, Vec::new(), error), + ipc::Event::Failed(error) => (IpcEventKind::Failed, None, Vec::new(), Some(error)), + }; + IpcEvent { + kind, + pid, + payload, + error, + } +} + +#[frb(opaque)] +pub struct IpcServer { + server: ipc::Server, +} + +impl IpcServer { + pub fn bind(address: String) -> Result { + let server = ipc::Server::bind(address).map_err(|error| error.to_string())?; + Ok(Self { server }) + } + + pub fn events(&self, sink: StreamSink) { + self.server + .events(Box::new(move |event| sink.add(bridge(event)).is_ok())); + } + + #[frb(sync)] + pub fn send(&self, message: Vec) -> Result<(), String> { + self.server + .send(&message) + .map_err(|error| error.to_string()) + } + + pub fn close(&self) { + self.server.close(); + } } diff --git a/plugins/rust_api/rust/src/frb_generated.rs b/plugins/rust_api/rust/src/frb_generated.rs index 2dbe3051..7b5ede7e 100644 --- a/plugins/rust_api/rust/src/frb_generated.rs +++ b/plugins/rust_api/rust/src/frb_generated.rs @@ -28,6 +28,7 @@ // Section: imports use crate::api::editor::*; +use crate::api::ipc::*; use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; use flutter_rust_bridge::{Handler, IntoIntoDart}; @@ -40,7 +41,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.13.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1283005629; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 736531895; // Section: executor @@ -48,6 +49,192 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs +fn wire__crate__api__ipc__IpcServer_bind_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "IpcServer_bind", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_address = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::ipc::IpcServer::bind(api_address)?; + std::result::Result::Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__ipc__IpcServer_close_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "IpcServer_close", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>({ + crate::api::ipc::IpcServer::close(&*api_that_guard); + })?; + std::result::Result::Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__ipc__IpcServer_events_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "IpcServer_events", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_sink = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>({ + crate::api::ipc::IpcServer::events(&*api_that_guard, api_sink); + })?; + std::result::Result::Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__ipc__IpcServer_send_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "IpcServer_send", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_message = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::api::ipc::IpcServer::send(&*api_that_guard, api_message)?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__editor__RopeBridge_cached_lines_range_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -839,76 +1026,6 @@ fn wire__crate__api__init__init_app_impl( }, ) } -fn wire__crate__api__ipc__restart_ipc_server_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "restart_ipc_server", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_name = ::sse_decode(&mut deserializer); - let api_sink = - , flutter_rust_bridge::for_generated::SseCodec>>::sse_decode( - &mut deserializer, - ); - deserializer.end(); - move |context| { - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::ipc::restart_ipc_server(api_name, api_sink)?; - std::result::Result::Ok(output_ok) - })()) - } - }, - ) -} -fn wire__crate__api__ipc__send_ipc_message_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "send_ipc_message", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_data = >::sse_decode(&mut deserializer); - deserializer.end(); - move |context| { - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::ipc::send_ipc_message(api_data)?; - std::result::Result::Ok(output_ok) - })()) - } - }, - ) -} fn wire__crate__api__hotkey__set_hot_keys_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -942,38 +1059,6 @@ fn wire__crate__api__hotkey__set_hot_keys_impl( }, ) } -fn wire__crate__api__ipc__stop_ipc_server_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "stop_ipc_server", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - deserializer.end(); - move |context| { - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::ipc::stop_ipc_server()?; - std::result::Result::Ok(output_ok) - })()) - } - }, - ) -} fn wire__crate__api__editor__words_extract_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1027,6 +1112,9 @@ fn wire__crate__api__editor__words_extract_impl( // Section: related_funcs +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::for_generated::RustAutoOpaqueInner ); @@ -1041,6 +1129,16 @@ impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { } } +impl SseDecode for IpcServer { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + impl SseDecode for RopeBridge { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1051,6 +1149,16 @@ impl SseDecode for RopeBridge { } } +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + impl SseDecode for RustOpaqueMoi> { @@ -1061,7 +1169,9 @@ impl SseDecode } } -impl SseDecode for StreamSink, flutter_rust_bridge::for_generated::SseCodec> { +impl SseDecode + for StreamSink +{ // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { let mut inner = ::sse_decode(deserializer); @@ -1150,6 +1260,36 @@ impl SseDecode for i32 { } } +impl SseDecode for crate::api::ipc::IpcEvent { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_kind = ::sse_decode(deserializer); + let mut var_pid = >::sse_decode(deserializer); + let mut var_payload = >::sse_decode(deserializer); + let mut var_error = >::sse_decode(deserializer); + return crate::api::ipc::IpcEvent { + kind: var_kind, + pid: var_pid, + payload: var_payload, + error: var_error, + }; + } +} + +impl SseDecode for crate::api::ipc::IpcEventKind { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::ipc::IpcEventKind::Connected, + 1 => crate::api::ipc::IpcEventKind::Message, + 2 => crate::api::ipc::IpcEventKind::Disconnected, + 3 => crate::api::ipc::IpcEventKind::Failed, + _ => unreachable!("Invalid variant for IpcEventKind: {}", inner), + }; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1240,6 +1380,28 @@ impl SseDecode for Vec { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1305,15 +1467,15 @@ fn pde_ffi_dispatcher_primary_impl( ) { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 12 => wire__crate__api__script__evaluate_script_impl(port, ptr, rust_vec_len, data_len), - 13 => wire__crate__api__editor__folds_compute_all_impl(port, ptr, rust_vec_len, data_len), - 16 => wire__crate__api__hotkey__hot_key_events_impl(port, ptr, rust_vec_len, data_len), - 17 => wire__crate__api__init__init_app_impl(port, ptr, rust_vec_len, data_len), - 18 => wire__crate__api__ipc__restart_ipc_server_impl(port, ptr, rust_vec_len, data_len), - 19 => wire__crate__api__ipc__send_ipc_message_impl(port, ptr, rust_vec_len, data_len), - 20 => wire__crate__api__hotkey__set_hot_keys_impl(port, ptr, rust_vec_len, data_len), - 21 => wire__crate__api__ipc__stop_ipc_server_impl(port, ptr, rust_vec_len, data_len), - 22 => wire__crate__api__editor__words_extract_impl(port, ptr, rust_vec_len, data_len), + 1 => wire__crate__api__ipc__IpcServer_bind_impl(port, ptr, rust_vec_len, data_len), + 2 => wire__crate__api__ipc__IpcServer_close_impl(port, ptr, rust_vec_len, data_len), + 3 => wire__crate__api__ipc__IpcServer_events_impl(port, ptr, rust_vec_len, data_len), + 16 => wire__crate__api__script__evaluate_script_impl(port, ptr, rust_vec_len, data_len), + 17 => wire__crate__api__editor__folds_compute_all_impl(port, ptr, rust_vec_len, data_len), + 20 => wire__crate__api__hotkey__hot_key_events_impl(port, ptr, rust_vec_len, data_len), + 21 => wire__crate__api__init__init_app_impl(port, ptr, rust_vec_len, data_len), + 22 => wire__crate__api__hotkey__set_hot_keys_impl(port, ptr, rust_vec_len, data_len), + 23 => wire__crate__api__editor__words_extract_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1326,31 +1488,47 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 1 => wire__crate__api__editor__RopeBridge_cached_lines_range_impl( + 4 => wire__crate__api__ipc__IpcServer_send_impl(ptr, rust_vec_len, data_len), + 5 => wire__crate__api__editor__RopeBridge_cached_lines_range_impl( ptr, rust_vec_len, data_len, ), - 2 => wire__crate__api__editor__RopeBridge_char_to_line_impl(ptr, rust_vec_len, data_len), - 3 => wire__crate__api__editor__RopeBridge_create_impl(ptr, rust_vec_len, data_len), - 4 => wire__crate__api__editor__RopeBridge_get_text_impl(ptr, rust_vec_len, data_len), - 5 => wire__crate__api__editor__RopeBridge_insert_impl(ptr, rust_vec_len, data_len), - 6 => wire__crate__api__editor__RopeBridge_len_chars_impl(ptr, rust_vec_len, data_len), - 7 => wire__crate__api__editor__RopeBridge_len_lines_impl(ptr, rust_vec_len, data_len), - 8 => wire__crate__api__editor__RopeBridge_line_impl(ptr, rust_vec_len, data_len), - 9 => wire__crate__api__editor__RopeBridge_line_to_char_impl(ptr, rust_vec_len, data_len), - 10 => wire__crate__api__editor__RopeBridge_remove_impl(ptr, rust_vec_len, data_len), - 11 => wire__crate__api__editor__RopeBridge_slice_impl(ptr, rust_vec_len, data_len), - 14 => { + 6 => wire__crate__api__editor__RopeBridge_char_to_line_impl(ptr, rust_vec_len, data_len), + 7 => wire__crate__api__editor__RopeBridge_create_impl(ptr, rust_vec_len, data_len), + 8 => wire__crate__api__editor__RopeBridge_get_text_impl(ptr, rust_vec_len, data_len), + 9 => wire__crate__api__editor__RopeBridge_insert_impl(ptr, rust_vec_len, data_len), + 10 => wire__crate__api__editor__RopeBridge_len_chars_impl(ptr, rust_vec_len, data_len), + 11 => wire__crate__api__editor__RopeBridge_len_lines_impl(ptr, rust_vec_len, data_len), + 12 => wire__crate__api__editor__RopeBridge_line_impl(ptr, rust_vec_len, data_len), + 13 => wire__crate__api__editor__RopeBridge_line_to_char_impl(ptr, rust_vec_len, data_len), + 14 => wire__crate__api__editor__RopeBridge_remove_impl(ptr, rust_vec_len, data_len), + 15 => wire__crate__api__editor__RopeBridge_slice_impl(ptr, rust_vec_len, data_len), + 18 => { wire__crate__api__editor__folds_find_matching_bracket_impl(ptr, rust_vec_len, data_len) } - 15 => wire__crate__api__editor__guides_compute_viewport_impl(ptr, rust_vec_len, data_len), + 19 => wire__crate__api__editor__guides_compute_viewport_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } // Section: rust2dart +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for IpcServer { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for FrbWrapper { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -1458,6 +1636,44 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::ipc::IpcEvent { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.kind.into_into_dart().into_dart(), + self.pid.into_into_dart().into_dart(), + self.payload.into_into_dart().into_dart(), + self.error.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::ipc::IpcEvent {} +impl flutter_rust_bridge::IntoIntoDart for crate::api::ipc::IpcEvent { + fn into_into_dart(self) -> crate::api::ipc::IpcEvent { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::ipc::IpcEventKind { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Connected => 0.into_dart(), + Self::Message => 1.into_dart(), + Self::Disconnected => 2.into_dart(), + Self::Failed => 3.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::ipc::IpcEventKind {} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::ipc::IpcEventKind +{ + fn into_into_dart(self) -> crate::api::ipc::IpcEventKind { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::editor::RustFoldRange { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -1486,6 +1702,13 @@ impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { } } +impl SseEncode for IpcServer { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + impl SseEncode for RopeBridge { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1493,6 +1716,17 @@ impl SseEncode for RopeBridge { } } +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + impl SseEncode for RustOpaqueMoi> { @@ -1504,7 +1738,9 @@ impl SseEncode } } -impl SseEncode for StreamSink, flutter_rust_bridge::for_generated::SseCodec> { +impl SseEncode + for StreamSink +{ // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { unimplemented!("") @@ -1579,6 +1815,34 @@ impl SseEncode for i32 { } } +impl SseEncode for crate::api::ipc::IpcEvent { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.kind, serializer); + >::sse_encode(self.pid, serializer); + >::sse_encode(self.payload, serializer); + >::sse_encode(self.error, serializer); + } +} + +impl SseEncode for crate::api::ipc::IpcEventKind { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::ipc::IpcEventKind::Connected => 0, + crate::api::ipc::IpcEventKind::Message => 1, + crate::api::ipc::IpcEventKind::Disconnected => 2, + crate::api::ipc::IpcEventKind::Failed => 3, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1649,6 +1913,26 @@ impl SseEncode for Vec { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1712,6 +1996,7 @@ mod io { use super::*; use crate::api::editor::*; + use crate::api::ipc::*; use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, }; @@ -1722,6 +2007,20 @@ mod io { flutter_rust_bridge::frb_generated_boilerplate_io!(); + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_rust_api_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_rust_api_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_rust_api_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( ptr: *const std::ffi::c_void, @@ -1749,6 +2048,7 @@ mod web { use super::*; use crate::api::editor::*; + use crate::api::ipc::*; use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, }; @@ -1761,6 +2061,20 @@ mod web { flutter_rust_bridge::frb_generated_boilerplate_web!(); + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[wasm_bindgen] pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( ptr: *const std::ffi::c_void, diff --git a/plugins/rust_api/rust/src/ipc/access.rs b/plugins/rust_api/rust/src/ipc/access.rs new file mode 100644 index 00000000..8f485a75 --- /dev/null +++ b/plugins/rust_api/rust/src/ipc/access.rs @@ -0,0 +1,83 @@ +use interprocess::local_socket::tokio::{prelude::*, Stream}; +use interprocess::local_socket::ListenerOptions; +use std::io; + +/// Linux carries an `fchmod` on the unbound socket over to the file `bind` +/// creates; other Unixes reject it and get `restrict_socket` after `bind`. +pub fn owner_only(options: ListenerOptions<'_>) -> ListenerOptions<'_> { + #[cfg(target_os = "linux")] + { + use interprocess::os::unix::local_socket::ListenerOptionsExt as _; + + options.mode(0o600) + } + #[cfg(not(target_os = "linux"))] + { + options + } +} + +pub fn restrict_socket(path: &str) -> io::Result<()> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; + } + #[cfg(windows)] + { + let _ = path; + } + Ok(()) +} + +/// Reports the admitted peer's process ID where the platform knows it. +pub fn admit(stream: &Stream) -> io::Result> { + let credentials = stream.peer_creds()?; + #[cfg(unix)] + { + let peer_uid = credentials + .euid() + .ok_or_else(|| denied("peer user ID is unavailable".into()))?; + // SAFETY: geteuid() takes no arguments and cannot fail. + let own_uid = unsafe { libc::geteuid() }; + if !is_permitted_uid(peer_uid, own_uid) { + return Err(denied(format!( + "peer uid {peer_uid} is neither {own_uid} nor root" + ))); + } + } + #[allow(clippy::useless_conversion)] + Ok(credentials.pid().and_then(|pid| u32::try_from(pid).ok())) +} + +#[cfg(unix)] +fn denied(reason: String) -> io::Error { + io::Error::new(io::ErrorKind::PermissionDenied, reason) +} + +#[cfg(unix)] +fn is_permitted_uid(peer_uid: libc::uid_t, own_uid: libc::uid_t) -> bool { + peer_uid == own_uid || peer_uid == 0 +} + +#[cfg(all(test, unix))] +mod tests { + use super::is_permitted_uid; + + #[test] + fn admits_the_socket_owner() { + assert!(is_permitted_uid(501, 501)); + } + + #[test] + fn admits_root_because_the_core_runs_privileged_for_tun() { + assert!(is_permitted_uid(0, 501)); + } + + #[test] + fn rejects_every_other_user() { + assert!(!is_permitted_uid(502, 501)); + assert!(!is_permitted_uid(501, 0)); + } +} diff --git a/plugins/rust_api/rust/src/ipc/frame.rs b/plugins/rust_api/rust/src/ipc/frame.rs index deb7ac28..463dd7e8 100644 --- a/plugins/rust_api/rust/src/ipc/frame.rs +++ b/plugins/rust_api/rust/src/ipc/frame.rs @@ -1,283 +1,94 @@ -use std::io::{self, Read, Write}; -use std::thread; -use std::time::Duration; - -pub const TYPE_READY: u8 = 0x00; -pub const TYPE_CONNECTED: u8 = 0x01; -pub const TYPE_DISCONNECTED: u8 = 0x02; -pub const TYPE_DATA: u8 = 0x03; -pub const TYPE_ERROR: u8 = 0x04; +use std::io; +use tokio::io::{AsyncRead, AsyncReadExt}; pub const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024; -pub const IO_POLL_INTERVAL: Duration = Duration::from_millis(20); -const MIN_WRITE_BACKOFF: Duration = Duration::from_millis(1); +const HEADER_SIZE: usize = 4; -pub fn make_frame(ty: u8, payload: &[u8]) -> Vec { - let mut frame = Vec::with_capacity(1 + payload.len()); - frame.push(ty); - frame.extend_from_slice(payload); - frame -} - -pub fn validate_frame_len(len: usize) -> io::Result { - if len > MAX_FRAME_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("IPC frame exceeds {MAX_FRAME_SIZE} bytes"), - )); - } - u32::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "IPC frame is too large")) -} - -pub fn is_expected_disconnect_error(error: &io::Error) -> bool { - matches!( - error.kind(), - io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe +fn too_large() -> io::Error { + io::Error::new( + io::ErrorKind::InvalidData, + format!("IPC frame exceeds {MAX_FRAME_SIZE} bytes"), ) } -#[cfg(any(windows, test))] -fn normalize_windows_pipe_write(result: io::Result) -> io::Result { - match result { - Ok(0) => Err(io::ErrorKind::WouldBlock.into()), - result => result, +/// Prefixes `payload` with its little-endian length, the layout `readFrame` +/// in `core/server.go` expects. +pub fn encode(payload: &[u8]) -> io::Result> { + if payload.len() > MAX_FRAME_SIZE { + return Err(too_large()); } + let mut frame = Vec::with_capacity(HEADER_SIZE + payload.len()); + frame.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + frame.extend_from_slice(payload); + Ok(frame) } -fn write_all_interruptible( - writer: &mut impl Write, - mut data: &[u8], - is_running: &impl Fn() -> bool, -) -> io::Result<()> { - let mut backoff = MIN_WRITE_BACKOFF; - while !data.is_empty() { - if !is_running() { - return Err(io::Error::new( - io::ErrorKind::Interrupted, - "IPC connection stopped", - )); - } - let result = writer.write(data); - #[cfg(windows)] - let result = normalize_windows_pipe_write(result); - match result { - Ok(0) => return Err(io::ErrorKind::WriteZero.into()), - Ok(written) => { - data = &data[written..]; - backoff = MIN_WRITE_BACKOFF; - } - Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, - Err(e) if e.kind() == io::ErrorKind::WouldBlock => { - // A Windows PIPE_NOWAIT handle reports a full buffer through a - // zero-byte write, and every frame costs two writes. A flat - // 20ms wait there parks the whole queue behind one frame, so - // back off from 1ms instead. - thread::sleep(backoff); - backoff = (backoff * 2).min(IO_POLL_INTERVAL); - } - Err(e) => return Err(e), - } - } - Ok(()) -} - -pub fn write_frame( - writer: &mut impl Write, - data: &[u8], - is_running: &impl Fn() -> bool, -) -> io::Result<()> { - let len = validate_frame_len(data.len())?; - write_all_interruptible(writer, &len.to_le_bytes(), is_running)?; - write_all_interruptible(writer, data, is_running) -} - -#[derive(Default)] -pub struct FrameReader { - header: [u8; 4], - header_read: usize, - payload: Vec, - payload_read: usize, -} - -impl FrameReader { - pub fn poll(&mut self, reader: &mut impl Read) -> io::Result>> { - loop { - if self.header_read < self.header.len() { - match reader.read(&mut self.header[self.header_read..]) { - Ok(0) => return Err(io::ErrorKind::UnexpectedEof.into()), - Ok(read) => self.header_read += read, - Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, - Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(None), - Err(e) => return Err(e), - } - if self.header_read < self.header.len() { - continue; - } - - let len = u32::from_le_bytes(self.header) as usize; - validate_frame_len(len)?; - if len == 0 { - self.reset(); - return Ok(Some(Vec::new())); - } - self.payload = vec![0; len]; - } - - match reader.read(&mut self.payload[self.payload_read..]) { - Ok(0) => return Err(io::ErrorKind::UnexpectedEof.into()), - Ok(read) => self.payload_read += read, - Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, - Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(None), - Err(e) => return Err(e), - } - if self.payload_read == self.payload.len() { - let payload = std::mem::take(&mut self.payload); - self.reset(); - return Ok(Some(payload)); - } - } - } - - fn reset(&mut self) { - self.header = [0; 4]; - self.header_read = 0; - self.payload.clear(); - self.payload_read = 0; +pub async fn read(reader: &mut (impl AsyncRead + Unpin)) -> io::Result> { + let mut header = [0; HEADER_SIZE]; + reader.read_exact(&mut header).await?; + let len = u32::from_le_bytes(header) as usize; + if len > MAX_FRAME_SIZE { + return Err(too_large()); } + let mut payload = vec![0; len]; + reader.read_exact(&mut payload).await?; + Ok(payload) } #[cfg(test)] mod tests { use super::*; - use std::collections::VecDeque; - use std::io::Cursor; - enum ReadStep { - Data(Vec), - WouldBlock, + fn read_all(bytes: &[u8]) -> io::Result> { + let mut reader = bytes; + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on(read(&mut reader)) } - struct StepReader { - steps: VecDeque, + #[test] + fn a_frame_survives_an_encode_and_read_round_trip() { + let frame = encode(b"hello").unwrap(); + + assert_eq!(&frame[..HEADER_SIZE], [5, 0, 0, 0]); + assert_eq!(read_all(&frame).unwrap(), b"hello"); } - impl Read for StepReader { - fn read(&mut self, buffer: &mut [u8]) -> io::Result { - match self.steps.pop_front() { - Some(ReadStep::Data(mut data)) => { - let read = data.len().min(buffer.len()); - buffer[..read].copy_from_slice(&data[..read]); - if read < data.len() { - data.drain(..read); - self.steps.push_front(ReadStep::Data(data)); - } - Ok(read) - } - Some(ReadStep::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()), - None => Ok(0), - } + #[test] + fn an_empty_frame_is_a_frame() { + assert_eq!(read_all(&encode(b"").unwrap()).unwrap(), b""); + } + + #[test] + fn a_truncated_frame_reads_as_end_of_stream() { + let frame = encode(b"hello").unwrap(); + + for len in [0, 2, HEADER_SIZE, frame.len() - 1] { + assert_eq!( + read_all(&frame[..len]).unwrap_err().kind(), + io::ErrorKind::UnexpectedEof, + ); } } #[test] - fn frame_reader_reads_complete_frame() { - let payload = b"hello"; - let mut bytes = (payload.len() as u32).to_le_bytes().to_vec(); - bytes.extend_from_slice(payload); - let mut reader = Cursor::new(bytes); - let mut frame_reader = FrameReader::default(); + fn an_oversized_header_is_rejected_before_its_payload_is_read() { + let header = (MAX_FRAME_SIZE as u32 + 1).to_le_bytes(); assert_eq!( - frame_reader.poll(&mut reader).unwrap(), - Some(payload.to_vec()) - ); - } - - #[test] - fn frame_reader_preserves_partial_nonblocking_reads() { - let mut reader = StepReader { - steps: VecDeque::from([ - ReadStep::Data(vec![5, 0]), - ReadStep::WouldBlock, - ReadStep::Data(vec![0, 0]), - ReadStep::Data(b"he".to_vec()), - ReadStep::WouldBlock, - ReadStep::Data(b"llo".to_vec()), - ]), - }; - let mut frame_reader = FrameReader::default(); - - assert_eq!(frame_reader.poll(&mut reader).unwrap(), None); - assert_eq!(frame_reader.poll(&mut reader).unwrap(), None); - assert_eq!( - frame_reader.poll(&mut reader).unwrap(), - Some(b"hello".to_vec()) - ); - } - - #[test] - fn frame_reader_rejects_oversized_frame_before_allocation() { - let len = (MAX_FRAME_SIZE as u32 + 1).to_le_bytes(); - let mut reader = Cursor::new(len); - let mut frame_reader = FrameReader::default(); - - assert_eq!( - frame_reader.poll(&mut reader).unwrap_err().kind(), - io::ErrorKind::InvalidData - ); - assert!(frame_reader.payload.is_empty()); - } - - #[test] - fn windows_zero_write_is_treated_as_pending() { - assert_eq!( - normalize_windows_pipe_write(Ok(0)).unwrap_err().kind(), - io::ErrorKind::WouldBlock, - ); - assert_eq!(normalize_windows_pipe_write(Ok(2)).unwrap(), 2); - - let error = io::Error::from(io::ErrorKind::BrokenPipe); - assert_eq!( - normalize_windows_pipe_write(Err(error)).unwrap_err().kind(), - io::ErrorKind::BrokenPipe, - ); - } - - #[test] - fn write_backoff_starts_short_and_is_capped() { - let mut backoff = MIN_WRITE_BACKOFF; - assert_eq!(backoff, Duration::from_millis(1)); - for _ in 0..10 { - backoff = (backoff * 2).min(IO_POLL_INTERVAL); - } - assert_eq!(backoff, IO_POLL_INTERVAL); - } - - #[test] - fn write_frame_stops_when_the_connection_is_gone() { - let mut sink = Vec::new(); - - assert_eq!( - write_frame(&mut sink, b"payload", &|| false) - .unwrap_err() - .kind(), - io::ErrorKind::Interrupted, - ); - assert!(sink.is_empty()); - } - - #[test] - fn expected_disconnect_errors_are_not_reported() { - for kind in [ - io::ErrorKind::UnexpectedEof, - io::ErrorKind::ConnectionReset, - io::ErrorKind::BrokenPipe, - ] { - assert!(is_expected_disconnect_error(&io::Error::from(kind))); - } - assert!(!is_expected_disconnect_error(&io::Error::from( + read_all(&header).unwrap_err().kind(), io::ErrorKind::InvalidData, - ))); + ); + } + + #[test] + fn an_oversized_payload_is_not_encoded() { + let payload = vec![0; MAX_FRAME_SIZE + 1]; + + assert_eq!( + encode(&payload).unwrap_err().kind(), + io::ErrorKind::InvalidData, + ); } } diff --git a/plugins/rust_api/rust/src/ipc/mod.rs b/plugins/rust_api/rust/src/ipc/mod.rs index 45a179e6..88475d7f 100644 --- a/plugins/rust_api/rust/src/ipc/mod.rs +++ b/plugins/rust_api/rust/src/ipc/mod.rs @@ -1,15 +1,27 @@ #[cfg(not(target_os = "android"))] +mod access; +#[cfg(not(target_os = "android"))] mod frame; #[cfg(not(target_os = "android"))] -mod platform; -#[cfg(not(target_os = "android"))] -mod queue; +mod outbox; #[cfg(not(target_os = "android"))] mod server; #[cfg(target_os = "android")] mod unsupported; #[cfg(not(target_os = "android"))] -pub use server::{restart_server, send_message, stop_server}; +pub use server::Server; #[cfg(target_os = "android")] -pub use unsupported::{restart_server, send_message, stop_server}; +pub use unsupported::Server; + +#[cfg_attr(target_os = "android", allow(dead_code))] +#[derive(Debug, PartialEq, Eq)] +pub enum Event { + Connected { pid: Option }, + Message(Vec), + Disconnected { error: Option }, + Failed(String), +} + +/// Returns false once nobody listens any more, which ends the server. +pub type EventSink = Box bool + Send>; diff --git a/plugins/rust_api/rust/src/ipc/outbox.rs b/plugins/rust_api/rust/src/ipc/outbox.rs new file mode 100644 index 00000000..891fcf3e --- /dev/null +++ b/plugins/rust_api/rust/src/ipc/outbox.rs @@ -0,0 +1,165 @@ +use std::collections::VecDeque; +use std::io; +use std::sync::{Mutex, MutexGuard, PoisonError}; +use tokio::sync::Notify; + +// Frames are control messages of a few KB, so this only fills up when the peer +// has stopped reading. +const MAX_PENDING_BYTES: usize = 16 * 1024 * 1024; + +#[derive(Default)] +struct Queue { + frames: VecDeque>, + bytes: usize, + open: bool, +} + +/// Frames waiting for the connection's writer. `push` never blocks, so it can +/// be called from the Dart thread. +#[derive(Default)] +pub struct Outbox { + queue: Mutex, + ready: Notify, +} + +impl Outbox { + pub fn open(&self) { + *self.queue() = Queue { + open: true, + ..Queue::default() + }; + } + + pub fn close(&self) { + *self.queue() = Queue::default(); + } + + pub fn push(&self, frame: Vec) -> io::Result<()> { + let mut queue = self.queue(); + if !queue.open { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "IPC peer is not connected", + )); + } + // An empty queue takes a frame of any size: refusing one that is larger + // than the whole budget would refuse it forever. + if queue.bytes > 0 && queue.bytes + frame.len() > MAX_PENDING_BYTES { + return Err(io::Error::new( + io::ErrorKind::WouldBlock, + "IPC send queue is full", + )); + } + queue.bytes += frame.len(); + queue.frames.push_back(frame); + drop(queue); + self.ready.notify_one(); + Ok(()) + } + + pub async fn next(&self) -> Vec { + loop { + if let Some(frame) = self.pop() { + return frame; + } + self.ready.notified().await; + } + } + + fn pop(&self) -> Option> { + let mut queue = self.queue(); + let frame = queue.frames.pop_front()?; + queue.bytes -= frame.len(); + Some(frame) + } + + fn queue(&self) -> MutexGuard<'_, Queue> { + self.queue.lock().unwrap_or_else(PoisonError::into_inner) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::time::Duration; + + fn open_outbox() -> Outbox { + let outbox = Outbox::default(); + outbox.open(); + outbox + } + + #[test] + fn frames_leave_in_the_order_they_were_pushed() { + let outbox = open_outbox(); + outbox.push(b"first".to_vec()).unwrap(); + outbox.push(b"second".to_vec()).unwrap(); + + assert_eq!(outbox.pop().unwrap(), b"first"); + assert_eq!(outbox.pop().unwrap(), b"second"); + assert!(outbox.pop().is_none()); + } + + #[test] + fn a_push_without_a_peer_is_refused() { + let outbox = Outbox::default(); + + assert_eq!( + outbox.push(b"frame".to_vec()).unwrap_err().kind(), + io::ErrorKind::NotConnected, + ); + } + + #[test] + fn closing_drops_what_the_last_peer_never_read() { + let outbox = open_outbox(); + outbox.push(b"stale".to_vec()).unwrap(); + + outbox.close(); + outbox.open(); + + assert!(outbox.pop().is_none()); + } + + #[test] + fn the_byte_budget_refuses_a_frame_that_does_not_fit() { + let outbox = open_outbox(); + outbox.push(vec![0; MAX_PENDING_BYTES]).unwrap(); + + assert_eq!( + outbox.push(b"overflow".to_vec()).unwrap_err().kind(), + io::ErrorKind::WouldBlock, + ); + + outbox.pop().unwrap(); + outbox.push(b"fits again".to_vec()).unwrap(); + } + + #[test] + fn a_lone_frame_larger_than_the_budget_is_accepted() { + let outbox = open_outbox(); + + outbox.push(vec![0; MAX_PENDING_BYTES + 1]).unwrap(); + + assert_eq!(outbox.pop().unwrap().len(), MAX_PENDING_BYTES + 1); + } + + #[test] + fn next_wakes_for_a_frame_pushed_from_another_thread() { + let outbox = Arc::new(open_outbox()); + let producer = Arc::clone(&outbox); + let pushed = std::thread::spawn(move || { + std::thread::sleep(Duration::from_millis(20)); + producer.push(b"late".to_vec()).unwrap(); + }); + + let frame = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on(outbox.next()); + + pushed.join().unwrap(); + assert_eq!(frame, b"late"); + } +} diff --git a/plugins/rust_api/rust/src/ipc/platform.rs b/plugins/rust_api/rust/src/ipc/platform.rs deleted file mode 100644 index 729fed9d..00000000 --- a/plugins/rust_api/rust/src/ipc/platform.rs +++ /dev/null @@ -1,170 +0,0 @@ -use std::io; - -#[cfg(windows)] -use interprocess::local_socket::RecvHalf; -use interprocess::local_socket::{ListenerOptions, Stream}; -#[cfg(windows)] -use std::io::Read; -#[cfg(unix)] -use std::path::Path; -#[cfg(windows)] -use std::{ - os::windows::io::{AsHandle, AsRawHandle}, - ptr, -}; -#[cfg(windows)] -use windows_sys::Win32::System::Pipes::PeekNamedPipe; - -pub fn cleanup_socket(path: &str) -> io::Result<()> { - #[cfg(unix)] - { - if Path::new(path).exists() { - std::fs::remove_file(path)?; - } - } - #[cfg(windows)] - { - let _ = path; - } - Ok(()) -} - -/// Linux carries an `fchmod` on the unbound socket over to the file `bind` -/// creates; other Unixes reject it and keep the chmod after `bind`. -pub fn restrict_listener_mode(options: ListenerOptions<'_>) -> ListenerOptions<'_> { - #[cfg(target_os = "linux")] - { - use interprocess::os::unix::local_socket::ListenerOptionsExt as _; - - options.mode(0o600) - } - #[cfg(not(target_os = "linux"))] - { - options - } -} - -pub fn restrict_socket_to_owner(path: &str) -> io::Result<()> { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; - } - #[cfg(windows)] - { - let _ = path; - } - Ok(()) -} - -#[cfg(unix)] -pub fn authorize_peer(stream: &Stream) -> io::Result<()> { - use interprocess::local_socket::traits::StreamCommon as _; - - let peer_uid = stream.peer_creds()?.euid().ok_or_else(|| { - io::Error::new( - io::ErrorKind::PermissionDenied, - "peer user ID is unavailable", - ) - })?; - // SAFETY: geteuid() takes no arguments and cannot fail. - let own_uid = unsafe { libc::geteuid() }; - if is_permitted_uid(peer_uid, own_uid) { - return Ok(()); - } - Err(io::Error::new( - io::ErrorKind::PermissionDenied, - format!("peer uid {peer_uid} is neither {own_uid} nor root"), - )) -} - -#[cfg(unix)] -fn is_permitted_uid(peer_uid: libc::uid_t, own_uid: libc::uid_t) -> bool { - peer_uid == own_uid || peer_uid == 0 -} - -#[cfg(windows)] -pub fn authorize_peer(_stream: &Stream) -> io::Result<()> { - Ok(()) -} - -#[cfg(windows)] -pub fn connected_payload(stream: &Stream) -> io::Result> { - use interprocess::local_socket::traits::StreamCommon as _; - - let credentials = stream.peer_creds()?; - let process_id = credentials.pid().ok_or_else(|| { - io::Error::new( - io::ErrorKind::PermissionDenied, - "peer process ID is unavailable", - ) - })?; - Ok(process_id.to_le_bytes().to_vec()) -} - -#[cfg(not(windows))] -pub fn connected_payload(_stream: &Stream) -> io::Result> { - Ok(Vec::new()) -} - -#[cfg(windows)] -fn bytes_available(receiver: &RecvHalf) -> io::Result { - let RecvHalf::NamedPipe(pipe) = receiver; - let mut available = 0_u32; - // SAFETY: `pipe` owns a live named-pipe handle for this receive half, and - // `available` is a valid output pointer. No input buffer is supplied. - let result = unsafe { - PeekNamedPipe( - pipe.as_handle().as_raw_handle(), - ptr::null_mut(), - 0, - ptr::null_mut(), - &mut available, - ptr::null_mut(), - ) - }; - if result == 0 { - Err(io::Error::last_os_error()) - } else { - Ok(available) - } -} - -/// A blocking read on a Windows named pipe waits for the peer, which would park -/// the loop that also has to notice a stop request. -#[cfg(windows)] -pub struct PipeReader<'a> { - pub receiver: &'a mut RecvHalf, -} - -#[cfg(windows)] -impl Read for PipeReader<'_> { - fn read(&mut self, buffer: &mut [u8]) -> io::Result { - if bytes_available(self.receiver)? == 0 { - return Err(io::ErrorKind::WouldBlock.into()); - } - self.receiver.read(buffer) - } -} - -#[cfg(all(test, unix))] -mod tests { - use super::is_permitted_uid; - - #[test] - fn admits_the_socket_owner() { - assert!(is_permitted_uid(501, 501)); - } - - #[test] - fn admits_root_because_the_core_runs_privileged_for_tun() { - assert!(is_permitted_uid(0, 501)); - } - - #[test] - fn rejects_every_other_user() { - assert!(!is_permitted_uid(502, 501)); - assert!(!is_permitted_uid(501, 0)); - } -} diff --git a/plugins/rust_api/rust/src/ipc/queue.rs b/plugins/rust_api/rust/src/ipc/queue.rs deleted file mode 100644 index 7fc8cd11..00000000 --- a/plugins/rust_api/rust/src/ipc/queue.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::mpsc::{self, SyncSender, TrySendError}; -use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; - -pub const MAX_PENDING_MESSAGES: usize = 256; -// The slot count alone bounds nothing: 256 slots times the frame ceiling is -// gigabytes. Real frames are control messages of a few KB, so cap the queue by -// bytes too and let the message count stay generous for fan-out bursts. -const MAX_PENDING_BYTES: usize = 16 * 1024 * 1024; -// Callers fan out: a group delay test issues one request per proxy at once, so -// a full queue means the writer is behind, not that the message is surplus. -// Wait it out, but never past this deadline, so a wedged peer still surfaces. -pub const SEND_QUEUE_TIMEOUT: Duration = Duration::from_secs(5); -const SEND_RETRY_INTERVAL: Duration = Duration::from_millis(2); - -#[derive(Clone)] -pub struct MessageSender { - tx: SyncSender>, - pending_bytes: Arc, -} - -impl MessageSender { - pub fn channel(capacity: usize) -> (Self, mpsc::Receiver>, Arc) { - let (tx, rx) = mpsc::sync_channel::>(capacity); - let pending_bytes = Arc::new(AtomicUsize::new(0)); - let sender = Self { - tx, - pending_bytes: Arc::clone(&pending_bytes), - }; - (sender, rx, pending_bytes) - } - - // An empty queue always accepts, even past the budget: a lone frame may be - // larger than the budget, and refusing it would stall forever with nothing - // draining ahead of it. - fn try_reserve(&self, len: usize) -> bool { - self.pending_bytes - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |pending| { - let next = pending.checked_add(len)?; - (pending == 0 || next <= MAX_PENDING_BYTES).then_some(next) - }) - .is_ok() - } - - fn release(&self, len: usize) { - self.pending_bytes.fetch_sub(len, Ordering::SeqCst); - } -} - -pub fn enqueue_message( - sender: &MessageSender, - data: Vec, - timeout: Duration, - running: &AtomicBool, -) -> Result<(), String> { - let deadline = Instant::now() + timeout; - let len = data.len(); - let mut pending = data; - loop { - if sender.try_reserve(len) { - match sender.tx.try_send(pending) { - Ok(()) => return Ok(()), - Err(TrySendError::Disconnected(_)) => { - sender.release(len); - return Err("IPC client is disconnected".into()); - } - Err(TrySendError::Full(rejected)) => { - sender.release(len); - pending = rejected; - } - } - } - if !running.load(Ordering::SeqCst) { - return Err("IPC server is stopped".into()); - } - if Instant::now() >= deadline { - return Err("IPC send queue is full".into()); - } - thread::sleep(SEND_RETRY_INTERVAL); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn enqueue_message_waits_for_a_draining_writer() { - let (sender, rx, _) = MessageSender::channel(1); - enqueue_message( - &sender, - b"first".to_vec(), - Duration::from_secs(5), - &AtomicBool::new(true), - ) - .unwrap(); - let writer = thread::spawn(move || { - thread::sleep(Duration::from_millis(30)); - (rx.recv().unwrap(), rx.recv().unwrap()) - }); - - enqueue_message( - &sender, - b"second".to_vec(), - Duration::from_secs(5), - &AtomicBool::new(true), - ) - .unwrap(); - - assert_eq!( - writer.join().unwrap(), - (b"first".to_vec(), b"second".to_vec()) - ); - } - - #[test] - fn enqueue_message_gives_up_on_a_writer_that_never_drains() { - let (sender, _rx, _) = MessageSender::channel(1); - sender.tx.send(b"first".to_vec()).unwrap(); - - assert_eq!( - enqueue_message( - &sender, - b"second".to_vec(), - Duration::from_millis(20), - &AtomicBool::new(true), - ) - .unwrap_err(), - "IPC send queue is full" - ); - } - - #[test] - fn enqueue_message_stops_waiting_once_the_server_is_stopped() { - let (sender, _rx, _) = MessageSender::channel(1); - sender.tx.send(b"first".to_vec()).unwrap(); - let running = Arc::new(AtomicBool::new(true)); - let stopper = Arc::clone(&running); - thread::spawn(move || { - thread::sleep(Duration::from_millis(20)); - stopper.store(false, Ordering::SeqCst); - }); - - let started = Instant::now(); - let error = enqueue_message( - &sender, - b"second".to_vec(), - Duration::from_secs(30), - &running, - ) - .unwrap_err(); - - assert_eq!(error, "IPC server is stopped"); - assert!(started.elapsed() < Duration::from_secs(5)); - } - - #[test] - fn enqueue_message_reports_a_gone_writer_without_waiting() { - let (sender, rx, _) = MessageSender::channel(1); - drop(rx); - - assert_eq!( - enqueue_message( - &sender, - b"first".to_vec(), - Duration::from_secs(30), - &AtomicBool::new(true), - ) - .unwrap_err(), - "IPC client is disconnected" - ); - } - - #[test] - fn enqueue_message_caps_the_queue_by_bytes_before_the_slot_count() { - let (sender, _rx, pending_bytes) = MessageSender::channel(MAX_PENDING_MESSAGES); - enqueue_message( - &sender, - vec![0u8; MAX_PENDING_BYTES], - Duration::from_secs(5), - &AtomicBool::new(true), - ) - .unwrap(); - - assert_eq!(pending_bytes.load(Ordering::SeqCst), MAX_PENDING_BYTES); - assert_eq!( - enqueue_message( - &sender, - b"overflow".to_vec(), - Duration::from_millis(20), - &AtomicBool::new(true), - ) - .unwrap_err(), - "IPC send queue is full" - ); - assert_eq!(pending_bytes.load(Ordering::SeqCst), MAX_PENDING_BYTES); - } - - #[test] - fn enqueue_message_admits_a_lone_frame_larger_than_the_budget() { - let (sender, rx, pending_bytes) = MessageSender::channel(MAX_PENDING_MESSAGES); - let len = MAX_PENDING_BYTES + 1; - - enqueue_message( - &sender, - vec![0u8; len], - Duration::from_millis(20), - &AtomicBool::new(true), - ) - .unwrap(); - - assert_eq!(pending_bytes.load(Ordering::SeqCst), len); - assert_eq!(rx.recv().unwrap().len(), len); - } - - #[test] - fn draining_a_message_frees_its_share_of_the_budget() { - let (sender, rx, pending_bytes) = MessageSender::channel(MAX_PENDING_MESSAGES); - enqueue_message( - &sender, - vec![0u8; MAX_PENDING_BYTES], - Duration::from_secs(5), - &AtomicBool::new(true), - ) - .unwrap(); - - let drained = rx.recv().unwrap(); - pending_bytes.fetch_sub(drained.len(), Ordering::SeqCst); - - assert_eq!(pending_bytes.load(Ordering::SeqCst), 0); - enqueue_message( - &sender, - b"next".to_vec(), - Duration::from_millis(20), - &AtomicBool::new(true), - ) - .unwrap(); - } -} diff --git a/plugins/rust_api/rust/src/ipc/server.rs b/plugins/rust_api/rust/src/ipc/server.rs index 7cfcb226..57d1a6c2 100644 --- a/plugins/rust_api/rust/src/ipc/server.rs +++ b/plugins/rust_api/rust/src/ipc/server.rs @@ -1,265 +1,471 @@ -use super::frame::{ - self, FrameReader, IO_POLL_INTERVAL, TYPE_CONNECTED, TYPE_DATA, TYPE_DISCONNECTED, TYPE_ERROR, - TYPE_READY, -}; -use super::platform; -use super::queue::{enqueue_message, MessageSender, MAX_PENDING_MESSAGES, SEND_QUEUE_TIMEOUT}; -use crate::frb_generated::StreamSink; -use flutter_rust_bridge::for_generated::SseCodec; -use interprocess::local_socket::prelude::*; -use interprocess::local_socket::{GenericFilePath, ListenerNonblockingMode, ListenerOptions}; +use super::outbox::Outbox; +use super::{access, frame, Event, EventSink}; +use interprocess::local_socket::tokio::{prelude::*, Listener, RecvHalf, SendHalf, Stream}; +use interprocess::local_socket::{GenericFilePath, ListenerOptions}; use std::io; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc; -use std::sync::{Arc, Mutex}; -use std::thread; +use std::sync::{mpsc, Arc, Mutex, MutexGuard, PoisonError}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; +use tokio::io::{AsyncWriteExt, BufReader}; +use tokio::sync::oneshot; -macro_rules! ipc_debug { - ($($arg:tt)*) => { - if cfg!(debug_assertions) { - eprintln!($($arg)*); +const STALE_SOCKET_RETRY_LIMIT: Duration = Duration::from_secs(1); + +struct Control { + events: Option>, + shutdown: Option>, + worker: Option>, +} + +/// A local socket serving one peer at a time from a thread of its own. +/// Dropping it ends that thread without waiting for it; `close` waits. +pub struct Server { + outbox: Arc, + control: Mutex, +} + +impl Server { + /// Returns once `address` is bound. Peers that connect before `events` + /// attaches a sink wait in the backlog. + pub fn bind(address: String) -> io::Result { + let outbox = Arc::new(Outbox::default()); + let (bound_tx, bound_rx) = mpsc::sync_channel(1); + let (events_tx, events_rx) = oneshot::channel(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let worker_outbox = Arc::clone(&outbox); + let worker = thread::Builder::new() + .name("ipc-server".into()) + .spawn(move || run(address, bound_tx, events_rx, shutdown_rx, worker_outbox))?; + bound_rx + .recv() + .map_err(|_| io::Error::other("IPC server thread ended before binding"))??; + Ok(Self { + outbox, + control: Mutex::new(Control { + events: Some(events_tx), + shutdown: Some(shutdown_tx), + worker: Some(worker), + }), + }) + } + + pub fn events(&self, sink: EventSink) { + if let Some(events) = self.control().events.take() { + let _ = events.send(sink); } - }; -} - -const ACCEPT_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50); - -static RUNNING: AtomicBool = AtomicBool::new(false); -static LIFECYCLE: Mutex<()> = Mutex::new(()); - -struct ServerState { - sender: Option, - handle: Option>, -} - -static STATE: Mutex = Mutex::new(ServerState { - sender: None, - handle: None, -}); - -fn server_active() -> bool { - RUNNING.load(Ordering::SeqCst) -} - -fn stop_server_thread() -> Result<(), String> { - RUNNING.store(false, Ordering::SeqCst); - let handle = { - let mut state = STATE.lock().map_err(|e| format!("Lock poisoned: {e}"))?; - state.sender = None; - state.handle.take() - }; - if let Some(handle) = handle { - handle - .join() - .map_err(|_| "IPC server thread panicked".to_owned())?; } - Ok(()) -} -pub fn restart_server(name: String, sink: StreamSink, SseCodec>) -> Result<(), String> { - let _lifecycle = LIFECYCLE - .lock() - .map_err(|e| format!("Lifecycle lock poisoned: {e}"))?; - - stop_server_thread()?; - platform::cleanup_socket(&name).map_err(|e| format!("Failed to remove stale socket: {e}"))?; - - RUNNING.store(true, Ordering::SeqCst); - let handle = thread::Builder::new() - .name("ipc-server".into()) - .spawn(move || io_loop(name, sink)) - .map_err(|e| { - RUNNING.store(false, Ordering::SeqCst); - format!("Failed to spawn IPC server thread: {e}") - })?; - - let mut state = STATE.lock().map_err(|e| format!("Lock poisoned: {e}"))?; - state.handle = Some(handle); - Ok(()) -} - -pub fn stop_server() -> Result<(), String> { - let _lifecycle = LIFECYCLE - .lock() - .map_err(|e| format!("Lifecycle lock poisoned: {e}"))?; - stop_server_thread() -} - -pub fn send_message(data: Vec) -> Result<(), String> { - frame::validate_frame_len(data.len()).map_err(|e| e.to_string())?; - let sender = STATE - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))? - .sender - .clone() - .ok_or("IPC client is not connected")?; - - enqueue_message(&sender, data, SEND_QUEUE_TIMEOUT, &RUNNING) -} - -fn report_error(sink: &StreamSink, SseCodec>, message: impl AsRef) { - let _ = sink.add(frame::make_frame(TYPE_ERROR, message.as_ref().as_bytes())); -} - -fn finish_server(name: &str) { - RUNNING.store(false, Ordering::SeqCst); - if let Ok(mut state) = STATE.lock() { - state.sender = None; + pub fn send(&self, payload: &[u8]) -> io::Result<()> { + self.outbox.push(frame::encode(payload)?) } - if let Err(e) = platform::cleanup_socket(name) { - ipc_debug!("[IPC] cleanup socket failed: {e}"); + + pub fn close(&self) { + let worker = { + let mut control = self.control(); + control.events = None; + control.shutdown = None; + control.worker.take() + }; + if let Some(worker) = worker { + let _ = worker.join(); + } + } + + fn control(&self) -> MutexGuard<'_, Control> { + self.control.lock().unwrap_or_else(PoisonError::into_inner) } } -fn io_loop(name: String, sink: StreamSink, SseCodec>) { - let fs_name = match name.clone().to_fs_name::() { - Ok(name) => name, - Err(e) => { - report_error(&sink, format!("name error: {e}")); - finish_server(&name); +fn run( + address: String, + bound: mpsc::SyncSender>, + events: oneshot::Receiver, + shutdown: oneshot::Receiver<()>, + outbox: Arc, +) { + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(runtime) => runtime, + Err(error) => { + let _ = bound.send(Err(error)); return; } }; - - let options = platform::restrict_listener_mode(ListenerOptions::new().name(fs_name)); - let listener = match options.create_sync() { - Ok(listener) => listener, - Err(e) => { - report_error(&sink, format!("bind error: {e}")); - finish_server(&name); - return; + runtime.block_on(async { + let listener = match listen(&address) { + Ok(listener) => listener, + Err(error) => { + let _ = bound.send(Err(error)); + return; + } + }; + let _ = bound.send(Ok(())); + let serve = async { + if let Ok(emit) = events.await { + accept_peers(&listener, &emit, &outbox).await; + } + }; + tokio::select! { + _ = serve => {} + _ = shutdown => {} } - }; + outbox.close(); + }); +} - if let Err(e) = platform::restrict_socket_to_owner(&name) { - report_error(&sink, format!("socket permission error: {e}")); - finish_server(&name); - return; - } +fn listen(address: &str) -> io::Result { + let options = ListenerOptions::new() + .name(address.to_fs_name::()?) + .try_overwrite(true) + .max_spin_time(STALE_SOCKET_RETRY_LIMIT); + let listener = access::owner_only(options).create_tokio()?; + access::restrict_socket(address)?; + Ok(listener) +} - if let Err(e) = listener.set_nonblocking(ListenerNonblockingMode::Accept) { - report_error(&sink, format!("listener nonblocking error: {e}")); - finish_server(&name); - return; - } - - if sink.add(frame::make_frame(TYPE_READY, &[])).is_err() { - finish_server(&name); - return; - } - - while server_active() { - let stream = match listener.accept() { +async fn accept_peers(listener: &Listener, emit: &EventSink, outbox: &Outbox) { + loop { + let stream = match listener.accept().await { Ok(stream) => stream, - Err(e) if e.kind() == io::ErrorKind::WouldBlock => { - thread::sleep(ACCEPT_POLL_INTERVAL); - continue; - } - Err(e) => { - if server_active() { - report_error(&sink, format!("accept error: {e}")); - } - break; + Err(error) => { + emit(Event::Failed(format!("accept error: {error}"))); + return; } }; - - if let Err(e) = platform::authorize_peer(&stream) { - ipc_debug!("[IPC] rejected connection: {e}"); - continue; - } - - if let Err(e) = stream.set_nonblocking(true) { - report_error(&sink, format!("stream nonblocking error: {e}")); - continue; - } - - let connected_payload = match platform::connected_payload(&stream) { - Ok(payload) => payload, - Err(e) => { - report_error(&sink, format!("peer credentials error: {e}")); + let pid = match access::admit(&stream) { + Ok(pid) => pid, + Err(error) => { + if cfg!(debug_assertions) { + eprintln!("[IPC] rejected connection: {error}"); + } continue; } }; - - let (sender, rx, pending_bytes) = MessageSender::channel(MAX_PENDING_MESSAGES); - match STATE.lock() { - Ok(mut state) if server_active() => state.sender = Some(sender), - Ok(_) => break, - Err(e) => { - report_error(&sink, format!("state lock error: {e}")); - break; - } + // The host may send as soon as it sees `Connected`. + outbox.open(); + let listening = emit(Event::Connected { pid }); + let error = if listening { + exchange(stream, emit, outbox).await + } else { + None + }; + outbox.close(); + if !listening || !emit(Event::Disconnected { error }) { + return; } + } +} - if sink - .add(frame::make_frame(TYPE_CONNECTED, &connected_payload)) - .is_err() - { - break; +async fn exchange(stream: Stream, emit: &EventSink, outbox: &Outbox) -> Option { + let (reader, writer) = stream.split(); + let result = tokio::select! { + result = receive(reader, emit) => result, + result = transmit(writer, outbox) => result, + }; + match result { + Err(error) if !is_peer_gone(&error) => Some(error.to_string()), + _ => None, + } +} + +async fn receive(reader: RecvHalf, emit: &EventSink) -> io::Result<()> { + let mut reader = BufReader::new(reader); + loop { + let payload = frame::read(&mut reader).await?; + if !emit(Event::Message(payload)) { + return Ok(()); } + } +} - let (mut receiver, mut sender) = stream.split(); - let connection_running = Arc::new(AtomicBool::new(true)); - let writer_running = Arc::clone(&connection_running); - let writer = thread::spawn(move || { - let mut error = None; - let is_running = || writer_running.load(Ordering::SeqCst) && server_active(); - while is_running() { - match rx.recv_timeout(IO_POLL_INTERVAL) { - Ok(data) => { - pending_bytes.fetch_sub(data.len(), Ordering::SeqCst); - if let Err(e) = frame::write_frame(&mut sender, &data, &is_running) { - if e.kind() != io::ErrorKind::Interrupted { - error = Some(format!("write error: {e}")); - } - break; - } - } - Err(mpsc::RecvTimeoutError::Timeout) => continue, - Err(mpsc::RecvTimeoutError::Disconnected) => break, - } - } - writer_running.store(false, Ordering::SeqCst); - error - }); +async fn transmit(mut writer: SendHalf, outbox: &Outbox) -> io::Result<()> { + loop { + let frame = outbox.next().await; + writer.write_all(&frame).await?; + } +} - let mut frame_reader = FrameReader::default(); - while connection_running.load(Ordering::SeqCst) && server_active() { - #[cfg(windows)] - let poll_result = frame_reader.poll(&mut platform::PipeReader { - receiver: &mut receiver, - }); - #[cfg(not(windows))] - let poll_result = frame_reader.poll(&mut receiver); +fn is_peer_gone(error: &io::Error) -> bool { + matches!( + error.kind(), + io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe + ) +} - match poll_result { - Ok(Some(data)) => { - if sink.add(frame::make_frame(TYPE_DATA, &data)).is_err() { - break; - } - } - Ok(None) => thread::sleep(IO_POLL_INTERVAL), - Err(e) => { - if !frame::is_expected_disconnect_error(&e) && server_active() { - ipc_debug!("[IPC] read error: {e}, raw={:?}", e.raw_os_error()); - report_error(&sink, format!("read error: {e}")); - } - break; - } - } - } +#[cfg(all(test, unix))] +mod tests { + use super::*; + use std::io::{Read, Write}; + use std::os::unix::fs::PermissionsExt; + use std::os::unix::net::{UnixListener, UnixStream}; + use std::path::Path; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::mpsc::{channel, Receiver}; + use std::time::Instant; - connection_running.store(false, Ordering::SeqCst); - if let Ok(mut state) = STATE.lock() { - state.sender = None; - } - if let Ok(Some(message)) = writer.join() { - report_error(&sink, message); - } - if server_active() && sink.add(frame::make_frame(TYPE_DISCONNECTED, &[])).is_err() { - break; + const WAIT: Duration = Duration::from_secs(5); + + fn socket_path() -> String { + static NEXT: AtomicUsize = AtomicUsize::new(0); + let name = format!( + "flclash-ipc-{}-{}.sock", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed), + ); + std::env::temp_dir().join(name).to_str().unwrap().to_owned() + } + + fn attach(server: &Server) -> Receiver { + let (tx, rx) = channel(); + server.events(Box::new(move |event| tx.send(event).is_ok())); + rx + } + + fn serve(path: &str) -> (Server, Receiver) { + let server = Server::bind(path.to_owned()).unwrap(); + let events = attach(&server); + (server, events) + } + + fn connect(path: &str) -> UnixStream { + let peer = UnixStream::connect(path).unwrap(); + peer.set_read_timeout(Some(WAIT)).unwrap(); + peer + } + + fn next(events: &Receiver) -> Event { + events.recv_timeout(WAIT).expect("no IPC event arrived") + } + + fn expect_connected(events: &Receiver) { + assert!(matches!(next(events), Event::Connected { .. })); + } + + fn write_frame(peer: &mut UnixStream, payload: &[u8]) { + peer.write_all(&frame::encode(payload).unwrap()).unwrap(); + } + + fn read_frame(peer: &mut UnixStream) -> Vec { + let mut header = [0; 4]; + peer.read_exact(&mut header).unwrap(); + let mut payload = vec![0; u32::from_le_bytes(header) as usize]; + peer.read_exact(&mut payload).unwrap(); + payload + } + + fn wait_until(condition: impl Fn() -> bool) { + let deadline = Instant::now() + WAIT; + while !condition() { + assert!(Instant::now() < deadline, "condition never held"); + thread::sleep(Duration::from_millis(5)); } } - finish_server(&name); + #[test] + fn frames_flow_both_ways_between_connect_and_disconnect() { + let path = socket_path(); + let (server, events) = serve(&path); + let mut peer = connect(&path); + expect_connected(&events); + + write_frame(&mut peer, b"ping"); + assert_eq!(next(&events), Event::Message(b"ping".to_vec())); + + server.send(b"pong").unwrap(); + assert_eq!(read_frame(&mut peer), b"pong"); + + drop(peer); + assert_eq!(next(&events), Event::Disconnected { error: None }); + server.close(); + } + + #[cfg(target_os = "linux")] + #[test] + fn a_connection_reports_the_peer_process() { + let path = socket_path(); + let (server, events) = serve(&path); + let _peer = connect(&path); + + let pid = Some(std::process::id()); + assert_eq!(next(&events), Event::Connected { pid }); + server.close(); + } + + #[test] + fn frames_sent_in_a_burst_arrive_in_order() { + let path = socket_path(); + let (server, events) = serve(&path); + let mut peer = connect(&path); + expect_connected(&events); + + for index in 0..200_u32 { + server.send(&index.to_le_bytes()).unwrap(); + write_frame(&mut peer, &index.to_le_bytes()); + } + + for index in 0..200_u32 { + assert_eq!(read_frame(&mut peer), index.to_le_bytes()); + assert_eq!(next(&events), Event::Message(index.to_le_bytes().to_vec())); + } + server.close(); + } + + #[test] + fn a_frame_larger_than_the_socket_buffer_arrives_whole() { + let path = socket_path(); + let (server, events) = serve(&path); + let mut peer = connect(&path); + expect_connected(&events); + let payload: Vec = (0..4 * 1024 * 1024).map(|index| index as u8).collect(); + + server.send(&payload).unwrap(); + assert_eq!(read_frame(&mut peer), payload); + + write_frame(&mut peer, &payload); + assert_eq!(next(&events), Event::Message(payload)); + server.close(); + } + + #[test] + fn the_next_peer_is_served_after_the_last_one_left() { + let path = socket_path(); + let (server, events) = serve(&path); + drop(connect(&path)); + expect_connected(&events); + assert_eq!(next(&events), Event::Disconnected { error: None }); + + let mut peer = connect(&path); + expect_connected(&events); + server.send(b"again").unwrap(); + + assert_eq!(read_frame(&mut peer), b"again"); + server.close(); + } + + #[test] + fn a_send_without_a_peer_is_refused() { + let path = socket_path(); + let (server, events) = serve(&path); + assert_eq!( + server.send(b"early").unwrap_err().kind(), + io::ErrorKind::NotConnected, + ); + + drop(connect(&path)); + expect_connected(&events); + assert_eq!(next(&events), Event::Disconnected { error: None }); + + assert_eq!( + server.send(b"late").unwrap_err().kind(), + io::ErrorKind::NotConnected, + ); + server.close(); + } + + #[test] + fn a_peer_that_connects_before_events_are_attached_is_still_reported() { + let path = socket_path(); + let server = Server::bind(path.clone()).unwrap(); + let mut peer = connect(&path); + write_frame(&mut peer, b"early"); + + let events = attach(&server); + + expect_connected(&events); + assert_eq!(next(&events), Event::Message(b"early".to_vec())); + server.close(); + } + + #[test] + fn an_oversized_frame_ends_the_connection_with_an_error() { + let path = socket_path(); + let (server, events) = serve(&path); + let mut peer = connect(&path); + expect_connected(&events); + + let header = (frame::MAX_FRAME_SIZE as u32 + 1).to_le_bytes(); + peer.write_all(&header).unwrap(); + + let Event::Disconnected { error: Some(error) } = next(&events) else { + panic!("the connection did not end with an error"); + }; + assert!(error.contains("exceeds"), "{error}"); + assert_eq!(peer.read(&mut [0; 1]).unwrap(), 0); + server.close(); + } + + #[test] + fn closing_disconnects_the_peer_and_removes_the_socket() { + let path = socket_path(); + let (server, events) = serve(&path); + let mut peer = connect(&path); + expect_connected(&events); + + server.close(); + + assert_eq!(peer.read(&mut [0; 1]).unwrap(), 0); + assert!(!Path::new(&path).exists()); + assert!(events.recv_timeout(WAIT).is_err()); + assert_eq!( + server.send(b"closed").unwrap_err().kind(), + io::ErrorKind::NotConnected, + ); + server.close(); + } + + #[test] + fn dropping_the_server_releases_the_socket() { + let path = socket_path(); + let (server, _events) = serve(&path); + + drop(server); + + wait_until(|| !Path::new(&path).exists()); + } + + #[test] + fn the_server_ends_once_nobody_listens_for_events() { + let path = socket_path(); + let (_server, events) = serve(&path); + drop(events); + + let _peer = connect(&path); + + wait_until(|| !Path::new(&path).exists()); + } + + #[test] + fn binding_takes_over_a_stale_socket_file() { + let path = socket_path(); + drop(UnixListener::bind(&path).unwrap()); + assert!(Path::new(&path).exists()); + + let (server, events) = serve(&path); + let _peer = connect(&path); + + expect_connected(&events); + server.close(); + } + + #[test] + fn the_socket_is_reachable_by_its_owner_only() { + let path = socket_path(); + let server = Server::bind(path.clone()).unwrap(); + + let mode = std::fs::metadata(&path).unwrap().permissions().mode(); + + assert_eq!(mode & 0o777, 0o600); + server.close(); + } + + #[test] + fn an_unusable_address_fails_the_bind() { + let path = std::env::temp_dir() + .join("flclash-ipc-missing-dir") + .join("core.sock"); + + assert!(Server::bind(path.to_str().unwrap().to_owned()).is_err()); + } } diff --git a/plugins/rust_api/rust/src/ipc/unsupported.rs b/plugins/rust_api/rust/src/ipc/unsupported.rs index f2538e36..d6aac479 100644 --- a/plugins/rust_api/rust/src/ipc/unsupported.rs +++ b/plugins/rust_api/rust/src/ipc/unsupported.rs @@ -1,18 +1,26 @@ -use crate::frb_generated::StreamSink; -use flutter_rust_bridge::for_generated::SseCodec; +use super::EventSink; +use std::io; -// Android loads the Core in-process; these entry points exist only so that one -// set of bindings serves every platform. -const UNSUPPORTED: &str = "IPC server is not available on this platform"; +// Android loads the Core in-process; this keeps one set of bindings for every platform. +pub struct Server; -pub fn restart_server(_name: String, _sink: StreamSink, SseCodec>) -> Result<(), String> { - Err(UNSUPPORTED.into()) +fn unsupported() -> io::Error { + io::Error::new( + io::ErrorKind::Unsupported, + "IPC server is not available on this platform", + ) } -pub fn stop_server() -> Result<(), String> { - Ok(()) -} +impl Server { + pub fn bind(_address: String) -> io::Result { + Err(unsupported()) + } -pub fn send_message(_data: Vec) -> Result<(), String> { - Err(UNSUPPORTED.into()) + pub fn events(&self, _sink: EventSink) {} + + pub fn send(&self, _payload: &[u8]) -> io::Result<()> { + Err(unsupported()) + } + + pub fn close(&self) {} } diff --git a/plugins/rust_api/rust/src/script/mod.rs b/plugins/rust_api/rust/src/script/mod.rs index 2864b9c2..e29b24d1 100644 --- a/plugins/rust_api/rust/src/script/mod.rs +++ b/plugins/rust_api/rust/src/script/mod.rs @@ -226,7 +226,7 @@ mod tests { assert_eq!(result["entries"], json!({ "a": 0, "b": 1 })); } - fn overwrite_fixture() -> Json { + fn extension_fixture() -> Json { let script = include_str!("../../tests/fixtures/profile_script.js"); let config = include_str!("../../tests/fixtures/profile_config.json"); @@ -234,8 +234,8 @@ mod tests { } #[test] - fn runs_a_profile_overwrite_end_to_end() { - let result = overwrite_fixture(); + fn runs_a_profile_extension_end_to_end() { + let result = extension_fixture(); let proxies = result["proxies"].as_array().unwrap(); let groups = result["proxy-groups"].as_array().unwrap(); let rules = result["rules"].as_array().unwrap(); @@ -247,8 +247,8 @@ mod tests { } #[test] - fn the_overwrite_renames_nodes_and_rewrites_the_chains_through_them() { - let result = overwrite_fixture(); + fn the_extension_renames_nodes_and_rewrites_the_chains_through_them() { + let result = extension_fixture(); let proxies = result["proxies"].as_array().unwrap(); let named = |name: &str| { proxies @@ -266,8 +266,8 @@ mod tests { } #[test] - fn the_overwrite_merges_dns_instead_of_replacing_it() { - let dns = overwrite_fixture()["dns"].clone(); + fn the_extension_merges_dns_instead_of_replacing_it() { + let dns = extension_fixture()["dns"].clone(); assert_eq!(dns["enhanced-mode"], json!("fake-ip")); assert_eq!(dns["nameserver"], json!(["223.5.5.5", "119.29.29.29"])); diff --git a/plugins/rust_api/rust/tests/fixtures/profile_script.js b/plugins/rust_api/rust/tests/fixtures/profile_script.js index ecac6c03..d2e01605 100644 --- a/plugins/rust_api/rust/tests/fixtures/profile_script.js +++ b/plugins/rust_api/rust/tests/fixtures/profile_script.js @@ -1,6 +1,6 @@ /** - * A profile overwrite script written for this test suite. It does the work a - * real overwrite does — drop subscription notices, canonicalise node names, + * A profile extension script written for this test suite. It does the work a + * real one does — drop subscription notices, canonicalise node names, * group by region, rate and dedicated line, build policy groups and rules, * merge DNS — so the engine is measured against how such scripts are written * rather than against a checklist of language features. diff --git a/plugins/tray/test/windows_menu_source_test.dart b/plugins/tray/test/windows_menu_source_test.dart index e5bc2f2e..d32dba92 100644 --- a/plugins/tray/test/windows_menu_source_test.dart +++ b/plugins/tray/test/windows_menu_source_test.dart @@ -14,6 +14,13 @@ File _resolveSource(String relativePath) { return direct; } +String _body(String source, String signature) { + final start = source.indexOf(signature); + expect(start, isNonNegative, reason: signature); + final end = source.indexOf('\n}\n', start); + return source.substring(start, end); +} + void main() { late String pluginSource; @@ -42,16 +49,39 @@ void main() { }); test('windows rejected show leaves the visible menu untouched', () { - expect(pluginSource, contains('bool applied = ApplyIcon(!visible_);')); - expect( - pluginSource, - contains(''' - if (!applied) { - return false; - } - visible_ = true; + final show = _body(pluginSource, 'bool TrayPlugin::Show('); + final lastRejection = show.lastIndexOf('return false;'); + final rebuild = show.indexOf('RebuildMenu(menu_, *items);'); - const flutter::EncodableList* items'''), + expect(lastRejection, isNonNegative); + expect(rebuild, greaterThan(lastRejection)); + }); + + test('windows retries an icon the shell refused at sign-in', () { + final show = _body(pluginSource, 'bool TrayPlugin::Show('); + + expect(show, contains('ScheduleRestore();')); + expect(show.trimRight(), endsWith('return true;')); + expect( + _body(pluginSource, 'void TrayPlugin::RestoreIcon('), + contains('ApplyIcon(false) || ApplyIcon(true)'), + ); + expect(pluginSource, contains('message == WM_TIMER')); + }); + + test('windows hide deletes an icon whose add may have landed', () { + final hide = _body(pluginSource, 'void TrayPlugin::Hide('); + + expect( + hide, + contains(''' + if (icon_requested_) { + ::Shell_NotifyIconW(NIM_DELETE, &icon_data_); + }'''), + ); + expect( + hide.indexOf('icon_requested_ = false;'), + greaterThan(hide.indexOf('NIM_DELETE')), ); }); } diff --git a/plugins/tray/windows/tray_plugin.cpp b/plugins/tray/windows/tray_plugin.cpp index c71ef370..95e4f162 100644 --- a/plugins/tray/windows/tray_plugin.cpp +++ b/plugins/tray/windows/tray_plugin.cpp @@ -10,6 +10,8 @@ namespace { constexpr UINT kTrayCallbackMessage = WM_USER + 1; constexpr UINT kTrayIconId = 1; +constexpr UINT_PTR kRestoreTimerId = 0x54524159; // "TRAY" +constexpr UINT kRestoreIntervalMilliseconds = 2000; const flutter::EncodableValue* ValueAt(const flutter::EncodableMap& map, const char* key) { @@ -188,15 +190,6 @@ bool TrayPlugin::Show(const flutter::EncodableMap& arguments) { tool_tip_ = Utf16FromUtf8(*tool_tip); } - bool applied = ApplyIcon(!visible_); - if (!applied && visible_) { - applied = ApplyIcon(true); - } - if (!applied) { - return false; - } - visible_ = true; - const flutter::EncodableList* items = ListAt(arguments, "menu"); if (items != nullptr) { if (menu_ == nullptr) { @@ -205,13 +198,40 @@ bool TrayPlugin::Show(const flutter::EncodableMap& arguments) { RebuildMenu(menu_, *items); } + icon_requested_ = true; + bool applied = ApplyIcon(!visible_); + if (!applied && visible_) { + applied = ApplyIcon(true); + } + if (applied) { + visible_ = true; + } else { + ScheduleRestore(); + } return true; } +void TrayPlugin::ScheduleRestore() { + ::SetTimer(MainWindow(), kRestoreTimerId, kRestoreIntervalMilliseconds, + nullptr); +} + +// An add that timed out may still have landed, so modify goes first. +void TrayPlugin::RestoreIcon() { + if (ApplyIcon(false) || ApplyIcon(true)) { + visible_ = true; + ::KillTimer(MainWindow(), kRestoreTimerId); + } else { + ScheduleRestore(); + } +} + void TrayPlugin::Hide() { - if (visible_) { + ::KillTimer(MainWindow(), kRestoreTimerId); + if (icon_requested_) { ::Shell_NotifyIconW(NIM_DELETE, &icon_data_); } + icon_requested_ = false; if (icon_data_.hIcon != nullptr) { ::DestroyIcon(icon_data_.hIcon); } @@ -267,12 +287,17 @@ std::optional TrayPlugin::HandleWindowProc(HWND window, return std::nullopt; } + if (message == WM_TIMER && wparam == kRestoreTimerId) { + RestoreIcon(); + return 0; + } + const bool should_restore = (taskbar_created_message_ != 0 && message == taskbar_created_message_) || (message == WM_POWERBROADCAST && (wparam == PBT_APMRESUMEAUTOMATIC || wparam == PBT_APMRESUMESUSPEND)); - if (should_restore && visible_) { - ApplyIcon(true); + if (should_restore && icon_requested_) { + RestoreIcon(); } return std::nullopt; diff --git a/plugins/tray/windows/tray_plugin.h b/plugins/tray/windows/tray_plugin.h index ddb09097..2d48b81b 100644 --- a/plugins/tray/windows/tray_plugin.h +++ b/plugins/tray/windows/tray_plugin.h @@ -37,6 +37,8 @@ class TrayPlugin : public flutter::Plugin { void Hide(); bool OpenMenu(); bool ApplyIcon(bool add); + void RestoreIcon(); + void ScheduleRestore(); void RebuildMenu(HMENU menu, const flutter::EncodableList& items); void SendEvent(const char* name, const flutter::EncodableValue& arguments); @@ -53,6 +55,7 @@ class TrayPlugin : public flutter::Plugin { HMENU menu_ = nullptr; std::wstring tool_tip_; bool visible_ = false; + bool icon_requested_ = false; UINT taskbar_created_message_ = 0; int window_proc_id_ = -1; diff --git a/plugins/window/windows/window_controller.cpp b/plugins/window/windows/window_controller.cpp index 8dd2d72f..74b83411 100644 --- a/plugins/window/windows/window_controller.cpp +++ b/plugins/window/windows/window_controller.cpp @@ -46,7 +46,9 @@ int WindowController::TopBorderHeight() const { } void WindowController::PlaceView() const { - if (hwnd_ == nullptr || view_ == nullptr) { + // Minimizing changes the frame too. Fitting the view to the sliver of client + // area left would lay the app out at mobile width, which a restore then shows. + if (hwnd_ == nullptr || view_ == nullptr || IsMinimized()) { return; } RECT client{}; diff --git a/plugins/window/windows/window_style.h b/plugins/window/windows/window_style.h index 8ce2ddbb..ae998304 100644 --- a/plugins/window/windows/window_style.h +++ b/plugins/window/windows/window_style.h @@ -50,8 +50,8 @@ struct WindowStyle { // Rows the controller keeps clear above the view and paints transparent. // Windows 10 draws the top border only inside a frame extended over the // whole caption (a shorter non-client strip makes DWM draw the caption), so - // Apply() extends it there; Windows 11 draws the border into the 1 px - // non-client strip WM_NCCALCSIZE leaves. Zero while an accent effect is on, + // Apply() extends it there; Windows 11 draws the border into the non-client + // strip, one physical pixel tall, that WM_NCCALCSIZE leaves. Zero while an accent effect is on, // maximized or full screen. int TopBorderHeight(HWND hwnd) const; }; diff --git a/services/helper/src/service/hub.rs b/services/helper/src/service/hub.rs index 723766a5..5962d33f 100644 --- a/services/helper/src/service/hub.rs +++ b/services/helper/src/service/hub.rs @@ -37,8 +37,11 @@ use windows_sys::Win32::System::JobObjects::{ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, }; -#[cfg(not(target_os = "linux"))] -pub(super) const LISTEN_PORT: u16 = 47890; +#[cfg(not(any( + all(feature = "windows-service", target_os = "windows"), + target_os = "linux" +)))] +const LISTEN_PORT: u16 = 47890; #[cfg(not(target_os = "linux"))] const CORE_PIPE_PREFIX: &str = r"\\.\pipe\FlClashCore_"; #[cfg(target_os = "linux")] @@ -356,6 +359,36 @@ fn lock_surviving_poison(mutex: &Mutex) -> MutexGuard<'_, T> { .unwrap_or_else(|poisoned| poisoned.into_inner()) } +/// A Core whose app went away exits on its own, and stays a zombie until it is +/// waited on; without this nothing waits before the next start or stop. +fn reap_exited_core(session_id: &str) { + let deadline = Instant::now() + CORE_EXIT_TIMEOUT; + loop { + { + let mut managed = lock_surviving_poison(&MANAGED_CORE); + let Some(core) = managed + .as_mut() + .filter(|core| core.session_id == session_id) + else { + return; + }; + match core.child.try_wait() { + Ok(Some(status)) => { + log_message(format!("Core exited on its own: {status}")); + *managed = None; + return; + } + Ok(None) => {} + Err(_) => return, + } + } + if Instant::now() >= deadline { + return; + } + thread::sleep(CORE_EXIT_POLL_INTERVAL); + } +} + fn release_managed_core(managed: &mut Option) -> Result<(), Error> { let Some(core) = managed.as_mut() else { return Ok(()); @@ -418,7 +451,12 @@ fn start(start_params: StartParams) -> warp::reply::Response { if let Err(error) = super::linux::ensure_owner_socket(&start_params.address) { return error_response("invalidRequest", error.to_string(), StatusCode::BAD_REQUEST); } + launch(start_params) +} +/// Releases the managed Core before anything else, so a request reaching this +/// point must already be accepted: a rejected one has to leave it running. +fn launch(start_params: StartParams) -> warp::reply::Response { let mut managed = lock_surviving_poison(&MANAGED_CORE); if let Err(error) = release_managed_core(&mut managed) { log_message(format!( @@ -446,6 +484,7 @@ fn start(start_params: StartParams) -> warp::reply::Response { let process_id = child.id(); if let Some(stderr) = child.stderr.take() { let reader = io::BufReader::new(stderr); + let session_id = start_params.session_id.clone(); thread::spawn(move || { for line in reader.lines() { match line { @@ -457,6 +496,7 @@ fn start(start_params: StartParams) -> warp::reply::Response { } } } + reap_exited_core(&session_id); }); } *managed = match ManagedCore::adopt(start_params.session_id.clone(), child) { @@ -1077,7 +1117,7 @@ mod tests { #[tokio::test] #[allow(clippy::await_holding_lock)] - async fn start_releases_the_managed_core_before_rejecting_an_unverified_core() { + async fn launch_releases_the_managed_core_before_rejecting_an_unverified_core() { let _state = lock_process_state(); *lock_surviving_poison(&MANAGED_CORE) = Some( ManagedCore::adopt( @@ -1087,20 +1127,56 @@ mod tests { .unwrap(), ); - let response = warp::test::request() - .method("POST") - .path("/start") - .json(&StartParams { - address: ALLOWED_CORE_ADDRESS.to_string(), - session_id: "0123456789abcdef0123456789abcdef".to_string(), - }) - .reply(&routes()) - .await; + let response = launch(StartParams { + address: ALLOWED_CORE_ADDRESS.to_string(), + session_id: "0123456789abcdef0123456789abcdef".to_string(), + }); assert_eq!(response.status(), StatusCode::CONFLICT); - let body: serde_json::Value = serde_json::from_slice(response.body()).unwrap(); - assert_eq!(body["code"], "coreVerificationFailed"); assert!(lock_surviving_poison(&MANAGED_CORE).is_none()); + let body: serde_json::Value = serde_json::from_slice( + &warp::hyper::body::to_bytes(response.into_body()) + .await + .unwrap(), + ) + .unwrap(); + assert_eq!(body["code"], "coreVerificationFailed"); + } + + #[test] + fn a_core_that_exits_on_its_own_is_reaped() { + let _state = lock_process_state(); + let session_id = "0123456789abcdef0123456789abcdef"; + *lock_surviving_poison(&MANAGED_CORE) = + Some(ManagedCore::adopt(session_id.to_string(), spawn_placeholder_core()).unwrap()); + + reap_exited_core(session_id); + + assert!(lock_surviving_poison(&MANAGED_CORE).is_none()); + } + + #[test] + fn reaping_leaves_a_running_or_newer_core_owned() { + let _state = lock_process_state(); + let session_id = "0123456789abcdef0123456789abcdef"; + *lock_surviving_poison(&MANAGED_CORE) = + Some(ManagedCore::adopt(session_id.to_string(), spawn_placeholder_core()).unwrap()); + + reap_exited_core("fedcba9876543210fedcba9876543210"); + assert!(lock_surviving_poison(&MANAGED_CORE).is_some()); + + adopt_core(session_id); + reap_exited_core(session_id); + + let mut managed = lock_surviving_poison(&MANAGED_CORE); + assert!(managed + .as_mut() + .unwrap() + .child + .try_wait() + .unwrap() + .is_none()); + release_managed_core(&mut managed).unwrap(); } #[tokio::test] diff --git a/services/helper/src/service/peer.rs b/services/helper/src/service/peer.rs index f69d8e51..1dc789c6 100644 --- a/services/helper/src/service/peer.rs +++ b/services/helper/src/service/peer.rs @@ -1,6 +1,5 @@ use crate::service::hub::{ ensure_core_sha256_configured, log_message, release_managed_core_on_shutdown, routes, - LISTEN_PORT, }; use crate::service::owner::{client_pid, same_sid, TcpConnection}; @@ -9,7 +8,7 @@ use std::future::Future; use std::io::{Error, Result as IoResult}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::pin::Pin; -use std::ptr::null_mut; +use std::ptr::{null, null_mut}; use std::sync::atomic::{AtomicBool, Ordering}; use std::task::{Context, Poll}; use std::time::Duration; @@ -17,18 +16,26 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::time::Sleep; use tokio_stream::Stream; use windows_sys::Win32::Foundation::{ - CloseHandle, LocalFree, ERROR_INSUFFICIENT_BUFFER, HANDLE, NO_ERROR, + CloseHandle, LocalFree, ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS, HANDLE, NO_ERROR, }; use windows_sys::Win32::NetworkManagement::IpHelper::{ GetExtendedTcpTable, MIB_TCPTABLE_OWNER_PID, TCP_TABLE_OWNER_PID_ALL, }; use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW; use windows_sys::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER}; +use windows_sys::Win32::System::Registry::{ + RegCloseKey, RegCreateKeyExW, RegDeleteKeyW, RegSetValueExW, HKEY, HKEY_LOCAL_MACHINE, + KEY_SET_VALUE, REG_DWORD, REG_OPTION_VOLATILE, +}; use windows_sys::Win32::System::Threading::{ OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION, }; const AF_INET: u32 = 2; +/// Hyper-V and WinNAT reserve ranges that can cover any fixed port; only admins +/// write this volatile key under the service's own, and a reboot clears it. +const PORT_KEY: &str = r"SYSTEM\CurrentControlSet\Services\FlClashHelperService\Runtime"; +const PORT_VALUE: &str = "Port"; const TABLE_ATTEMPTS: usize = 5; const ACCEPT_RETRY_DELAY: Duration = Duration::from_secs(1); @@ -199,6 +206,61 @@ impl Stream for AuthorizedIncoming { } } +fn wide(value: &str) -> Vec { + value.encode_utf16().chain(std::iter::once(0)).collect() +} + +struct PublishedPort; + +impl PublishedPort { + fn publish(port: u16) -> IoResult { + let key_path = wide(PORT_KEY); + let value_name = wide(PORT_VALUE); + let data = u32::from(port).to_le_bytes(); + // SAFETY: the buffers outlive the calls, and the opened key is closed. + let status = unsafe { + let mut key: HKEY = 0; + let status = RegCreateKeyExW( + HKEY_LOCAL_MACHINE, + key_path.as_ptr(), + 0, + null(), + REG_OPTION_VOLATILE, + KEY_SET_VALUE, + null(), + &mut key, + null_mut(), + ); + if status != ERROR_SUCCESS { + return Err(Error::from_raw_os_error(status as i32)); + } + let status = RegSetValueExW( + key, + value_name.as_ptr(), + 0, + REG_DWORD, + data.as_ptr(), + data.len() as u32, + ); + RegCloseKey(key); + status + }; + if status != ERROR_SUCCESS { + return Err(Error::from_raw_os_error(status as i32)); + } + Ok(Self) + } +} + +impl Drop for PublishedPort { + fn drop(&mut self) { + let key_path = wide(PORT_KEY); + unsafe { + RegDeleteKeyW(HKEY_LOCAL_MACHINE, key_path.as_ptr()); + } + } +} + pub(super) async fn serve_until( owner_sid: String, shutdown: F, @@ -210,9 +272,15 @@ where { ensure_core_sha256_configured()?; - let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, LISTEN_PORT)) + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) .await .map_err(|error| anyhow::anyhow!("bind helper server: {error}"))?; + let port = listener + .local_addr() + .map_err(|error| anyhow::anyhow!("read helper server port: {error}"))? + .port(); + let published = PublishedPort::publish(port) + .map_err(|error| anyhow::anyhow!("publish helper server port: {error}"))?; on_started()?; let incoming = AuthorizedIncoming { listener, @@ -222,6 +290,7 @@ where warp::serve(routes()) .serve_incoming_with_graceful_shutdown(incoming, shutdown) .await; + drop(published); release_managed_core_on_shutdown(); Ok(()) diff --git a/test/common/preferences_store_test.dart b/test/common/preferences_store_test.dart new file mode 100644 index 00000000..41195eb9 --- /dev/null +++ b/test/common/preferences_store_test.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:fl_clash/common/preferences_store.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'package:shared_preferences_platform_interface/types.dart'; + +void main() { + late Directory dir; + late String path; + + setUp(() async { + dir = await Directory.systemTemp.createTemp('preferences_store'); + path = join(dir.path, 'shared_preferences.json'); + }); + + tearDown(() => dir.delete(recursive: true)); + + AtomicFilePreferencesStore openStore() { + return AtomicFilePreferencesStore(Future.value(path)); + } + + Object? readFile() => json.decode(File(path).readAsStringSync()); + + List dirEntries() { + return dir.listSync().map((entity) => basename(entity.path)).toList(); + } + + test('reads the file the upstream store writes', () async { + File(path).writeAsStringSync( + json.encode({ + 'flutter.config': '{"a":1}', + 'flutter.version': 1, + 'flutter.flag': true, + 'flutter.ratio': 0.5, + 'flutter.list': ['a', 'b'], + 'other': 'kept', + }), + ); + + expect(await openStore().getAll(), { + 'flutter.config': '{"a":1}', + 'flutter.version': 1, + 'flutter.flag': true, + 'flutter.ratio': 0.5, + 'flutter.list': ['a', 'b'], + }); + }); + + test('a missing or empty file reads as empty', () async { + expect(await openStore().getAll(), isEmpty); + + File(path).writeAsStringSync(''); + + expect(await openStore().getAll(), isEmpty); + }); + + test('a torn file fails to load until it is readable again', () async { + File(path).writeAsStringSync('{"flutter.config": "{'); + final store = openStore(); + + await expectLater(store.getAll(), throwsFormatException); + + File(path).writeAsStringSync(json.encode({'flutter.config': '{}'})); + + expect(await store.getAll(), {'flutter.config': '{}'}); + }); + + test('writes keep keys outside the prefix', () async { + File(path).writeAsStringSync(json.encode({'other': 'kept'})); + final store = openStore(); + + expect(await store.setValue('String', 'flutter.config', 'x'), isTrue); + expect(await store.remove('flutter.missing'), isTrue); + expect(readFile(), {'other': 'kept', 'flutter.config': 'x'}); + + expect(await store.clear(), isTrue); + expect(readFile(), {'other': 'kept'}); + }); + + test('parameters filter by prefix and allow list', () async { + final store = openStore(); + await store.setValue('String', 'flutter.a', 'a'); + await store.setValue('String', 'flutter.b', 'b'); + await store.setValue('String', 'other.c', 'c'); + + expect( + await store.getAllWithParameters( + GetAllParameters( + filter: PreferencesFilter( + prefix: 'flutter.', + allowList: {'flutter.a', 'other.c'}, + ), + ), + ), + {'flutter.a': 'a'}, + ); + + await store.clearWithParameters( + ClearParameters(filter: PreferencesFilter(prefix: 'other.')), + ); + expect(readFile(), {'flutter.a': 'a', 'flutter.b': 'b'}); + }); + + test('overlapping writes all land and leave no temp file', () async { + final store = openStore(); + + final results = await Future.wait([ + for (var i = 0; i < 50; i++) store.setValue('Int', 'flutter.k$i', i), + ]); + + expect(results, everyElement(isTrue)); + expect(readFile(), {for (var i = 0; i < 50; i++) 'flutter.k$i': i}); + expect(dirEntries(), ['shared_preferences.json']); + }); + + test('a save that cannot be written leaves the previous file', () async { + File(path).writeAsStringSync(json.encode({'flutter.config': 'old'})); + Directory('$path.$pid.tmp').createSync(); + + expect( + await openStore().setValue('String', 'flutter.config', 'new'), + isFalse, + ); + expect(readFile(), {'flutter.config': 'old'}); + }); + + test('a save that cannot replace the file removes its temp file', () async { + Directory(path).createSync(); + File(join(path, 'occupied')).createSync(); + + expect( + await openStore().setValue('String', 'flutter.config', 'new'), + isFalse, + ); + expect(dirEntries(), ['shared_preferences.json']); + }); + + test('backs SharedPreferences', () async { + addTearDown(SharedPreferences.resetStatic); + SharedPreferencesStorePlatform.instance = openStore(); + final preferences = await SharedPreferences.getInstance(); + await preferences.setString('config', '{}'); + await preferences.setStringList('list', ['a']); + + expect(readFile(), { + 'flutter.config': '{}', + 'flutter.list': ['a'], + }); + + SharedPreferences.resetStatic(); + SharedPreferencesStorePlatform.instance = openStore(); + final reopened = await SharedPreferences.getInstance(); + + expect(reopened.getString('config'), '{}'); + expect(reopened.getStringList('list'), ['a']); + }); +} diff --git a/test/common/preferences_test.dart b/test/common/preferences_test.dart index b5599c8e..290f0f49 100644 --- a/test/common/preferences_test.dart +++ b/test/common/preferences_test.dart @@ -49,7 +49,6 @@ void main() { const config = Config( themeProps: defaultThemeProps, currentProfileId: 42, - overrideDns: true, excludeSSIDs: ['home'], ); @@ -58,7 +57,6 @@ void main() { expect(restored, isNotNull); expect(restored!.currentProfileId, 42); - expect(restored.overrideDns, isTrue); expect(restored.excludeSSIDs, ['home']); }); diff --git a/test/common/system_test.dart b/test/common/system_test.dart index 47c62748..d1df74f4 100644 --- a/test/common/system_test.dart +++ b/test/common/system_test.dart @@ -28,6 +28,8 @@ class _RecordedRun { String get key => arguments.isEmpty ? executable : '$executable ${arguments.first}'; + + String get commandLine => [executable, ...arguments].join(' '); } /// Stands in for `Process.run`, keyed by executable plus its first argument so @@ -47,6 +49,17 @@ class _FakeProcesses { _failures.add(key); } + final List inputs = []; + + Future runWithInput( + String executable, + List arguments, + String input, + ) { + inputs.add(input); + return run(executable, arguments); + } + Future run(String executable, List arguments) async { final recorded = _RecordedRun(executable, arguments); runs.add(recorded); @@ -55,8 +68,14 @@ class _FakeProcesses { } return ProcessResult( 0, - _exitCodes[recorded.key] ?? _exitCodes[executable] ?? 0, - _stdout[recorded.key] ?? _stdout[executable] ?? '', + _exitCodes[recorded.commandLine] ?? + _exitCodes[recorded.key] ?? + _exitCodes[executable] ?? + 0, + _stdout[recorded.commandLine] ?? + _stdout[recorded.key] ?? + _stdout[executable] ?? + '', '', ); } @@ -94,6 +113,7 @@ void main() { late Directory root; late _FakeProcesses processes; + String? password; setUpAll(() { root = Directory.systemTemp.createTempSync('system_test'); @@ -112,13 +132,16 @@ void main() { processes = _FakeProcesses(); system.runProcess = processes.run; MacOS().runProcess = processes.run; - Linux().runProcess = processes.run; + linuxElevation.run = processes.run; + linuxElevation.runWithInput = processes.runWithInput; + linuxElevation.askPassword = () async => password; + password = null; }); tearDown(() { system.runProcess = Process.run; MacOS().runProcess = Process.run; - Linux().runProcess = Process.run; + linuxElevation.run = Process.run; }); group('statArguments', () { @@ -325,7 +348,10 @@ void main() { test('Linux hands the Core to the requesting user\'s group', () { final shell = System.linuxElevationShell('/opt/FlClash/FlClashCore'); - expect(shell, startsWith(r'group=$(id -g "$PKEXEC_UID") && ')); + expect( + shell, + startsWith(r'group=$(id -g "${PKEXEC_UID:-$SUDO_UID}") && '), + ); expect(shell, contains(r'chown "root:$group" ')); expect(shell, endsWith("chmod 4750 '/opt/FlClash/FlClashCore'")); }); @@ -375,10 +401,60 @@ void main() { expect(await Linux().installService(), isFalse); }); - test('reports a host with no pkexec at all', () async { + test('installs through cached sudo credentials without pkexec', () async { processes.stubThrow('pkexec'); + expect(await Linux().installService(), isTrue); + expect(processes.argumentsFor('sudo'), [ + '-n', + '--', + appPath.helperPath, + 'install', + ]); + expect(processes.inputs, isEmpty); + }); + + test('asks for the sudo password when no polkit agent answers', () async { + processes.stub('pkexec', '', exitCode: 127); + processes.stub('sudo -n', '', exitCode: 1); + password = 'secret'; + + expect(await Linux().installService(), isTrue); + expect(processes.runs.last.arguments, [ + '-S', + '-p', + '', + '--', + appPath.helperPath, + 'install', + ]); + expect(processes.inputs, ['secret\n']); + }); + + test('reports a command that failed under cached sudo', () async { + processes.stubThrow('pkexec'); + processes.stub('sudo -n', '', exitCode: 1); + processes.stub('sudo -n true', ''); + password = 'secret'; + expect(await Linux().installService(), isFalse); + expect(processes.ran('sudo -S'), isFalse); + expect(processes.inputs, isEmpty); + }); + + test('gives up when the sudo prompt is cancelled', () async { + processes.stubThrow('pkexec'); + processes.stub('sudo -n', '', exitCode: 1); + + expect(await Linux().installService(), isFalse); + expect(processes.ran('sudo -S'), isFalse); + }); + + test('does not ask sudo after the polkit dialog is dismissed', () async { + processes.stub('pkexec', '', exitCode: 126); + + expect(await Linux().installService(), isFalse); + expect(processes.ran('sudo'), isFalse); }); }); diff --git a/test/common/tray_menu_test.dart b/test/common/tray_menu_test.dart index 6aab9da8..0a098205 100644 --- a/test/common/tray_menu_test.dart +++ b/test/common/tray_menu_test.dart @@ -326,11 +326,21 @@ void main() { overrides: [ selectedProxyNameProvider('Proxy').overrideWithValue('A'), trayDelaysProvider.overrideWithValue({ - 'Proxy': {'A': 120, 'B': -1}, + 'Proxy': {'A': 120, 'B': -1, 'C': -2}, }), ], ); - await update(_trayState(groups: [_proxyGroup])); + const group = Group( + name: 'Proxy', + type: GroupType.Selector, + all: [ + Proxy(name: 'A', type: 'Direct'), + Proxy(name: 'B', type: 'Direct'), + Proxy(name: 'C', type: 'Direct'), + Proxy(name: 'D', type: 'Direct'), + ], + ); + await update(_trayState(groups: [group])); final submenu = _item(showCall(), 'Proxy'); expect(submenu['detail'], '120'); @@ -338,6 +348,7 @@ void main() { expect(children.map((item) => item['detail']), [ '120', currentAppLocalizations.timeout, + currentAppLocalizations.delayFailed, null, ]); }, diff --git a/test/common/window_test.dart b/test/common/window_test.dart index 3ab77d6a..267ae057 100644 --- a/test/common/window_test.dart +++ b/test/common/window_test.dart @@ -5,6 +5,7 @@ import 'package:fl_clash/common/window.dart'; import 'package:fl_clash/models/config.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:screen_retriever/screen_retriever.dart'; const _windowChannel = MethodChannel('window'); @@ -198,6 +199,89 @@ void main() { }, ); + group('restoredWindowPosition', () { + const primary = Display( + id: 'primary', + size: Size(1920, 1080), + visiblePosition: Offset.zero, + scaleFactor: 1, + ); + // 2560x1440 physical at 150%, right of the primary: screen_retriever + // divides its origin by its own scale. + const secondary = Display( + id: 'secondary', + size: Size(1707, 960), + visiblePosition: Offset(1280, 0), + scaleFactor: 1.5, + ); + + test('places a window saved on a 150% monitor back on it', () { + const props = WindowProps( + width: 800, + height: 600, + left: 2200 / 1.5, + top: 150 / 1.5, + scale: 1.5, + ); + + final position = restoredWindowPosition( + props, + displays: const [primary, secondary], + currentScale: 1, + ); + + expect(position!.dx, closeTo(2200, 0.001)); + expect(position.dy, closeTo(150, 0.001)); + }); + + test('rejects a position no display shows any more', () { + const props = WindowProps( + width: 800, + height: 600, + left: 2200, + top: 100, + scale: 1, + ); + + expect( + restoredWindowPosition( + props, + displays: const [primary], + currentScale: 1, + ), + isNull, + ); + }); + + test('reads a position saved without a scale at the current one', () { + const props = WindowProps(width: 800, height: 600, left: 100, top: 50); + + expect( + restoredWindowPosition( + props, + displays: const [primary], + currentScale: 1.25, + ), + const Offset(100, 50), + ); + }); + + test('keeps logical coordinates where the platform has them', () { + const props = WindowProps( + width: 800, + height: 600, + left: 1500, + top: 100, + scale: 2, + ); + + expect( + restoredWindowPosition(props, displays: const [primary, secondary]), + const Offset(1500, 100), + ); + }); + }); + test('maximized geometry is not captured', () async { isMaximized = true; @@ -213,4 +297,37 @@ void main() { isMinimized = true; expect(await Window().captureNormalGeometry(const WindowProps()), isNull); }); + + test('the init failure window closes and quits through onExit', () async { + var exits = 0; + + await Window().showInitFailure(onExit: () async => exits++); + + MethodCall callOf(String method) => + methodCalls.firstWhere((call) => call.method == method); + expect(callOf('setPreventClose').arguments, {'value': true}); + expect( + (callOf('setTitleBarStyle').arguments as Map)['style'], + 'normal', + reason: 'the error screen draws no window header of its own', + ); + + for (final event in ['close', 'should-terminate']) { + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + _windowChannel.name, + _windowChannel.codec.encodeMethodCall( + MethodCall('onEvent', {'name': event}), + ), + null, + ); + } + expect( + exits, + 2, + reason: + 'the macOS runner cancels every quit and closing its last window ' + 'leaves the process running, so both have to end in onExit', + ); + }); } diff --git a/test/core/desktop/fakes.dart b/test/core/desktop/fakes.dart index 82a23328..14cd0bdf 100644 --- a/test/core/desktop/fakes.dart +++ b/test/core/desktop/fakes.dart @@ -127,7 +127,7 @@ final class FakeDesktopCoreTransport implements DesktopCoreTransport { } @override - Future send(String message) async { + void send(List frame) { if (state == DesktopTransportState.closed) { throw StateError('IPC transport is closed'); } @@ -135,7 +135,7 @@ final class FakeDesktopCoreTransport implements DesktopCoreTransport { if (error != null) { Error.throwWithStackTrace(error, StackTrace.current); } - sentMessages.add(message); + sentMessages.add(utf8.decode(frame)); } @override diff --git a/test/core/desktop/helper_client_test.dart b/test/core/desktop/helper_client_test.dart index db09158d..403df703 100644 --- a/test/core/desktop/helper_client_test.dart +++ b/test/core/desktop/helper_client_test.dart @@ -16,6 +16,13 @@ const _coreSha256 = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; void main() { + test('reads the port from the key the Windows service publishes it in', () { + final peer = File('services/helper/src/service/peer.rs').readAsStringSync(); + + expect(peer, contains('const PORT_KEY: &str = r"$helperPortKey";')); + expect(peer, contains('const PORT_VALUE: &str = "$helperPortValue";')); + }); + test( 'start returns a Helper lease identity with matching session and PID', () async { diff --git a/test/core/desktop/ipc_native_test.dart b/test/core/desktop/ipc_native_test.dart new file mode 100644 index 00000000..0eb9dddc --- /dev/null +++ b/test/core/desktop/ipc_native_test.dart @@ -0,0 +1,100 @@ +@TestOn('!windows') +library; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:fl_clash/core/desktop/transport.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../plugins/code_forge/support.dart' show initEditorNative; + +const _wait = Duration(seconds: 5); + +Uint8List _frame(String payload) { + final bytes = utf8.encode(payload); + final header = ByteData(4)..setUint32(0, bytes.length, Endian.little); + return Uint8List.fromList([...header.buffer.asUint8List(), ...bytes]); +} + +void main() { + late Directory directory; + late String address; + late IpcCoreTransport transport; + + setUpAll(initEditorNative); + + setUp(() async { + directory = await Directory.systemTemp.createTemp('ipc'); + address = '${directory.path}/core.sock'; + transport = IpcCoreTransport(address: address); + }); + + tearDown(() async { + await transport.close(); + await directory.delete(recursive: true); + }); + + Future connect() { + return Socket.connect( + InternetAddress(address, type: InternetAddressType.unix), + 0, + ); + } + + test('a socket peer exchanges frames with the transport', () async { + await transport.open(); + expect( + FileSystemEntity.typeSync(address), + FileSystemEntityType.unixDomainSock, + ); + + final connected = transport.waitUntilConnected(_wait); + final peer = await connect(); + final received = StreamIterator(peer); + expect((await connected).generation, 1); + + final incoming = transport.frames.first.timeout(_wait); + peer.add(_frame('ping')); + expect(utf8.decode(await incoming), 'ping'); + + transport.send(utf8.encode('pong')); + expect(await received.moveNext().timeout(_wait), isTrue); + expect(received.current, _frame('pong')); + + final disconnected = transport.events + .where((event) => event is TransportDisconnected) + .cast() + .first + .timeout(_wait); + await received.cancel(); + peer.destroy(); + expect((await disconnected).generation, 1); + expect(transport.state, DesktopTransportState.ready); + + await transport.close(); + expect(FileSystemEntity.typeSync(address), FileSystemEntityType.notFound); + }); + + test('sending without a peer is refused', () async { + await transport.open(); + + expect( + () => transport.send(utf8.encode('early')), + throwsA(contains('not connected')), + ); + }); + + test('an address that cannot be bound fails the transport', () async { + final unbindable = IpcCoreTransport( + address: '${directory.path}/missing/core.sock', + ); + + await expectLater(unbindable.open(), throwsA(anything)); + + expect(unbindable.state, DesktopTransportState.failed); + await unbindable.close(); + }); +} diff --git a/test/core/desktop/transport_test.dart b/test/core/desktop/transport_test.dart index 46ead885..f359c5a2 100644 --- a/test/core/desktop/transport_test.dart +++ b/test/core/desktop/transport_test.dart @@ -4,42 +4,91 @@ import 'dart:typed_data'; import 'package:fl_clash/core/desktop/transport.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:rust_api/rust_api.dart'; import 'fakes.dart'; -Uint8List _frame(int type, [List payload = const []]) { - return Uint8List.fromList([type, ...payload]); -} +final class _FakeIpcServer implements IpcServer { + final StreamController _events = StreamController(); + final List> sent = []; + Object? sendError; + int closeCount = 0; -Uint8List _processIdPayload(int processId) { - return Uint8List(4) - ..buffer.asByteData().setUint32(0, processId, Endian.little); + void _add( + IpcEventKind kind, { + int? pid, + List payload = const [], + String? error, + }) { + _events.add( + IpcEvent( + kind: kind, + pid: pid, + payload: Uint8List.fromList(payload), + error: error, + ), + ); + } + + void connect([int? pid]) => _add(IpcEventKind.connected, pid: pid); + + void message(List payload) { + _add(IpcEventKind.message, payload: payload); + } + + void disconnect([String? error]) { + _add(IpcEventKind.disconnected, error: error); + } + + void fail(String error) => _add(IpcEventKind.failed, error: error); + + void end() => unawaited(_events.close()); + + @override + Stream events() => _events.stream; + + @override + void send({required List message}) { + final error = sendError; + if (error != null) { + Error.throwWithStackTrace(error, StackTrace.current); + } + sent.add(message); + } + + @override + Future close() async { + closeCount++; + end(); + } + + @override + void dispose() {} + + @override + bool get isDisposed => false; } void main() { - group('IPCCoreTransport typed events', () { - late StreamController rawEvents; - late List> sentMessages; - late int stopCount; - late IPCCoreTransport transport; + group('IpcCoreTransport', () { + late _FakeIpcServer server; + late List boundAddresses; + late IpcCoreTransport transport; setUp(() { - rawEvents = StreamController(); - sentMessages = []; - stopCount = 0; - transport = IPCCoreTransport( + server = _FakeIpcServer(); + boundAddresses = []; + transport = IpcCoreTransport( address: 'test-address', - startServer: (_) => rawEvents.stream, - sendMessage: (data) async => sentMessages.add(data), - stopServer: () async => stopCount++, + bind: (address) async { + boundAddresses.add(address); + return server; + }, ); }); tearDown(() async { await transport.close(); - if (!rawEvents.isClosed) { - await rawEvents.close(); - } }); test( @@ -47,99 +96,72 @@ void main() { () async { final events = []; final subscription = transport.events.listen(events.add); - final open = transport.open(); - rawEvents.add(_frame(0x00)); - await open; + await transport.open(); + expect(boundAddresses, ['test-address']); + expect(transport.state, DesktopTransportState.ready); final frame = transport.frames.first; - rawEvents.add(_frame(0x01, _processIdPayload(1234))); - rawEvents.add(_frame(0x03, utf8.encode('payload'))); + server.connect(1234); + server.message(utf8.encode('payload')); expect(await frame, utf8.encode('payload')); - rawEvents.add(_frame(0x02)); + expect(transport.state, DesktopTransportState.connected); + server.disconnect(); await pumpEventQueue(); expect(events.whereType(), hasLength(1)); expect(events.whereType().single.pid, 1234); expect(events.whereType().single.generation, 1); - expect(events.whereType().single.generation, 1); + final disconnected = events.whereType().single; + expect(disconnected.generation, 1); + expect(disconnected.error, isNull); expect(transport.state, DesktopTransportState.ready); await subscription.cancel(); }, ); - test('waitUntilConnected waits for a typed connected event', () async { - final open = transport.open(); - rawEvents.add(_frame(0x00)); - await open; + test('opening twice binds once', () async { + await Future.wait([transport.open(), transport.open()]); + await transport.open(); + + expect(boundAddresses, hasLength(1)); + }); + + test('waitUntilConnected waits for a connected event', () async { + await transport.open(); final connected = transport.waitUntilConnected( const Duration(seconds: 1), ); - rawEvents.add(_frame(0x01, _processIdPayload(4321))); + server.connect(4321); expect((await connected).pid, 4321); expect(transport.state, DesktopTransportState.connected); }); - test('forwards outgoing messages after ready', () async { - final open = transport.open(); - rawEvents.add(_frame(0x00)); - await open; + test('hands outgoing frames to the server', () async { + await transport.open(); - await transport.send('hello'); + transport.send(utf8.encode('hello')); - expect(sentMessages, [utf8.encode('hello')]); + expect(server.sent, [utf8.encode('hello')]); }); - test('emits startup failure and fails open', () async { - final failures = transport.events - .where((event) => event is TransportFailed) - .cast() - .first; - final open = transport.open(); - rawEvents.add(_frame(0x04, utf8.encode('bind failed'))); + test('refuses to send before it is open and after it is closed', () async { + expect(() => transport.send(const [1]), throwsStateError); - await expectLater(open, throwsA(isA())); - expect((await failures).error.toString(), contains('bind failed')); - expect(transport.state, DesktopTransportState.failed); - }); + await transport.open(); + await transport.close(); - test('fails when the Rust event stream closes before ready', () async { - final open = transport.open(); - await rawEvents.close(); - - await expectLater(open, throwsA(isA())); - expect(transport.state, DesktopTransportState.failed); - }); - - test('rejects an invalid connected process ID frame', () async { - final open = transport.open(); - rawEvents.add(_frame(0x00)); - await open; - - final failure = transport.events - .where((event) => event is TransportFailed) - .cast() - .first; - rawEvents.add(_frame(0x01, [0x01])); - - expect((await failure).error.toString(), contains('Invalid IPC')); - expect(transport.state, DesktopTransportState.failed); + expect(() => transport.send(const [1]), throwsStateError); + expect(server.sent, isEmpty); }); test('propagates send failures', () async { - final failingTransport = IPCCoreTransport( - address: 'test-address', - startServer: (_) => rawEvents.stream, - sendMessage: (_) async => throw StateError('send failed'), - stopServer: () async => stopCount++, - ); - final open = failingTransport.open(); - rawEvents.add(_frame(0x00)); - await open; + server.sendError = StateError('send failed'); + await transport.open(); - await expectLater( - failingTransport.send('hello'), + expect( + () => transport.send(utf8.encode('hello')), throwsA( isA().having( (error) => error.message, @@ -148,23 +170,88 @@ void main() { ), ), ); + }); + + test('a failed bind fails open and the transport', () async { + final failingTransport = IpcCoreTransport( + address: 'test-address', + bind: (_) async => throw StateError('bind failed'), + ); + final failure = failingTransport.events + .where((event) => event is TransportFailed) + .cast() + .first; + + await expectLater(failingTransport.open(), throwsStateError); + + expect((await failure).error.toString(), contains('bind failed')); + expect(failingTransport.state, DesktopTransportState.failed); + await expectLater(failingTransport.open(), throwsStateError); + await expectLater( + failingTransport.waitUntilConnected(const Duration(seconds: 1)), + throwsStateError, + ); await failingTransport.close(); }); - test('assigns a fresh generation after reconnecting', () async { - final open = transport.open(); - rawEvents.add(_frame(0x00)); - await open; + test('a server failure is terminal for the transport', () async { + await transport.open(); + server.connect(); + await pumpEventQueue(); + final events = []; + final subscription = transport.events.listen(events.add); + server.fail('accept error'); + server.connect(); + server.message(utf8.encode('late')); + await pumpEventQueue(); + + expect(transport.state, DesktopTransportState.failed); + final failure = events.single as TransportFailed; + expect(failure.error.toString(), contains('accept error')); + await expectLater(transport.open(), throwsStateError); + await subscription.cancel(); + }); + + test('fails when the server stops reporting events', () async { + await transport.open(); + final failure = transport.events + .where((event) => event is TransportFailed) + .cast() + .first; + + server.end(); + + expect((await failure).error.toString(), contains('stopped')); + expect(transport.state, DesktopTransportState.failed); + }); + + test('a disconnect carries the error that ended the connection', () async { + await transport.open(); + final disconnected = transport.events + .where((event) => event is TransportDisconnected) + .cast() + .first; + + server.connect(); + server.disconnect('frame exceeds the limit'); + + expect((await disconnected).error.toString(), contains('exceeds')); + expect(transport.state, DesktopTransportState.ready); + }); + + test('assigns a fresh generation after reconnecting', () async { + await transport.open(); final connections = []; final subscription = transport.events.listen((event) { if (event is TransportConnected) { connections.add(event); } }); - rawEvents.add(_frame(0x01)); - rawEvents.add(_frame(0x02)); - rawEvents.add(_frame(0x01, _processIdPayload(4321))); + + server.connect(); + server.disconnect(); + server.connect(4321); await pumpEventQueue(); expect(connections.map((event) => event.generation), [1, 2]); @@ -172,78 +259,47 @@ void main() { await subscription.cancel(); }); - test('a data frame hands out a view rather than a copy', () async { - final open = transport.open(); - rawEvents.add(_frame(0x00)); - await open; + test('close is idempotent', () async { + await transport.open(); - final frame = transport.frames.first; - final source = _frame(0x03, utf8.encode('payload')); - rawEvents.add(source); + await transport.close(); + await transport.close(); - final payload = await frame; - expect(payload, utf8.encode('payload')); - expect(payload.offsetInBytes, 1); - source[1] = 0x5a; - expect(payload[0], 0x5a); + expect(server.closeCount, 1); + expect(transport.state, DesktopTransportState.closed); + await expectLater(transport.open(), throwsStateError); }); - test( - 'a recovered transport stops answering with the old failure', - () async { - final open = transport.open(); - rawEvents.add(_frame(0x00)); - await open; + test('closing while binding closes the server that arrives', () async { + final bound = Completer(); + final slowTransport = IpcCoreTransport( + address: 'test-address', + bind: (_) => bound.future, + ); + final open = slowTransport.open(); + final rejected = expectLater(open, throwsStateError); - rawEvents.add(_frame(0x04, utf8.encode('boom'))); - await pumpEventQueue(); - expect(transport.state, DesktopTransportState.failed); - await expectLater( - transport.waitUntilConnected(const Duration(seconds: 1)), - throwsA(isA()), - ); + await slowTransport.close(); + bound.complete(server); - rawEvents.add(_frame(0x00)); - rawEvents.add(_frame(0x01, _processIdPayload(4321))); - await pumpEventQueue(); - - final connected = await transport.waitUntilConnected( - const Duration(seconds: 1), - ); - expect(connected.pid, 4321); - }, - ); - - test('close is idempotent', () async { - final open = transport.open(); - rawEvents.add(_frame(0x00)); - await open; - - await transport.close(); - await transport.close(); - - expect(stopCount, 1); - expect(transport.state, DesktopTransportState.closed); + await rejected; + expect(server.closeCount, 1); + expect(slowTransport.state, DesktopTransportState.closed); }); }); test( 'transport binding forwards events after replacing its transport', () async { - final firstEvents = StreamController(); - final secondEvents = StreamController(); - var firstStopCount = 0; - final first = IPCCoreTransport( + final firstServer = _FakeIpcServer(); + final secondServer = _FakeIpcServer(); + final first = IpcCoreTransport( address: 'first', - startServer: (_) => firstEvents.stream, - sendMessage: (_) async {}, - stopServer: () async => firstStopCount++, + bind: (_) async => firstServer, ); - final second = IPCCoreTransport( + final second = IpcCoreTransport( address: 'second', - startServer: (_) => secondEvents.stream, - sendMessage: (_) async {}, - stopServer: () async {}, + bind: (_) async => secondServer, ); final binding = DesktopCoreTransportBinding(first); final connectedEvents = []; @@ -253,24 +309,21 @@ void main() { } }); - final firstOpen = binding.open(); - firstEvents.add(_frame(0x00)); - await firstOpen; + await binding.open(); await binding.replace(second); - final secondOpen = binding.open(); - secondEvents.add(_frame(0x00)); - await secondOpen; - secondEvents.add(_frame(0x01, _processIdPayload(5678))); + await binding.open(); + secondServer.connect(5678); await pumpEventQueue(); + binding.send(utf8.encode('hello')); - expect(firstStopCount, 1); + expect(firstServer.closeCount, 1); + expect(firstServer.sent, isEmpty); + expect(secondServer.sent, [utf8.encode('hello')]); expect(binding.address, 'second'); expect(connectedEvents.single.pid, 5678); await subscription.cancel(); await binding.close(); - await firstEvents.close(); - await secondEvents.close(); }, ); diff --git a/test/lint/platform_layering_test.dart b/test/lint/platform_layering_test.dart index 2aa893e0..bf4ea420 100644 --- a/test/lint/platform_layering_test.dart +++ b/test/lint/platform_layering_test.dart @@ -15,7 +15,6 @@ const _platformModules = [ 'lib/common/window.dart', 'lib/common/launch.dart', 'lib/common/system_dns.dart', - 'lib/common/permission.dart', ]; final _platformImport = RegExp( diff --git a/test/manager/window_manager_test.dart b/test/manager/window_manager_test.dart index 8af6251c..170966c0 100644 --- a/test/manager/window_manager_test.dart +++ b/test/manager/window_manager_test.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:fl_clash/common/app_localizations.dart'; import 'package:fl_clash/common/app_ports.dart'; @@ -142,11 +143,19 @@ void main() { container.dispose(); }); - Future pumpWindowManager(WidgetTester tester) async { + Future pumpWindowManager( + WidgetTester tester, { + Stream? terminateSignals, + }) async { await tester.pumpWidget( UncontrolledProviderScope( container: container, - child: const MaterialApp(home: WindowManager(child: SizedBox.shrink())), + child: MaterialApp( + home: WindowManager( + terminateSignals: terminateSignals, + child: const SizedBox.shrink(), + ), + ), ), ); await tester.pump(); @@ -186,6 +195,22 @@ void main() { expect(_RecordingSystemAction.calls, ['exit']); }); + testWidgets('a terminate signal exits like a terminate request', ( + tester, + ) async { + final signals = StreamController(); + addTearDown(signals.close); + await pumpWindowManager(tester, terminateSignals: signals.stream); + + signals.add(ProcessSignal.sigterm); + await tester.pumpAndSettle(); + + expect(_RecordingSystemAction.calls, ['exit']); + + await tester.pumpWidget(const SizedBox.shrink()); + expect(signals.hasListener, isFalse); + }); + testWidgets('moving the window records its new position', (tester) async { final listener = await pumpWindowManager(tester); window.bounds = const Rect.fromLTWH(120, 64, 1000, 800); diff --git a/test/tool/packaging_config_test.dart b/test/tool/packaging_config_test.dart index f6ad8efb..aea4f19b 100644 --- a/test/tool/packaging_config_test.dart +++ b/test/tool/packaging_config_test.dart @@ -30,6 +30,24 @@ void main() { } }); + test('every Linux package names the GTK application id as its window ' + 'class', () { + final applicationId = RegExp( + r'set\(APPLICATION_ID "([^"]+)"\)', + ).firstMatch(File('linux/CMakeLists.txt').readAsStringSync())!.group(1); + + for (final format in ['deb', 'rpm', 'appimage']) { + final config = + loadYaml( + File( + 'linux/packaging/$format/make_config.yaml', + ).readAsStringSync(), + ) + as YamlMap; + expect(config['startup_wm_class'], applicationId, reason: format); + } + }); + test('rpm keeps the Core bytes the Helper was built against', () { final config = loadYaml( diff --git a/windows/packaging/exe/inno_setup.iss b/windows/packaging/exe/inno_setup.iss index 8740ae00..44a72124 100644 --- a/windows/packaging/exe/inno_setup.iss +++ b/windows/packaging/exe/inno_setup.iss @@ -45,6 +45,47 @@ begin end; end; +procedure ClearStaleProxy; +var + AppPath: String; + ResultCode: Integer; +begin + AppPath := ExpandConstant('{app}\\{{EXECUTABLE_NAME}}'); + if FileExists(AppPath) then + begin + Exec(AppPath, '--clear-stale-proxy', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + end; +end; + +function PointsIntoApp(Value: String): Boolean; +begin + Result := Pos(Lowercase(AddBackslash(ExpandConstant('{app}'))), Lowercase(Value)) > 0; +end; + +// The value name is appName and the schemes are protocolSchemes, both in +// lib/common. +procedure RemoveUserRegistrations; +var + Schemes: TArrayOfString; + Value: String; + i: Integer; +begin + if RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Run', '{{APP_NAME}}', Value) and PointsIntoApp(Value) then + begin + RegDeleteValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Run', '{{APP_NAME}}'); + RegDeleteValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run', '{{APP_NAME}}'); + end; + + Schemes := ['clash', 'clashmeta', 'flclash']; + for i := 0 to GetArrayLength(Schemes)-1 do + begin + if RegQueryStringValue(HKCU, 'Software\Classes\' + Schemes[i] + '\shell\open\command', '', Value) and PointsIntoApp(Value) then + begin + RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\' + Schemes[i]); + end; + end; +end; + function PrepareToInstall(var NeedsRestart: Boolean): String; begin UnregisterHelperService; @@ -52,13 +93,30 @@ begin Result := ''; end; +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + begin + ClearStaleProxy; + end; +end; + function InitializeUninstall(): Boolean; begin UnregisterHelperService; KillProcesses; + ClearStaleProxy; Result := True; end; +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usPostUninstall then + begin + RemoveUserRegistrations; + end; +end; + [Languages] {% for locale in LOCALES %} {% if locale.lang == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %} diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index deb46e0d..a2ab86e9 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -3,13 +3,27 @@ #include #include +#include #include +#include + #include "flutter_window.h" #include "utils.h" +// The installer and the uninstaller run this after killing the app, whose exit +// path is what normally turns the system proxy off. +constexpr char kClearStaleProxyArgument[] = "--clear-stale-proxy"; + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + std::vector command_line_arguments = + GetCommandLineArguments(); + if (std::find(command_line_arguments.begin(), command_line_arguments.end(), + kClearStaleProxyArgument) != command_line_arguments.end()) { + return ProxyPluginClearStaleProxy() ? EXIT_SUCCESS : EXIT_FAILURE; + } + if (HWND running = WindowPluginFindRunningWindow()) { SendAppLink(running); WindowPluginActivateWindow(running); @@ -28,9 +42,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, flutter::DartProject project(L"data"); - std::vector command_line_arguments = - GetCommandLineArguments(); - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project);