fix(desktop): fix the system proxy, tray and Helper on Windows and Linux

Changelog: Windows and Linux desktop fixes
This commit is contained in:
chen08209
2026-10-08 16:09:04 +08:00
parent 2fa2f7f816
commit 94a25f4827
64 changed files with 3947 additions and 1711 deletions
+123
View File
@@ -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<ProcessResult> Function(
String executable,
List<String> arguments,
String input,
);
typedef PasswordPrompt = Future<String?> 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<bool> elevate(List<String> 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<bool> _sudo(List<String> 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<ProcessResult> _runWithInput(
String executable,
List<String> 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<String?> _askPassword() async {
await windowPort?.show();
return dialogs.showPasswordInput(
title: currentAppLocalizations.sudoPasswordTitle,
);
}
}
final linuxElevation = LinuxElevation();
+111
View File
@@ -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<String> _path;
Future<Map<String, Object>>? _values;
Future<bool> _lastWrite = Future.value(true);
Future<Map<String, Object>> _read() {
return _values ??= _load().catchError((Object error, StackTrace stack) {
_values = null;
Error.throwWithStackTrace(error, stack);
});
}
Future<Map<String, Object>> _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<String, Object>.from(data) : {};
}
Future<bool> _update(void Function(Map<String, Object> values) change) async {
final values = await _read();
change(values);
final content = json.encode(values);
return _lastWrite = _lastWrite.then((_) => _write(content));
}
Future<bool> _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<Map<String, Object>> getAll() {
return getAllWithParameters(
GetAllParameters(filter: PreferencesFilter(prefix: _defaultPrefix)),
);
}
@override
Future<Map<String, Object>> 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<bool> setValue(String valueType, String key, Object value) {
return _update((values) => values[key] = value);
}
@override
Future<bool> remove(String key) {
return _update((values) => values.remove(key));
}
@override
Future<bool> clear() {
return clearWithParameters(
ClearParameters(filter: PreferencesFilter(prefix: _defaultPrefix)),
);
}
@override
Future<bool> 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);
}
}
+18 -53
View File
@@ -106,7 +106,8 @@ class System {
@visibleForTesting @visibleForTesting
static String linuxElevationShell(String corePath) { static String linuxElevationShell(String corePath) {
final path = _shellEscape(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'; 'chmod 4750 $path';
} }
@@ -248,28 +249,12 @@ class System {
} }
return AuthorizeCode.success; return AuthorizeCode.success;
} else if (system.isLinux) { } else if (system.isLinux) {
final ProcessResult result; final elevated = await linuxElevation.elevate([
try { '/bin/sh',
result = await runProcess('pkexec', [ '-c',
'/bin/sh', linuxElevationShell(appPath.corePath),
'-c', ]);
linuxElevationShell(appPath.corePath), return elevated ? AuthorizeCode.success : AuthorizeCode.error;
]);
} 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;
} }
return AuthorizeCode.error; return AuthorizeCode.error;
} }
@@ -289,6 +274,8 @@ final system = System();
class Windows { class Windows {
static Windows? _instance; static Windows? _instance;
static const _swHide = 0;
static const _swShowNormal = 1;
late DynamicLibrary _shell32; late DynamicLibrary _shell32;
Windows._internal() { Windows._internal() {
@@ -300,7 +287,7 @@ class Windows {
return _instance!; return _instance!;
} }
bool runas(String command, String arguments) { bool runas(String command, String arguments, {bool showWindow = true}) {
final commandPtr = command.toNativeUtf16(); final commandPtr = command.toNativeUtf16();
final argumentsPtr = arguments.toNativeUtf16(); final argumentsPtr = arguments.toNativeUtf16();
final operationPtr = 'runas'.toNativeUtf16(); final operationPtr = 'runas'.toNativeUtf16();
@@ -331,7 +318,7 @@ class Windows {
commandPtr, commandPtr,
argumentsPtr, argumentsPtr,
nullptr, nullptr,
1, showWindow ? _swShowNormal : _swHide,
); );
calloc.free(commandPtr); calloc.free(commandPtr);
@@ -356,7 +343,8 @@ class Windows {
Future<AuthorizeCode> registerService() { Future<AuthorizeCode> registerService() {
return registerHelperService( 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 { class Linux {
static Linux? _instance; static Linux? _instance;
@visibleForTesting
ProcessRunner runProcess = Process.run;
Linux._internal(); Linux._internal();
factory Linux() { factory Linux() {
@@ -447,31 +432,11 @@ class Linux {
return registerHelperService(installService); return registerHelperService(installService);
} }
/// pkexec raises the system polkit prompt and names the requesting user to /// pkexec and sudo both name the requesting user to the installer, which is
/// the installer, which is where the unit takes the account it grants the /// where the unit takes the account it grants the Helper socket to.
/// Helper socket to.
@visibleForTesting @visibleForTesting
Future<bool> installService() async { Future<bool> installService() {
try { return linuxElevation.elevate([appPath.helperPath, 'install']);
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;
} }
} }
+3 -1
View File
@@ -211,7 +211,9 @@ class AppTray implements TrayPort {
if (delay == null) { if (delay == null) {
return null; return null;
} }
return delay > 0 ? '$delay' : currentAppLocalizations.timeout; return delay > 0
? '$delay'
: delayFailureText(delay, currentAppLocalizations);
} }
List<TrayMenuItem> _buildGroupMenu({ List<TrayMenuItem> _buildGroupMenu({
+69 -38
View File
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/models/config.dart';
import 'package:flutter/foundation.dart';
import 'package:material_ui/material_ui.dart'; import 'package:material_ui/material_ui.dart';
import 'package:screen_retriever/screen_retriever.dart'; import 'package:screen_retriever/screen_retriever.dart';
import 'package:window/window.dart'; import 'package:window/window.dart';
@@ -31,11 +32,6 @@ class Window implements WindowPort {
} }
Future<void> init(int version, WindowProps props) async { Future<void> 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 (!safeModeBuild) {
if (system.isWindows) { if (system.isWindows) {
for (final scheme in protocolSchemes) { for (final scheme in protocolSchemes) {
@@ -61,39 +57,26 @@ class Window implements WindowPort {
} }
Future<void> _windowPosition(WindowProps props) async { Future<void> _windowPosition(WindowProps props) async {
if (_supportsPosition) { if (!_supportsPosition) {
final left = props.left; return;
final top = props.top; }
if (left == null || top == null) { final position = props.left == null || props.top == null
await desktopWindow.setAlignment(Alignment.center); ? null
} else { : restoredWindowPosition(
final size = props.size; props,
final right = left + size.width; displays: await screenRetriever.getAllDisplays(),
final bottom = top + size.height; currentScale: system.isWindows ? _windowScale : null,
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,
); );
return displayBounds.contains(Offset(left, top)) || if (position == null) {
displayBounds.contains(Offset(right, bottom)); await desktopWindow.setAlignment(Alignment.center);
}); } else {
if (isPositionValid) { await desktopWindow.setPosition(position);
await desktopWindow.setPosition(Offset(left, top));
} else {
await desktopWindow.setAlignment(Alignment.center);
}
}
} }
} }
double get _windowScale =>
PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 1;
@override @override
Future<WindowProps?> captureNormalGeometry(WindowProps current) async { Future<WindowProps?> captureNormalGeometry(WindowProps current) async {
final states = await Future.wait<bool>([ final states = await Future.wait<bool>([
@@ -119,6 +102,9 @@ class Window implements WindowPort {
height: bounds.height, height: bounds.height,
left: hasValidPosition ? bounds.left : current.left, left: hasValidPosition ? bounds.left : current.left,
top: hasValidPosition ? bounds.top : current.top, 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 /// 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. /// a failure before that point would leave the error screen with no window.
Future<void> showInitFailure() async { Future<void> showInitFailure({required AsyncCallback onExit}) async {
desktopWindow.addListener(_InitFailureWindowListener(onExit));
try { try {
await desktopWindow.ensureInitialized(); await desktopWindow.ensureInitialized();
await desktopWindow.setPreventClose(true);
await desktopWindow.setTitleBarStyle(TitleBarStyle.normal);
if (await desktopWindow.isVisible()) { if (await desktopWindow.isVisible()) {
return; return;
} }
@@ -200,9 +189,7 @@ class Window implements WindowPort {
} }
Future<bool> _isWindowVisible() async { Future<bool> _isWindowVisible() async {
final value = await desktopWindow.isVisible(); return desktopWindow.isVisible();
commonPrint.log('window visible check: $value');
return value;
} }
@override @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<Display> 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 /// Serializes visibility requests so a burst of hotkey toggles lands in
/// order, and holds back the Dock-hiding activation policy switch while a /// order, and holds back the Dock-hiding activation policy switch while a
/// preceding regular switch settles: flipping regular → accessory → regular /// preceding regular switch settles: flipping regular → accessory → regular
+23 -3
View File
@@ -8,6 +8,7 @@ import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/common/print.dart'; import 'package:fl_clash/common/print.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:win32_registry/win32_registry.dart';
import 'core_manifest.dart'; import 'core_manifest.dart';
import 'launcher.dart'; import 'launcher.dart';
@@ -54,7 +55,7 @@ final class HelperClient {
final Dio _dio; final Dio _dio;
final String Function() _expectedHelperPath; final String Function() _expectedHelperPath;
final Future<String> Function() _readCoreSha256; final Future<String> Function() _readCoreSha256;
final String baseUrl; final String Function() _baseUrl;
String? _coreSha256Cache; String? _coreSha256Cache;
HelperClient({ HelperClient({
@@ -63,7 +64,7 @@ final class HelperClient {
Future<String> Function()? readCoreSha256, Future<String> Function()? readCoreSha256,
String? baseUrl, String? baseUrl,
}) : _dio = dio ?? _createDio(), }) : _dio = dio ?? _createDio(),
baseUrl = baseUrl ?? _defaultBaseUrl(), _baseUrl = baseUrl == null ? _defaultBaseUrl : (() => baseUrl),
_expectedHelperPath = expectedHelperPath ?? _defaultHelperPath, _expectedHelperPath = expectedHelperPath ?? _defaultHelperPath,
_readCoreSha256 = readCoreSha256 ?? _readBundledCoreSha256; _readCoreSha256 = readCoreSha256 ?? _readBundledCoreSha256;
@@ -111,10 +112,29 @@ final class HelperClient {
); );
} }
String get baseUrl => _baseUrl();
static String _defaultBaseUrl() { static String _defaultBaseUrl() {
return Platform.isLinux return Platform.isLinux
? 'http://$appHelperService' ? '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<String> _readBundledCoreSha256() async { static Future<String> _readBundledCoreSha256() async {
+2 -1
View File
@@ -710,11 +710,12 @@ final class DesktopCoreLifecycle implements DesktopCoreLifecycleController {
return; return;
} }
switch (event) { switch (event) {
case TransportDisconnected(:final generation) case TransportDisconnected(:final generation, :final error)
when generation == running.session.connectionGeneration: when generation == running.session.connectionGeneration:
_handleUnexpectedDisconnect( _handleUnexpectedDisconnect(
running.session, running.session,
code: 'unexpected_disconnect', code: 'unexpected_disconnect',
cause: error,
); );
case TransportFailed(:final error, :final stackTrace): case TransportFailed(:final error, :final stackTrace):
_handleUnexpectedDisconnect( _handleUnexpectedDisconnect(
+10 -10
View File
@@ -18,6 +18,9 @@ const _maxExtensionGrace = Duration(seconds: 30);
const _connectTimeout = Duration(seconds: 10); const _connectTimeout = Duration(seconds: 10);
final _requestEncoder = JsonUtf8Encoder();
final _frameDecoder = utf8.decoder.fuse(json.decoder);
abstract interface class CoreRpcChannel { abstract interface class CoreRpcChannel {
Future<T?> invoke<T>({ Future<T?> invoke<T>({
required CoreMethod method, required CoreMethod method,
@@ -83,17 +86,14 @@ final class CoreRpcClient implements CoreRpcChannel {
if (completer.isCompleted) { if (completer.isCompleted) {
return await completer.future as T?; return await completer.future as T?;
} }
final sendTimeout = requestTimeout - stopwatch.elapsed; if (stopwatch.elapsed >= requestTimeout) {
if (sendTimeout <= Duration.zero) {
throw TimeoutException('Core method ${method.name} timed out'); throw TimeoutException('Core method ${method.name} timed out');
} }
await transport transport.send(
.send( _requestEncoder.convert(
json.encode( CoreMethodCall(id: id, method: method, arguments: arguments),
CoreMethodCall(id: id, method: method, arguments: arguments), ),
), );
)
.timeout(sendTimeout);
return await _awaitResponse( return await _awaitResponse(
method: method, method: method,
completer: completer, completer: completer,
@@ -170,7 +170,7 @@ final class CoreRpcClient implements CoreRpcChannel {
void _handleFrame(Uint8List frame) { void _handleFrame(Uint8List frame) {
try { try {
final decoded = json.decode(utf8.decode(frame)); final decoded = _frameDecoder.convert(frame);
if (decoded is! Map) { if (decoded is! Map) {
throw const FormatException('Core transport data is not an object'); throw const FormatException('Core transport data is not an object');
} }
+93 -153
View File
@@ -1,20 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:rust_api/rust_api.dart'; import 'package:rust_api/rust_api.dart';
const _typeReady = 0x00; typedef IpcServerBinder = Future<IpcServer> Function(String address);
const _typeConnected = 0x01;
const _typeDisconnected = 0x02;
const _typeData = 0x03;
const _typeError = 0x04;
typedef IpcServerStarter = Stream<Uint8List> Function(String address);
typedef IpcMessageSender = Future<void> Function(List<int> data);
typedef IpcServerStopper = Future<void> Function();
enum DesktopTransportState { idle, starting, ready, connected, failed, closed } enum DesktopTransportState { idle, starting, ready, connected, failed, closed }
@@ -35,8 +26,9 @@ final class TransportConnected extends DesktopTransportEvent {
final class TransportDisconnected extends DesktopTransportEvent { final class TransportDisconnected extends DesktopTransportEvent {
final int generation; final int generation;
final Object? error;
const TransportDisconnected(this.generation); const TransportDisconnected(this.generation, {this.error});
} }
final class TransportFailed extends DesktopTransportEvent { final class TransportFailed extends DesktopTransportEvent {
@@ -59,7 +51,7 @@ abstract interface class DesktopCoreTransport {
Future<TransportConnected> waitUntilConnected(Duration timeout); Future<TransportConnected> waitUntilConnected(Duration timeout);
Future<void> send(String message); void send(List<int> frame);
Future<void> close(); Future<void> close();
} }
@@ -90,6 +82,13 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
); );
} }
Future<void> _unbind() async {
await _eventSubscription?.cancel();
await _frameSubscription?.cancel();
_eventSubscription = null;
_frameSubscription = null;
}
@override @override
String get address => _transport.address; String get address => _transport.address;
@@ -107,8 +106,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
throw StateError('IPC transport binding is closed'); throw StateError('IPC transport binding is closed');
} }
final previous = _transport; final previous = _transport;
if (previous.state == DesktopTransportState.connected && if (previous.state == DesktopTransportState.connected) {
!_eventController.isClosed) {
_eventController.add( _eventController.add(
TransportFailed( TransportFailed(
StateError('Connected IPC transport was replaced'), StateError('Connected IPC transport was replaced'),
@@ -116,22 +114,12 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
), ),
); );
} }
await _eventSubscription?.cancel(); await _unbind();
await _frameSubscription?.cancel();
_eventSubscription = null;
_frameSubscription = null;
Object? closeError;
StackTrace? closeStackTrace;
try { try {
await previous.close(); await previous.close();
} catch (error, stackTrace) { } finally {
closeError = error; _transport = next;
closeStackTrace = stackTrace; _bind(next);
}
_transport = next;
_bind(next);
if (closeError != null) {
Error.throwWithStackTrace(closeError, closeStackTrace!);
} }
} }
@@ -144,7 +132,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
} }
@override @override
Future<void> send(String message) => _transport.send(message); void send(List<int> frame) => _transport.send(frame);
@override @override
Future<void> close() { Future<void> close() {
@@ -152,10 +140,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
} }
Future<void> _close() async { Future<void> _close() async {
await _eventSubscription?.cancel(); await _unbind();
await _frameSubscription?.cancel();
_eventSubscription = null;
_frameSubscription = null;
try { try {
await _transport.close(); await _transport.close();
} finally { } finally {
@@ -165,21 +150,18 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
} }
} }
final class IPCCoreTransport implements DesktopCoreTransport { final class IpcCoreTransport implements DesktopCoreTransport {
@override @override
final String address; final String address;
final Duration readyTimeout; final IpcServerBinder _bind;
final IpcServerStarter _startServer;
final IpcMessageSender _sendMessage;
final IpcServerStopper _stopServer;
final StreamController<DesktopTransportEvent> _eventController = final StreamController<DesktopTransportEvent> _eventController =
StreamController<DesktopTransportEvent>.broadcast(); StreamController<DesktopTransportEvent>.broadcast();
final StreamController<Uint8List> _frameController = final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast(); StreamController<Uint8List>.broadcast();
StreamSubscription<Uint8List>? _subscription; IpcServer? _server;
StreamSubscription<IpcEvent>? _subscription;
Future<void>? _openOperation; Future<void>? _openOperation;
Future<void>? _closeOperation; Future<void>? _closeOperation;
DesktopTransportState _state = DesktopTransportState.idle; DesktopTransportState _state = DesktopTransportState.idle;
@@ -187,22 +169,11 @@ final class IPCCoreTransport implements DesktopCoreTransport {
TransportFailed? _failure; TransportFailed? _failure;
int _connectionGeneration = 0; int _connectionGeneration = 0;
IPCCoreTransport({ IpcCoreTransport({required this.address, IpcServerBinder? bind})
required this.address, : _bind = bind ?? _bindIpcServer;
this.readyTimeout = const Duration(seconds: 10),
IpcServerStarter? startServer,
IpcMessageSender? sendMessage,
IpcServerStopper? stopServer,
}) : _startServer = startServer ?? _restartIpcServer,
_sendMessage = sendMessage ?? _sendIpcMessage,
_stopServer = stopServer ?? stopIpcServer;
static Stream<Uint8List> _restartIpcServer(String address) { static Future<IpcServer> _bindIpcServer(String address) {
return restartIpcServer(name: address); return IpcServer.bind(address: address);
}
static Future<void> _sendIpcMessage(List<int> data) {
return sendIpcMessage(data: data);
} }
@override @override
@@ -216,134 +187,103 @@ final class IPCCoreTransport implements DesktopCoreTransport {
@override @override
Future<void> open() { Future<void> open() {
if (_state == DesktopTransportState.ready ||
_state == DesktopTransportState.connected) {
return Future.value();
}
if (_state == DesktopTransportState.closed) { if (_state == DesktopTransportState.closed) {
return Future.error(StateError('IPC transport is 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(); return _openOperation ??= _open();
} }
Future<void> _open() async { Future<void> _open() async {
_state = DesktopTransportState.starting; _state = DesktopTransportState.starting;
final readiness = events.firstWhere(
(event) => event is TransportReady || event is TransportFailed,
);
try { try {
_subscription = _startServer(address).listen( final server = await _bind(address);
_handleFrame, if (_state == DesktopTransportState.closed) {
onError: _handleStreamError, await server.close();
onDone: _handleStreamDone, throw StateError('IPC transport is closed');
cancelOnError: false, }
_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) { } catch (error, stackTrace) {
if (_state != DesktopTransportState.failed && _fail(error, stackTrace);
_state != DesktopTransportState.closed) {
_fail(error, stackTrace);
}
await _subscription?.cancel();
_subscription = null;
rethrow; rethrow;
} }
} }
void _handleFrame(Uint8List data) { bool get _isTerminal =>
if (data.isEmpty || _state == DesktopTransportState.closed) { _state == DesktopTransportState.failed ||
_state == DesktopTransportState.closed;
void _handleEvent(IpcEvent event) {
if (_isTerminal) {
return; return;
} }
final type = data[0]; switch (event.kind) {
final payload = data.length > 1 case IpcEventKind.connected:
? Uint8List.sublistView(data, 1) _connect(event.pid);
: Uint8List(0); case IpcEventKind.message:
switch (type) { _frameController.add(event.payload);
case _typeReady: case IpcEventKind.disconnected:
commonPrint.log('IPC Ready'); _disconnect(event.error);
_failure = null; case IpcEventKind.failed:
_state = DesktopTransportState.ready; _fail(StateError('IPC error: ${event.error}'), StackTrace.current);
_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,
);
} }
} }
int? _decodeConnectedProcessId(Uint8List payload) { void _connect(int? pid) {
if (payload.isEmpty) { commonPrint.log('IPC Connected${pid == null ? '' : ': $pid'}');
return null; final connection = TransportConnected(
} pid: pid,
if (payload.length != Uint32List.bytesPerElement) { generation: ++_connectionGeneration,
return null; );
} _connection = connection;
final processId = ByteData.sublistView(payload).getUint32(0, Endian.little); _state = DesktopTransportState.connected;
return processId == 0 ? null : processId; _eventController.add(connection);
} }
void _disconnect() { void _disconnect(String? error) {
final connection = _connection; final connection = _connection;
if (connection == null) { if (connection == null) {
return; return;
} }
commonPrint.log('IPC Disconnected'); commonPrint.log(
'IPC Disconnected${error == null ? '' : ': $error'}',
logLevel: error == null ? LogLevel.info : LogLevel.warning,
);
_connection = null; _connection = null;
_state = DesktopTransportState.ready; _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) { void _handleDone() {
_fail(error, stackTrace);
}
void _handleStreamDone() {
if (_state == DesktopTransportState.closed) {
return;
}
_disconnect();
_fail(StateError('IPC server stopped unexpectedly'), StackTrace.current); _fail(StateError('IPC server stopped unexpectedly'), StackTrace.current);
} }
void _fail(Object error, StackTrace stackTrace) { void _fail(Object error, StackTrace stackTrace) {
if (_state == DesktopTransportState.failed || if (_isTerminal) {
_state == DesktopTransportState.closed) {
return; return;
} }
commonPrint.log('IPC error: $error', logLevel: LogLevel.debug); commonPrint.log('IPC error: $error', logLevel: LogLevel.debug);
final failure = TransportFailed(error, stackTrace);
_connection = null;
_failure = failure;
_state = DesktopTransportState.failed; _state = DesktopTransportState.failed;
_failure = TransportFailed(error, stackTrace); _eventController.add(failure);
_eventController.add(_failure!);
} }
@override @override
@@ -371,11 +311,15 @@ final class IPCCoreTransport implements DesktopCoreTransport {
} }
@override @override
Future<void> send(String message) { void send(List<int> frame) {
if (_state == DesktopTransportState.closed) { 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 @override
@@ -384,16 +328,12 @@ final class IPCCoreTransport implements DesktopCoreTransport {
} }
Future<void> _close() async { Future<void> _close() async {
if (_state == DesktopTransportState.closed) {
return;
}
_state = DesktopTransportState.closed; _state = DesktopTransportState.closed;
_connection = null; _connection = null;
try { try {
await _stopServer(); await _server?.close();
} finally { } finally {
await _subscription?.cancel(); await _subscription?.cancel();
_subscription = null;
await _eventController.close(); await _eventController.close();
await _frameController.close(); await _frameController.close();
} }
+11 -9
View File
@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/launch.dart'; import 'package:fl_clash/common/launch.dart';
@@ -16,7 +17,11 @@ const _windowGeometryDelay = Duration(milliseconds: 120);
class WindowManager extends ConsumerStatefulWidget { class WindowManager extends ConsumerStatefulWidget {
final Widget child; 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<ProcessSignal>? terminateSignals;
const WindowManager({super.key, this.terminateSignals, required this.child});
@override @override
ConsumerState<WindowManager> createState() => _WindowContainerState(); ConsumerState<WindowManager> createState() => _WindowContainerState();
@@ -25,6 +30,7 @@ class WindowManager extends ConsumerStatefulWidget {
class _WindowContainerState extends ConsumerState<WindowManager> class _WindowContainerState extends ConsumerState<WindowManager>
with WindowListener { with WindowListener {
Timer? _windowGeometryTimer; Timer? _windowGeometryTimer;
StreamSubscription<ProcessSignal>? _terminateSubscription;
int _windowGeometryRevision = 0; int _windowGeometryRevision = 0;
int _windowBlurRevision = 0; int _windowBlurRevision = 0;
@@ -52,6 +58,9 @@ class _WindowContainerState extends ConsumerState<WindowManager>
} }
}, fireImmediately: true); }, fireImmediately: true);
desktopWindow.addListener(this); desktopWindow.addListener(this);
_terminateSubscription = widget.terminateSignals?.listen(
(_) => unawaited(onWindowShouldTerminate()),
);
} }
Future<void> _applyWindowBlur(WindowBlurRequest request) async { Future<void> _applyWindowBlur(WindowBlurRequest request) async {
@@ -86,12 +95,6 @@ class _WindowContainerState extends ConsumerState<WindowManager>
super.onWindowClose(); super.onWindowClose();
} }
@override
void onWindowFocus() {
super.onWindowFocus();
commonPrint.log('focus');
}
/// Another launch, or a Dock reopen, asked for the window. /// Another launch, or a Dock reopen, asked for the window.
@override @override
void onWindowActivate() { void onWindowActivate() {
@@ -182,13 +185,11 @@ class _WindowContainerState extends ConsumerState<WindowManager>
void onWindowMinimize() async { void onWindowMinimize() async {
_invalidateWindowGeometryCapture(); _invalidateWindowGeometryCapture();
ref.read(storeActionProvider.notifier).savePreferencesDebounce(); ref.read(storeActionProvider.notifier).savePreferencesDebounce();
commonPrint.log('minimize');
super.onWindowMinimize(); super.onWindowMinimize();
} }
@override @override
void onWindowRestore() { void onWindowRestore() {
commonPrint.log('restore');
super.onWindowRestore(); super.onWindowRestore();
_scheduleWindowGeometryCapture(); _scheduleWindowGeometryCapture();
} }
@@ -196,6 +197,7 @@ class _WindowContainerState extends ConsumerState<WindowManager>
@override @override
void dispose() { void dispose() {
_invalidateWindowGeometryCapture(); _invalidateWindowGeometryCapture();
unawaited(_terminateSubscription?.cancel());
desktopWindow.removeListener(this); desktopWindow.removeListener(this);
super.dispose(); super.dispose();
} }
+1
View File
@@ -53,6 +53,7 @@ add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies. # System-level dependencies.
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
# Application build; see runner/CMakeLists.txt. # Application build; see runner/CMakeLists.txt.
add_subdirectory("runner") add_subdirectory("runner")
@@ -20,4 +20,6 @@ categories:
startup_notify: true startup_notify: true
startup_wm_class: com.follow.clash
include: [] include: []
+2
View File
@@ -39,3 +39,5 @@ categories:
- Network - Network
startup_notify: true startup_notify: true
startup_wm_class: com.follow.clash
+2
View File
@@ -46,3 +46,5 @@ generic_name: FlClash
group: Applications/Internet group: Applications/Internet
startup_notify: true startup_notify: true
startup_wm_class: com.follow.clash
+1
View File
@@ -22,5 +22,6 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here. # Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 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}") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+9
View File
@@ -1,6 +1,15 @@
#include "my_application.h" #include "my_application.h"
#ifdef GDK_WINDOWING_X11
#include <X11/Xlib.h>
#endif
int main(int argc, char** argv) { 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(); g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv); return g_application_run(G_APPLICATION(app), argc, argv);
} }
+18 -2
View File
@@ -55,6 +55,21 @@ static void my_application_activate(GApplication* application) {
gtk_window_set_default_size(window, 680, 580); 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(); g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 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 // Implements GApplication::after_emit. A relaunch reaches the primary as a
// command-line signal that the gtk plugin consumes first, so the window is // command-line signal whose activate returns early on the existing window, so
// raised here, while the launcher's activation token is still current. // 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) { static void my_application_after_emit(GApplication* application, GVariant* platform_data) {
MyApplication* self = MY_APPLICATION(application); MyApplication* self = MY_APPLICATION(application);
if (self->had_window_before_emit) { if (self->had_window_before_emit) {
+5 -1
View File
@@ -4,6 +4,7 @@ import 'proxy_platform_interface.dart';
import 'src/linux_proxy.dart'; import 'src/linux_proxy.dart';
import 'src/macos_proxy.dart'; import 'src/macos_proxy.dart';
import 'src/proxy_command.dart'; import 'src/proxy_command.dart';
import 'src/windows_proxy.dart';
export 'src/proxy_command.dart' show ProxyExecutableChecker, ProxyProcessRunner; export 'src/proxy_command.dart' show ProxyExecutableChecker, ProxyProcessRunner;
@@ -41,7 +42,10 @@ class Proxy {
desktop: Platform.environment['XDG_CURRENT_DESKTOP'], desktop: Platform.environment['XDG_CURRENT_DESKTOP'],
homeDir: Platform.environment['HOME'], homeDir: Platform.environment['HOME'],
), ),
'windows' => await ProxyPlatform.instance.startProxy(port, bypassDomain), 'windows' => await ProxyPlatform.instance.startProxy(
port,
windowsBypassList(bypassDomain),
),
String() => false, String() => false,
}; };
} }
+13 -1
View File
@@ -325,7 +325,7 @@ class LinuxProxyCommands {
'Proxy Settings', 'Proxy Settings',
'--key', '--key',
'${type.name}Proxy', '${type.name}Proxy',
'${type.name}://$proxyHost:$port', '${type == _ProxyType.socks ? 'socks' : 'http'}://$proxyHost:$port',
]), ]),
ProxyCommand(executable, [ ProxyCommand(executable, [
'--file', '--file',
@@ -336,6 +336,7 @@ class LinuxProxyCommands {
'ProxyType', 'ProxyType',
'1', '1',
]), ]),
_kdeReparseConfiguration,
]; ];
} }
@@ -353,9 +354,20 @@ class LinuxProxyCommands {
'ProxyType', 'ProxyType',
'0', '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<String> values) { static String _formatGSettingsStringList(List<String> values) {
final escaped = values.map((value) { final escaped = values.map((value) {
return value.replaceAll(r'\', r'\\').replaceAll("'", r"\'"); return value.replaceAll(r'\', r'\\').replaceAll("'", r"\'");
+24 -15
View File
@@ -18,9 +18,14 @@ class ProxyCommand {
final String executable; final String executable;
final List<String> args; final List<String> args;
final bool runInShell; final bool runInShell;
final bool optional;
ProxyCommand(this.executable, List<String> args, {this.runInShell = false}) ProxyCommand(
: args = List.unmodifiable(args); this.executable,
List<String> args, {
this.runInShell = false,
this.optional = false,
}) : args = List.unmodifiable(args);
} }
class ProxyCommandRunner { class ProxyCommandRunner {
@@ -38,21 +43,25 @@ class ProxyCommandRunner {
Future<bool> run(Iterable<ProxyCommand> commands) async { Future<bool> run(Iterable<ProxyCommand> commands) async {
var executed = false; var executed = false;
try { for (final command in commands) {
for (final command in commands) { executed = true;
executed = true; if (!await _succeeds(command) && !command.optional) {
final result = await process( return false;
command.executable,
command.args,
runInShell: command.runInShell,
);
if (result.exitCode != 0) {
return false;
}
} }
} on ProcessException {
return false;
} }
return executed; return executed;
} }
Future<bool> _succeeds(ProxyCommand command) async {
try {
final result = await process(
command.executable,
command.args,
runInShell: command.runInShell,
);
return result.exitCode == 0;
} on ProcessException {
return false;
}
}
} }
+17
View File
@@ -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 `<local>` token for single-label intranet
/// hosts like `nas` has no cross-platform pattern. Loopback needs no entry:
/// WinINet never proxies it.
List<String> windowsBypassList(List<String> bypassDomain) {
final entries = <String>{};
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, '<local>'}.toList();
}
+92 -1
View File
@@ -7,6 +7,7 @@ import 'package:proxy/proxy_method_channel.dart';
import 'package:proxy/src/linux_proxy.dart'; import 'package:proxy/src/linux_proxy.dart';
import 'package:proxy/src/macos_proxy.dart'; import 'package:proxy/src/macos_proxy.dart';
import 'package:proxy/src/proxy_command.dart'; import 'package:proxy/src/proxy_command.dart';
import 'package:proxy/src/windows_proxy.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@@ -68,6 +69,30 @@ void main() {
expect(await runner.run([ProxyCommand('missing', const [])]), isFalse); expect(await runner.run([ProxyCommand('missing', const [])]), isFalse);
}); });
test('carries on past an optional command that fails', () async {
final started = <String>[];
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', () { group('Linux proxy command builders', () {
@@ -104,7 +129,7 @@ void main() {
'kwriteconfig6', 'kwriteconfig6',
'kwriteconfig5', 'kwriteconfig5',
]); ]);
expect(executedCommands, everyElement('kwriteconfig5')); expect(executedCommands.toSet(), {'kwriteconfig5', 'dbus-send'});
}, },
); );
@@ -237,6 +262,52 @@ void main() {
expect(commands.map((command) => command.executable).toSet(), { expect(commands.map((command) => command.executable).toSet(), {
'kwriteconfig6', '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(), { expect(commands.map((command) => command.executable).toSet(), {
'kwriteconfig5', 'kwriteconfig5',
'dbus-send',
}); });
}, },
); );
@@ -268,6 +340,7 @@ void main() {
expect(commands.map((command) => command.executable).toSet(), { expect(commands.map((command) => command.executable).toSet(), {
'kwriteconfig5', 'kwriteconfig5',
'dbus-send',
}); });
}); });
@@ -357,6 +430,24 @@ USB 10/100/1000 LAN
}); });
}); });
group('windowsBypassList', () {
test('adds the intranet token once', () {
expect(windowsBypassList(['localhost', '127.*', '<local>']), [
'localhost',
'127.*',
'<local>',
]);
expect(windowsBypassList(['localhost']), ['localhost', '<local>']);
});
test('brackets bare IPv6 literals and drops blank entries', () {
expect(
windowsBypassList([' ::1 ', 'fe80::1', '', '[fd00::1]', '*.lan']),
['[::1]', '[fe80::1]', '[fd00::1]', '*.lan', '<local>'],
);
});
});
group('MethodChannelProxy', () { group('MethodChannelProxy', () {
final proxy = MethodChannelProxy(); final proxy = MethodChannelProxy();
+2 -2
View File
@@ -46,7 +46,7 @@ target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
# dependencies here. # dependencies here.
target_include_directories(${PLUGIN_NAME} INTERFACE target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include") "${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. # List of absolute paths to libraries that should be bundled with the plugin.
# This list could contain prebuilt libraries, or libraries created by an # This list could contain prebuilt libraries, or libraries created by an
@@ -86,7 +86,7 @@ add_executable(${TEST_RUNNER}
) )
apply_standard_settings(${TEST_RUNNER}) apply_standard_settings(${TEST_RUNNER})
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") 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) target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
# flutter_wrapper_plugin has link dependencies on the Flutter DLL. # flutter_wrapper_plugin has link dependencies on the Flutter DLL.
add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD
@@ -16,6 +16,9 @@ extern "C" {
FLUTTER_PLUGIN_EXPORT void ProxyPluginCApiRegisterWithRegistrar( FLUTTER_PLUGIN_EXPORT void ProxyPluginCApiRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar); FlutterDesktopPluginRegistrarRef registrar);
// Safe pre-engine.
FLUTTER_PLUGIN_EXPORT bool ProxyPluginClearStaleProxy();
#if defined(__cplusplus) #if defined(__cplusplus)
} // extern "C" } // extern "C"
#endif #endif
+177 -4
View File
@@ -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 <winsock2.h>
#include <ws2ipdef.h>
#include "proxy_plugin.h" #include "proxy_plugin.h"
// This must be included before many other Windows headers. // This must be included before many other Windows headers.
@@ -6,12 +11,17 @@
#include <WinInet.h> #include <WinInet.h>
#include <Ras.h> #include <Ras.h>
#include <RasError.h> #include <RasError.h>
#include <iphlpapi.h>
#include <stdlib.h>
#include <algorithm> #include <algorithm>
#include <functional>
#include <map>
#include <string> #include <string>
#include <vector> #include <vector>
#pragma comment(lib, "wininet") #pragma comment(lib, "wininet")
#pragma comment(lib, "Rasapi32") #pragma comment(lib, "Rasapi32")
#pragma comment(lib, "iphlpapi")
#include <flutter/method_channel.h> #include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h> #include <flutter/plugin_registrar_windows.h>
@@ -24,6 +34,7 @@ namespace
constexpr int kMinProxyPort = 1; constexpr int kMinProxyPort = 1;
constexpr int kMaxProxyPort = 65535; constexpr int kMaxProxyPort = 65535;
constexpr wchar_t kProxyServerPrefix[] = L"127.0.0.1:";
std::wstring Utf8ToWide(const std::string& value) std::wstring Utf8ToWide(const std::string& value)
{ {
@@ -80,9 +91,9 @@ bool SetOptionsForConnection(
sizeof(list)) != FALSE; sizeof(list)) != FALSE;
} }
bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list) bool ForEachConnection(const std::function<bool(LPTSTR)>& visit)
{ {
bool success = SetOptionsForConnection(list, nullptr); bool success = visit(nullptr);
DWORD size = 0; DWORD size = 0;
DWORD count = 0; DWORD count = 0;
@@ -99,7 +110,7 @@ bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list)
{ {
for (DWORD i = 0; i < count; i++) for (DWORD i = 0; i < count; i++)
{ {
success = SetOptionsForConnection(list, entries[i].szEntryName) && success; success = visit(entries[i].szEntryName) && success;
} }
} }
else else
@@ -115,6 +126,14 @@ bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list)
return success; return success;
} }
bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list)
{
return ForEachConnection([&list](LPTSTR connection)
{
return SetOptionsForConnection(list, connection);
});
}
bool NotifySettingsChanged() bool NotifySettingsChanged()
{ {
const bool changed = InternetSetOption( const bool changed = InternetSetOption(
@@ -126,7 +145,7 @@ bool NotifySettingsChanged()
bool startProxy(const int port, const flutter::EncodableList& bypassDomain) 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); auto bypassList = BuildBypassList(bypassDomain);
std::vector<INTERNET_PER_CONN_OPTION> options(3); std::vector<INTERNET_PER_CONN_OPTION> options(3);
@@ -166,11 +185,165 @@ bool stopProxy()
return optionsApplied && settingsNotified; return optionsApplied && settingsNotified;
} }
std::optional<int> 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 <typename Table>
bool TableHasPort(const std::vector<BYTE>& buffer, int port)
{
const auto* table = reinterpret_cast<const Table*>(buffer.data());
for (DWORD i = 0; i < table->dwNumEntries; i++)
{
const auto localPort =
_byteswap_ushort(static_cast<USHORT>(table->table[i].dwLocalPort));
if (localPort == port)
{
return true;
}
}
return false;
}
std::optional<bool> HasTcpListener(ULONG family, int port)
{
std::vector<BYTE> 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<MIB_TCPTABLE_OWNER_PID>(buffer, port)
: TableHasPort<MIB_TCP6TABLE_OWNER_PID>(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<ULONG>(AF_INET),
static_cast<ULONG>(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<int, bool>& releasedPorts)
{
std::vector<INTERNET_PER_CONN_OPTION> 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<DWORD>(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
namespace proxy namespace proxy
{ {
bool ClearStaleProxy()
{
std::map<int, bool> releasedPorts;
const bool cleared = ForEachConnection(
[&releasedPorts](LPTSTR connection)
{
return ClearStaleProxyForConnection(connection, releasedPorts);
});
const bool settingsNotified = NotifySettingsChanged();
return cleared && settingsNotified;
}
// static // static
void ProxyPlugin::RegisterWithRegistrar( void ProxyPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows *registrar) flutter::PluginRegistrarWindows *registrar)
+4
View File
@@ -11,6 +11,10 @@
namespace proxy { 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 { class ProxyPlugin : public flutter::Plugin {
public: public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
@@ -10,3 +10,7 @@ void ProxyPluginCApiRegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance() flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar)); ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
} }
bool ProxyPluginClearStaleProxy() {
return proxy::ClearStaleProxy();
}
+41 -5
View File
@@ -6,10 +6,46 @@
import '../frb_generated.dart'; import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
Stream<Uint8List> restartIpcServer({required String name}) => // These functions are ignored because they are not marked as `pub`: `bridge`
RustLib.instance.api.crateApiIpcRestartIpcServer(name: name);
Future<void> stopIpcServer() => RustLib.instance.api.crateApiIpcStopIpcServer(); // Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>>
abstract class IpcServer implements RustOpaqueInterface {
static Future<IpcServer> bind({required String address}) =>
RustLib.instance.api.crateApiIpcIpcServerBind(address: address);
Future<void> sendIpcMessage({required List<int> data}) => Future<void> close();
RustLib.instance.api.crateApiIpcSendIpcMessage(data: data);
Stream<IpcEvent> events();
void send({required List<int> 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 }
+442 -124
View File
@@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.13.0'; String get codegenVersion => '2.13.0';
@override @override
int get rustContentHash => -1283005629; int get rustContentHash => 736531895;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@@ -81,6 +81,17 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
} }
abstract class RustLibApi extends BaseApi { abstract class RustLibApi extends BaseApi {
Future<IpcServer> crateApiIpcIpcServerBind({required String address});
Future<void> crateApiIpcIpcServerClose({required IpcServer that});
Stream<IpcEvent> crateApiIpcIpcServerEvents({required IpcServer that});
void crateApiIpcIpcServerSend({
required IpcServer that,
required List<int> message,
});
List<String> crateApiEditorRopeBridgeCachedLinesRange({ List<String> crateApiEditorRopeBridgeCachedLinesRange({
required RopeBridge that, required RopeBridge that,
required BigInt startLine, required BigInt startLine,
@@ -154,18 +165,20 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateApiInitInitApp(); Future<void> crateApiInitInitApp();
Stream<Uint8List> crateApiIpcRestartIpcServer({required String name});
Future<void> crateApiIpcSendIpcMessage({required List<int> data});
Future<List<HotKeyFailure>> crateApiHotkeySetHotKeys({ Future<List<HotKeyFailure>> crateApiHotkeySetHotKeys({
required List<HotKeySpec> specs, required List<HotKeySpec> specs,
}); });
Future<void> crateApiIpcStopIpcServer();
Future<List<String>> crateApiEditorWordsExtract({required RopeBridge rope}); Future<List<String>> 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 RustArcIncrementStrongCountFnType
get rust_arc_increment_strong_count_RopeBridge; get rust_arc_increment_strong_count_RopeBridge;
@@ -183,6 +196,136 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
required super.portManager, required super.portManager,
}); });
@override
Future<IpcServer> 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<void> 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<IpcEvent> crateApiIpcIpcServerEvents({required IpcServer that}) {
final sink = RustStreamSink<IpcEvent>();
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<int> 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 @override
List<String> crateApiEditorRopeBridgeCachedLinesRange({ List<String> crateApiEditorRopeBridgeCachedLinesRange({
required RopeBridge that, required RopeBridge that,
@@ -199,7 +342,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
sse_encode_usize(startLine, serializer); sse_encode_usize(startLine, serializer);
sse_encode_usize(endLine, serializer); sse_encode_usize(endLine, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_list_String, decodeSuccessData: sse_decode_list_String,
@@ -232,7 +375,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer, serializer,
); );
sse_encode_usize(charIdx, serializer); sse_encode_usize(charIdx, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_usize, decodeSuccessData: sse_decode_usize,
@@ -258,7 +401,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(initialText, serializer); sse_encode_String(initialText, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: decodeSuccessData:
@@ -288,7 +431,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
that, that,
serializer, serializer,
); );
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_String, decodeSuccessData: sse_decode_String,
@@ -320,7 +463,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
sse_encode_usize(charIdx, serializer); sse_encode_usize(charIdx, serializer);
sse_encode_String(text, serializer); sse_encode_String(text, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -349,7 +492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
that, that,
serializer, serializer,
); );
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_usize, decodeSuccessData: sse_decode_usize,
@@ -378,7 +521,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
that, that,
serializer, serializer,
); );
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_usize, decodeSuccessData: sse_decode_usize,
@@ -411,7 +554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer, serializer,
); );
sse_encode_usize(lineIdx, serializer); sse_encode_usize(lineIdx, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_String, decodeSuccessData: sse_decode_String,
@@ -444,7 +587,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer, serializer,
); );
sse_encode_usize(lineIdx, serializer); sse_encode_usize(lineIdx, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_usize, decodeSuccessData: sse_decode_usize,
@@ -479,7 +622,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
sse_encode_usize(start, serializer); sse_encode_usize(start, serializer);
sse_encode_usize(end, serializer); sse_encode_usize(end, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -514,7 +657,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
sse_encode_usize(start, serializer); sse_encode_usize(start, serializer);
sse_encode_usize(end, serializer); sse_encode_usize(end, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_String, decodeSuccessData: sse_decode_String,
@@ -547,7 +690,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 12, funcId: 16,
port: port_, port: port_,
); );
}, },
@@ -585,7 +728,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 13, funcId: 17,
port: port_, port: port_,
); );
}, },
@@ -620,7 +763,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer, serializer,
); );
sse_encode_usize(targetOffset, serializer); sse_encode_usize(targetOffset, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_opt_box_autoadd_usize, 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(firstVisible, serializer);
sse_encode_usize(lastVisible, serializer); sse_encode_usize(lastVisible, serializer);
sse_encode_usize(tabSize, serializer); sse_encode_usize(tabSize, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_list_guide_block, decodeSuccessData: sse_decode_list_guide_block,
@@ -688,7 +831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 16, funcId: 20,
port: port_, port: port_,
); );
}, },
@@ -717,7 +860,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 17, funcId: 21,
port: port_, port: port_,
); );
}, },
@@ -735,70 +878,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiInitInitAppConstMeta => TaskConstMeta get kCrateApiInitInitAppConstMeta =>
const TaskConstMeta(debugName: "init_app", argNames: []); const TaskConstMeta(debugName: "init_app", argNames: []);
@override
Stream<Uint8List> crateApiIpcRestartIpcServer({required String name}) {
final sink = RustStreamSink<Uint8List>();
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<void> crateApiIpcSendIpcMessage({required List<int> 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 @override
Future<List<HotKeyFailure>> crateApiHotkeySetHotKeys({ Future<List<HotKeyFailure>> crateApiHotkeySetHotKeys({
required List<HotKeySpec> specs, required List<HotKeySpec> specs,
@@ -811,7 +890,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 20, funcId: 22,
port: port_, port: port_,
); );
}, },
@@ -829,33 +908,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiHotkeySetHotKeysConstMeta => TaskConstMeta get kCrateApiHotkeySetHotKeysConstMeta =>
const TaskConstMeta(debugName: "set_hot_keys", argNames: ["specs"]); const TaskConstMeta(debugName: "set_hot_keys", argNames: ["specs"]);
@override
Future<void> 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 @override
Future<List<String>> crateApiEditorWordsExtract({required RopeBridge rope}) { Future<List<String>> crateApiEditorWordsExtract({required RopeBridge rope}) {
return handler.executeNormal( return handler.executeNormal(
@@ -869,7 +921,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 22, funcId: 23,
port: port_, port: port_,
); );
}, },
@@ -887,6 +939,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiEditorWordsExtractConstMeta => TaskConstMeta get kCrateApiEditorWordsExtractConstMeta =>
const TaskConstMeta(debugName: "words_extract", argNames: ["rope"]); 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 RustArcIncrementStrongCountFnType
get rust_arc_increment_strong_count_RopeBridge => wire get rust_arc_increment_strong_count_RopeBridge => wire
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge; .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); 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<dynamic>);
}
@protected @protected
RopeBridge RopeBridge
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( 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<dynamic>); return RopeBridgeImpl.frbInternalDcoDecode(raw as List<dynamic>);
} }
@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<dynamic>);
}
@protected @protected
RopeBridge RopeBridge
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( 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<dynamic>); return RopeBridgeImpl.frbInternalDcoDecode(raw as List<dynamic>);
} }
@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<dynamic>);
}
@protected @protected
RopeBridge RopeBridge
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -929,9 +1016,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
@protected @protected
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse( RustStreamSink<IpcEvent> dco_decode_StreamSink_ipc_event_Sse(dynamic raw) {
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
throw UnimplementedError(); throw UnimplementedError();
} }
@@ -948,6 +1033,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw as String; 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 @protected
BigInt dco_decode_box_autoadd_usize(dynamic raw) { BigInt dco_decode_box_autoadd_usize(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1005,6 +1096,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw as int; 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<dynamic>;
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 @protected
List<String> dco_decode_list_String(dynamic raw) { List<String> dco_decode_list_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1053,6 +1164,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (raw as List<dynamic>).map(dco_decode_rust_fold_range).toList(); return (raw as List<dynamic>).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 @protected
BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw) { BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1102,6 +1225,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return AnyhowException(inner); 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 @protected
RopeBridge RopeBridge
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( 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 @protected
RopeBridge RopeBridge
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( 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 @protected
RopeBridge RopeBridge
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -1139,7 +1298,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
@protected @protected
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse( RustStreamSink<IpcEvent> sse_decode_StreamSink_ipc_event_Sse(
SseDeserializer deserializer, SseDeserializer deserializer,
) { ) {
// Codec=Sse (Serialization based), see doc to use other codecs // 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); 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 @protected
BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer) { BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -1212,6 +1377,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return deserializer.buffer.getInt32(); 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 @protected
List<String> sse_decode_list_String(SseDeserializer deserializer) { List<String> sse_decode_list_String(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -1304,6 +1491,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return ans_; 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 @protected
BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer) { BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // 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); 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 @protected
void void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( 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 @protected
void void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( 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 @protected
void void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -1401,15 +1649,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
@protected @protected
void sse_encode_StreamSink_list_prim_u_8_strict_Sse( void sse_encode_StreamSink_ipc_event_Sse(
RustStreamSink<Uint8List> self, RustStreamSink<IpcEvent> self,
SseSerializer serializer, SseSerializer serializer,
) { ) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String( sse_encode_String(
self.setupAndSerialize( self.setupAndSerialize(
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeSuccessData: sse_decode_ipc_event,
decodeErrorData: sse_decode_AnyhowException, 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); 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 @protected
void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer) { void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -1488,6 +1742,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer.buffer.putInt32(self); 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 @protected
void sse_encode_list_String(List<String> self, SseSerializer serializer) { void sse_encode_list_String(List<String> self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // 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 @protected
void sse_encode_opt_box_autoadd_usize( void sse_encode_opt_box_autoadd_usize(
BigInt? self, 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<dynamic> 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<void> close() =>
RustLib.instance.api.crateApiIpcIpcServerClose(that: this);
Stream<IpcEvent> events() =>
RustLib.instance.api.crateApiIpcIpcServerEvents(that: this);
void send({required List<int> message}) => RustLib.instance.api
.crateApiIpcIpcServerSend(that: this, message: message);
}
@sealed @sealed
class RopeBridgeImpl extends RustOpaque implements RopeBridge { class RopeBridgeImpl extends RustOpaque implements RopeBridge {
// Not to be used by end users // Not to be used by end users
@@ -21,6 +21,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
required super.portManager, required super.portManager,
}); });
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_IpcServerPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServerPtr;
CrossPlatformFinalizerArg CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_RopeBridgePtr => wire get rust_arc_decrement_strong_count_RopeBridgePtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridgePtr; ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridgePtr;
@@ -28,18 +32,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException dco_decode_AnyhowException(dynamic raw); AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
IpcServer
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
dynamic raw,
);
@protected @protected
RopeBridge RopeBridge
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
dynamic raw, dynamic raw,
); );
@protected
IpcServer
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
dynamic raw,
);
@protected @protected
RopeBridge RopeBridge
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
dynamic raw, dynamic raw,
); );
@protected
IpcServer
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
dynamic raw,
);
@protected @protected
RopeBridge RopeBridge
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -47,9 +69,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse( RustStreamSink<IpcEvent> dco_decode_StreamSink_ipc_event_Sse(dynamic raw);
dynamic raw,
);
@protected @protected
RustStreamSink<int> dco_decode_StreamSink_u_32_Sse(dynamic raw); RustStreamSink<int> dco_decode_StreamSink_u_32_Sse(dynamic raw);
@@ -57,6 +77,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
String dco_decode_String(dynamic raw); String dco_decode_String(dynamic raw);
@protected
int dco_decode_box_autoadd_u_32(dynamic raw);
@protected @protected
BigInt dco_decode_box_autoadd_usize(dynamic raw); BigInt dco_decode_box_autoadd_usize(dynamic raw);
@@ -75,6 +98,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int dco_decode_i_32(dynamic raw); 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 @protected
List<String> dco_decode_list_String(dynamic raw); List<String> dco_decode_list_String(dynamic raw);
@@ -99,6 +128,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
List<RustFoldRange> dco_decode_list_rust_fold_range(dynamic raw); List<RustFoldRange> 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 @protected
BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw);
@@ -120,18 +155,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
IpcServer
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
SseDeserializer deserializer,
);
@protected @protected
RopeBridge RopeBridge
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
IpcServer
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
SseDeserializer deserializer,
);
@protected @protected
RopeBridge RopeBridge
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
IpcServer
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
SseDeserializer deserializer,
);
@protected @protected
RopeBridge RopeBridge
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -139,7 +192,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse( RustStreamSink<IpcEvent> sse_decode_StreamSink_ipc_event_Sse(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@@ -151,6 +204,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
String sse_decode_String(SseDeserializer deserializer); String sse_decode_String(SseDeserializer deserializer);
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected @protected
BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer);
@@ -169,6 +225,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int sse_decode_i_32(SseDeserializer deserializer); 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 @protected
List<String> sse_decode_list_String(SseDeserializer deserializer); List<String> sse_decode_list_String(SseDeserializer deserializer);
@@ -199,6 +261,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
@protected @protected
BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer);
@@ -226,6 +294,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
IpcServer self,
SseSerializer serializer,
);
@protected @protected
void void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -233,6 +308,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
IpcServer self,
SseSerializer serializer,
);
@protected @protected
void void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -240,6 +322,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
IpcServer self,
SseSerializer serializer,
);
@protected @protected
void void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -248,8 +337,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void sse_encode_StreamSink_list_prim_u_8_strict_Sse( void sse_encode_StreamSink_ipc_event_Sse(
RustStreamSink<Uint8List> self, RustStreamSink<IpcEvent> self,
SseSerializer serializer, SseSerializer serializer,
); );
@@ -262,6 +351,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_String(String self, SseSerializer serializer); void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected @protected
void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer);
@@ -283,6 +375,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_i_32(int self, SseSerializer serializer); 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 @protected
void sse_encode_list_String(List<String> self, SseSerializer serializer); void sse_encode_list_String(List<String> self, SseSerializer serializer);
@@ -325,6 +423,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, 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 @protected
void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer);
@@ -361,6 +465,40 @@ class RustLibWire implements BaseWire {
RustLibWire(ffi.DynamicLibrary dynamicLibrary) RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup; : _lookup = dynamicLibrary.lookup;
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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 Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'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 Function(ffi.Pointer<ffi.Void>)>();
void void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
ffi.Pointer<ffi.Void> ptr, ffi.Pointer<ffi.Void> ptr,
@@ -23,6 +23,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
required super.portManager, required super.portManager,
}); });
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_IpcServerPtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer;
CrossPlatformFinalizerArg CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_RopeBridgePtr => wire get rust_arc_decrement_strong_count_RopeBridgePtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge; .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge;
@@ -30,18 +34,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException dco_decode_AnyhowException(dynamic raw); AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
IpcServer
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
dynamic raw,
);
@protected @protected
RopeBridge RopeBridge
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
dynamic raw, dynamic raw,
); );
@protected
IpcServer
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
dynamic raw,
);
@protected @protected
RopeBridge RopeBridge
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
dynamic raw, dynamic raw,
); );
@protected
IpcServer
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
dynamic raw,
);
@protected @protected
RopeBridge RopeBridge
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -49,9 +71,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse( RustStreamSink<IpcEvent> dco_decode_StreamSink_ipc_event_Sse(dynamic raw);
dynamic raw,
);
@protected @protected
RustStreamSink<int> dco_decode_StreamSink_u_32_Sse(dynamic raw); RustStreamSink<int> dco_decode_StreamSink_u_32_Sse(dynamic raw);
@@ -59,6 +79,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
String dco_decode_String(dynamic raw); String dco_decode_String(dynamic raw);
@protected
int dco_decode_box_autoadd_u_32(dynamic raw);
@protected @protected
BigInt dco_decode_box_autoadd_usize(dynamic raw); BigInt dco_decode_box_autoadd_usize(dynamic raw);
@@ -77,6 +100,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int dco_decode_i_32(dynamic raw); 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 @protected
List<String> dco_decode_list_String(dynamic raw); List<String> dco_decode_list_String(dynamic raw);
@@ -101,6 +130,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
List<RustFoldRange> dco_decode_list_rust_fold_range(dynamic raw); List<RustFoldRange> 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 @protected
BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw);
@@ -122,18 +157,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
IpcServer
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
SseDeserializer deserializer,
);
@protected @protected
RopeBridge RopeBridge
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
IpcServer
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
SseDeserializer deserializer,
);
@protected @protected
RopeBridge RopeBridge
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
IpcServer
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
SseDeserializer deserializer,
);
@protected @protected
RopeBridge RopeBridge
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -141,7 +194,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse( RustStreamSink<IpcEvent> sse_decode_StreamSink_ipc_event_Sse(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@@ -153,6 +206,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
String sse_decode_String(SseDeserializer deserializer); String sse_decode_String(SseDeserializer deserializer);
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected @protected
BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer);
@@ -171,6 +227,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int sse_decode_i_32(SseDeserializer deserializer); 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 @protected
List<String> sse_decode_list_String(SseDeserializer deserializer); List<String> sse_decode_list_String(SseDeserializer deserializer);
@@ -201,6 +263,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
@protected @protected
BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer);
@@ -228,6 +296,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
IpcServer self,
SseSerializer serializer,
);
@protected @protected
void void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -235,6 +310,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
IpcServer self,
SseSerializer serializer,
);
@protected @protected
void void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -242,6 +324,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
IpcServer self,
SseSerializer serializer,
);
@protected @protected
void void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
@@ -250,8 +339,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void sse_encode_StreamSink_list_prim_u_8_strict_Sse( void sse_encode_StreamSink_ipc_event_Sse(
RustStreamSink<Uint8List> self, RustStreamSink<IpcEvent> self,
SseSerializer serializer, SseSerializer serializer,
); );
@@ -264,6 +353,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_String(String self, SseSerializer serializer); void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected @protected
void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer);
@@ -285,6 +377,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_i_32(int self, SseSerializer serializer); 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 @protected
void sse_encode_list_String(List<String> self, SseSerializer serializer); void sse_encode_list_String(List<String> self, SseSerializer serializer);
@@ -327,6 +425,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, 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 @protected
void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer);
@@ -354,6 +458,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
class RustLibWire implements BaseWire { class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib); 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 void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
int ptr, int ptr,
@@ -377,6 +497,16 @@ external RustLibWasmModule get wasmModule;
@JS() @JS()
@anonymous @anonymous
extension type RustLibWasmModule._(JSObject _) implements JSObject { 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 external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
int ptr, int ptr,
+51 -9
View File
@@ -3,17 +3,59 @@ use crate::ipc;
use flutter_rust_bridge::for_generated::SseCodec; use flutter_rust_bridge::for_generated::SseCodec;
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
#[frb] pub enum IpcEventKind {
pub fn restart_ipc_server(name: String, sink: StreamSink<Vec<u8>, SseCodec>) -> Result<(), String> { Connected,
ipc::restart_server(name, sink) Message,
Disconnected,
Failed,
} }
#[frb] pub struct IpcEvent {
pub fn stop_ipc_server() -> Result<(), String> { pub kind: IpcEventKind,
ipc::stop_server() pub pid: Option<u32>,
pub payload: Vec<u8>,
pub error: Option<String>,
} }
#[frb] fn bridge(event: ipc::Event) -> IpcEvent {
pub fn send_ipc_message(data: Vec<u8>) -> Result<(), String> { let (kind, pid, payload, error) = match event {
ipc::send_message(data) 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<IpcServer, String> {
let server = ipc::Server::bind(address).map_err(|error| error.to_string())?;
Ok(Self { server })
}
pub fn events(&self, sink: StreamSink<IpcEvent, SseCodec>) {
self.server
.events(Box::new(move |event| sink.add(bridge(event)).is_ok()));
}
#[frb(sync)]
pub fn send(&self, message: Vec<u8>) -> Result<(), String> {
self.server
.send(&message)
.map_err(|error| error.to_string())
}
pub fn close(&self) {
self.server.close();
}
} }
+441 -127
View File
@@ -28,6 +28,7 @@
// Section: imports // Section: imports
use crate::api::editor::*; use crate::api::editor::*;
use crate::api::ipc::*;
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; 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::for_generated::{transform_result_dco, Lifetimeable, Lockable};
use flutter_rust_bridge::{Handler, IntoIntoDart}; use flutter_rust_bridge::{Handler, IntoIntoDart};
@@ -40,7 +41,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi, default_rust_auto_opaque = RustAutoOpaqueMoi,
); );
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.13.0"; 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 // Section: executor
@@ -48,6 +49,192 @@ flutter_rust_bridge::frb_generated_default_handler!();
// Section: wire_funcs // 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::SseCodec, _, _>(
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 = <String>::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::SseCodec, _, _>(
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 = <RustOpaqueMoi<
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>,
>>::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::SseCodec, _, _>(
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 = <RustOpaqueMoi<
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>,
>>::sse_decode(&mut deserializer);
let api_sink = <StreamSink<
crate::api::ipc::IpcEvent,
flutter_rust_bridge::for_generated::SseCodec,
>>::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::SseCodec, _>(
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 = <RustOpaqueMoi<
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>,
>>::sse_decode(&mut deserializer);
let api_message = <Vec<u8>>::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( fn wire__crate__api__editor__RopeBridge_cached_lines_range_impl(
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32, 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::SseCodec, _, _>(
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 = <String>::sse_decode(&mut deserializer);
let api_sink =
<StreamSink<Vec<u8>, 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::SseCodec, _, _>(
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 = <Vec<u8>>::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( fn wire__crate__api__hotkey__set_hot_keys_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, 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::SseCodec, _, _>(
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( fn wire__crate__api__editor__words_extract_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -1027,6 +1112,9 @@ fn wire__crate__api__editor__words_extract_impl(
// Section: related_funcs // Section: related_funcs
flutter_rust_bridge::frb_generated_moi_arc_impl_value!(
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>
);
flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::frb_generated_moi_arc_impl_value!(
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RopeBridge> flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RopeBridge>
); );
@@ -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 = <RustOpaqueMoi<
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>,
>>::sse_decode(deserializer);
return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner);
}
}
impl SseDecode for RopeBridge { impl SseDecode for RopeBridge {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -1051,6 +1149,16 @@ impl SseDecode for RopeBridge {
} }
} }
impl SseDecode
for RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<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 = <usize>::sse_decode(deserializer);
return decode_rust_opaque_moi(inner);
}
}
impl SseDecode impl SseDecode
for RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RopeBridge>> for RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RopeBridge>>
{ {
@@ -1061,7 +1169,9 @@ impl SseDecode
} }
} }
impl SseDecode for StreamSink<Vec<u8>, flutter_rust_bridge::for_generated::SseCodec> { impl SseDecode
for StreamSink<crate::api::ipc::IpcEvent, flutter_rust_bridge::for_generated::SseCodec>
{
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <String>::sse_decode(deserializer); let mut inner = <String>::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 = <crate::api::ipc::IpcEventKind>::sse_decode(deserializer);
let mut var_pid = <Option<u32>>::sse_decode(deserializer);
let mut var_payload = <Vec<u8>>::sse_decode(deserializer);
let mut var_error = <Option<String>>::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 = <i32>::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<String> { impl SseDecode for Vec<String> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -1240,6 +1380,28 @@ impl SseDecode for Vec<crate::api::editor::RustFoldRange> {
} }
} }
impl SseDecode for Option<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(<String>::sse_decode(deserializer));
} else {
return None;
}
}
}
impl SseDecode for Option<u32> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(<u32>::sse_decode(deserializer));
} else {
return None;
}
}
}
impl SseDecode for Option<usize> { impl SseDecode for Option<usize> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { 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 // Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id { match func_id {
12 => wire__crate__api__script__evaluate_script_impl(port, ptr, rust_vec_len, data_len), 1 => wire__crate__api__ipc__IpcServer_bind_impl(port, ptr, rust_vec_len, data_len),
13 => wire__crate__api__editor__folds_compute_all_impl(port, ptr, rust_vec_len, data_len), 2 => wire__crate__api__ipc__IpcServer_close_impl(port, ptr, rust_vec_len, data_len),
16 => wire__crate__api__hotkey__hot_key_events_impl(port, ptr, rust_vec_len, data_len), 3 => wire__crate__api__ipc__IpcServer_events_impl(port, ptr, rust_vec_len, data_len),
17 => wire__crate__api__init__init_app_impl(port, ptr, rust_vec_len, data_len), 16 => wire__crate__api__script__evaluate_script_impl(port, ptr, rust_vec_len, data_len),
18 => wire__crate__api__ipc__restart_ipc_server_impl(port, ptr, rust_vec_len, data_len), 17 => wire__crate__api__editor__folds_compute_all_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__hot_key_events_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__init__init_app_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__hotkey__set_hot_keys_impl(port, ptr, rust_vec_len, data_len),
22 => wire__crate__api__editor__words_extract_impl(port, ptr, rust_vec_len, data_len), 23 => wire__crate__api__editor__words_extract_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -1326,31 +1488,47 @@ fn pde_ffi_dispatcher_sync_impl(
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
// Codec=Pde (Serialization + dispatch), see doc to use other codecs // Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id { 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, ptr,
rust_vec_len, rust_vec_len,
data_len, data_len,
), ),
2 => wire__crate__api__editor__RopeBridge_char_to_line_impl(ptr, rust_vec_len, data_len), 6 => 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), 7 => 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), 8 => 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), 9 => 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), 10 => 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), 11 => 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), 12 => 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), 13 => 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), 14 => 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), 15 => wire__crate__api__editor__RopeBridge_slice_impl(ptr, rust_vec_len, data_len),
14 => { 18 => {
wire__crate__api__editor__folds_find_matching_bracket_impl(ptr, rust_vec_len, data_len) 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!(), _ => unreachable!(),
} }
} }
// Section: rust2dart // Section: rust2dart
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for FrbWrapper<IpcServer> {
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<IpcServer> {}
impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<IpcServer>> for IpcServer {
fn into_into_dart(self) -> FrbWrapper<IpcServer> {
self.into()
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for FrbWrapper<RopeBridge> { impl flutter_rust_bridge::IntoDart for FrbWrapper<RopeBridge> {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
@@ -1458,6 +1636,44 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::hotkey::HotKeySpec>
} }
} }
// Codec=Dco (DartCObject based), see doc to use other codecs // 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<crate::api::ipc::IpcEvent> 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<crate::api::ipc::IpcEventKind>
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 { impl flutter_rust_bridge::IntoDart for crate::api::editor::RustFoldRange {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { 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) {
<RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>>>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer);
}
}
impl SseEncode for RopeBridge { impl SseEncode for RopeBridge {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -1493,6 +1716,17 @@ impl SseEncode for RopeBridge {
} }
} }
impl SseEncode
for RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>>
{
// 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();
<usize>::sse_encode(ptr, serializer);
<i32>::sse_encode(size, serializer);
}
}
impl SseEncode impl SseEncode
for RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RopeBridge>> for RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RopeBridge>>
{ {
@@ -1504,7 +1738,9 @@ impl SseEncode
} }
} }
impl SseEncode for StreamSink<Vec<u8>, flutter_rust_bridge::for_generated::SseCodec> { impl SseEncode
for StreamSink<crate::api::ipc::IpcEvent, flutter_rust_bridge::for_generated::SseCodec>
{
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
unimplemented!("") 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) {
<crate::api::ipc::IpcEventKind>::sse_encode(self.kind, serializer);
<Option<u32>>::sse_encode(self.pid, serializer);
<Vec<u8>>::sse_encode(self.payload, serializer);
<Option<String>>::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) {
<i32>::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<String> { impl SseEncode for Vec<String> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -1649,6 +1913,26 @@ impl SseEncode for Vec<crate::api::editor::RustFoldRange> {
} }
} }
impl SseEncode for Option<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<String>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<u32> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<u32>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<usize> { impl SseEncode for Option<usize> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -1712,6 +1996,7 @@ mod io {
use super::*; use super::*;
use crate::api::editor::*; use crate::api::editor::*;
use crate::api::ipc::*;
use flutter_rust_bridge::for_generated::byteorder::{ use flutter_rust_bridge::for_generated::byteorder::{
NativeEndian, ReadBytesExt, WriteBytesExt, NativeEndian, ReadBytesExt, WriteBytesExt,
}; };
@@ -1722,6 +2007,20 @@ mod io {
flutter_rust_bridge::frb_generated_boilerplate_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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>>::decrement_strong_count(ptr as _);
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn frbgen_rust_api_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( pub extern "C" fn frbgen_rust_api_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
ptr: *const std::ffi::c_void, ptr: *const std::ffi::c_void,
@@ -1749,6 +2048,7 @@ mod web {
use super::*; use super::*;
use crate::api::editor::*; use crate::api::editor::*;
use crate::api::ipc::*;
use flutter_rust_bridge::for_generated::byteorder::{ use flutter_rust_bridge::for_generated::byteorder::{
NativeEndian, ReadBytesExt, WriteBytesExt, NativeEndian, ReadBytesExt, WriteBytesExt,
}; };
@@ -1761,6 +2061,20 @@ mod web {
flutter_rust_bridge::frb_generated_boilerplate_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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>>::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::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>>::decrement_strong_count(ptr as _);
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge( pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
ptr: *const std::ffi::c_void, ptr: *const std::ffi::c_void,
+83
View File
@@ -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<Option<u32>> {
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));
}
}
+65 -254
View File
@@ -1,283 +1,94 @@
use std::io::{self, Read, Write}; use std::io;
use std::thread; use tokio::io::{AsyncRead, AsyncReadExt};
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;
pub const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024; pub const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024;
pub const IO_POLL_INTERVAL: Duration = Duration::from_millis(20); const HEADER_SIZE: usize = 4;
const MIN_WRITE_BACKOFF: Duration = Duration::from_millis(1);
pub fn make_frame(ty: u8, payload: &[u8]) -> Vec<u8> { fn too_large() -> io::Error {
let mut frame = Vec::with_capacity(1 + payload.len()); io::Error::new(
frame.push(ty); io::ErrorKind::InvalidData,
frame.extend_from_slice(payload); format!("IPC frame exceeds {MAX_FRAME_SIZE} bytes"),
frame
}
pub fn validate_frame_len(len: usize) -> io::Result<u32> {
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
) )
} }
#[cfg(any(windows, test))] /// Prefixes `payload` with its little-endian length, the layout `readFrame`
fn normalize_windows_pipe_write(result: io::Result<usize>) -> io::Result<usize> { /// in `core/server.go` expects.
match result { pub fn encode(payload: &[u8]) -> io::Result<Vec<u8>> {
Ok(0) => Err(io::ErrorKind::WouldBlock.into()), if payload.len() > MAX_FRAME_SIZE {
result => result, 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( pub async fn read(reader: &mut (impl AsyncRead + Unpin)) -> io::Result<Vec<u8>> {
writer: &mut impl Write, let mut header = [0; HEADER_SIZE];
mut data: &[u8], reader.read_exact(&mut header).await?;
is_running: &impl Fn() -> bool, let len = u32::from_le_bytes(header) as usize;
) -> io::Result<()> { if len > MAX_FRAME_SIZE {
let mut backoff = MIN_WRITE_BACKOFF; return Err(too_large());
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<u8>,
payload_read: usize,
}
impl FrameReader {
pub fn poll(&mut self, reader: &mut impl Read) -> io::Result<Option<Vec<u8>>> {
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;
} }
let mut payload = vec![0; len];
reader.read_exact(&mut payload).await?;
Ok(payload)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::collections::VecDeque;
use std::io::Cursor;
enum ReadStep { fn read_all(bytes: &[u8]) -> io::Result<Vec<u8>> {
Data(Vec<u8>), let mut reader = bytes;
WouldBlock, tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(read(&mut reader))
} }
struct StepReader { #[test]
steps: VecDeque<ReadStep>, 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 { #[test]
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> { fn an_empty_frame_is_a_frame() {
match self.steps.pop_front() { assert_eq!(read_all(&encode(b"").unwrap()).unwrap(), b"");
Some(ReadStep::Data(mut data)) => { }
let read = data.len().min(buffer.len());
buffer[..read].copy_from_slice(&data[..read]); #[test]
if read < data.len() { fn a_truncated_frame_reads_as_end_of_stream() {
data.drain(..read); let frame = encode(b"hello").unwrap();
self.steps.push_front(ReadStep::Data(data));
} for len in [0, 2, HEADER_SIZE, frame.len() - 1] {
Ok(read) assert_eq!(
} read_all(&frame[..len]).unwrap_err().kind(),
Some(ReadStep::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()), io::ErrorKind::UnexpectedEof,
None => Ok(0), );
}
} }
} }
#[test] #[test]
fn frame_reader_reads_complete_frame() { fn an_oversized_header_is_rejected_before_its_payload_is_read() {
let payload = b"hello"; let header = (MAX_FRAME_SIZE as u32 + 1).to_le_bytes();
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();
assert_eq!( assert_eq!(
frame_reader.poll(&mut reader).unwrap(), read_all(&header).unwrap_err().kind(),
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(
io::ErrorKind::InvalidData, 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,
);
} }
} }
+17 -5
View File
@@ -1,15 +1,27 @@
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
mod access;
#[cfg(not(target_os = "android"))]
mod frame; mod frame;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
mod platform; mod outbox;
#[cfg(not(target_os = "android"))]
mod queue;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
mod server; mod server;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
mod unsupported; mod unsupported;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
pub use server::{restart_server, send_message, stop_server}; pub use server::Server;
#[cfg(target_os = "android")] #[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<u32> },
Message(Vec<u8>),
Disconnected { error: Option<String> },
Failed(String),
}
/// Returns false once nobody listens any more, which ends the server.
pub type EventSink = Box<dyn Fn(Event) -> bool + Send>;
+165
View File
@@ -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<Vec<u8>>,
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<Queue>,
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<u8>) -> 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<u8> {
loop {
if let Some(frame) = self.pop() {
return frame;
}
self.ready.notified().await;
}
}
fn pop(&self) -> Option<Vec<u8>> {
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");
}
}
-170
View File
@@ -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<Vec<u8>> {
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<Vec<u8>> {
Ok(Vec::new())
}
#[cfg(windows)]
fn bytes_available(receiver: &RecvHalf) -> io::Result<u32> {
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<usize> {
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));
}
}
-241
View File
@@ -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<Vec<u8>>,
pending_bytes: Arc<AtomicUsize>,
}
impl MessageSender {
pub fn channel(capacity: usize) -> (Self, mpsc::Receiver<Vec<u8>>, Arc<AtomicUsize>) {
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(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<u8>,
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();
}
}
+439 -233
View File
@@ -1,265 +1,471 @@
use super::frame::{ use super::outbox::Outbox;
self, FrameReader, IO_POLL_INTERVAL, TYPE_CONNECTED, TYPE_DATA, TYPE_DISCONNECTED, TYPE_ERROR, use super::{access, frame, Event, EventSink};
TYPE_READY, use interprocess::local_socket::tokio::{prelude::*, Listener, RecvHalf, SendHalf, Stream};
}; use interprocess::local_socket::{GenericFilePath, ListenerOptions};
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 std::io; use std::io;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{mpsc, Arc, Mutex, MutexGuard, PoisonError};
use std::sync::mpsc; use std::thread::{self, JoinHandle};
use std::sync::{Arc, Mutex}; use std::time::Duration;
use std::thread; use tokio::io::{AsyncWriteExt, BufReader};
use tokio::sync::oneshot;
macro_rules! ipc_debug { const STALE_SOCKET_RETRY_LIMIT: Duration = Duration::from_secs(1);
($($arg:tt)*) => {
if cfg!(debug_assertions) { struct Control {
eprintln!($($arg)*); events: Option<oneshot::Sender<EventSink>>,
shutdown: Option<oneshot::Sender<()>>,
worker: Option<JoinHandle<()>>,
}
/// 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<Outbox>,
control: Mutex<Control>,
}
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<Self> {
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<MessageSender>,
handle: Option<thread::JoinHandle<()>>,
}
static STATE: Mutex<ServerState> = 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<Vec<u8>, SseCodec>) -> Result<(), String> { pub fn send(&self, payload: &[u8]) -> io::Result<()> {
let _lifecycle = LIFECYCLE self.outbox.push(frame::encode(payload)?)
.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<u8>) -> 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<Vec<u8>, SseCodec>, message: impl AsRef<str>) {
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;
} }
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<Vec<u8>, SseCodec>) { fn run(
let fs_name = match name.clone().to_fs_name::<GenericFilePath>() { address: String,
Ok(name) => name, bound: mpsc::SyncSender<io::Result<()>>,
Err(e) => { events: oneshot::Receiver<EventSink>,
report_error(&sink, format!("name error: {e}")); shutdown: oneshot::Receiver<()>,
finish_server(&name); outbox: Arc<Outbox>,
) {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(error) => {
let _ = bound.send(Err(error));
return; return;
} }
}; };
runtime.block_on(async {
let options = platform::restrict_listener_mode(ListenerOptions::new().name(fs_name)); let listener = match listen(&address) {
let listener = match options.create_sync() { Ok(listener) => listener,
Ok(listener) => listener, Err(error) => {
Err(e) => { let _ = bound.send(Err(error));
report_error(&sink, format!("bind error: {e}")); return;
finish_server(&name); }
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) { fn listen(address: &str) -> io::Result<Listener> {
report_error(&sink, format!("socket permission error: {e}")); let options = ListenerOptions::new()
finish_server(&name); .name(address.to_fs_name::<GenericFilePath>()?)
return; .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) { async fn accept_peers(listener: &Listener, emit: &EventSink, outbox: &Outbox) {
report_error(&sink, format!("listener nonblocking error: {e}")); loop {
finish_server(&name); let stream = match listener.accept().await {
return;
}
if sink.add(frame::make_frame(TYPE_READY, &[])).is_err() {
finish_server(&name);
return;
}
while server_active() {
let stream = match listener.accept() {
Ok(stream) => stream, Ok(stream) => stream,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => { Err(error) => {
thread::sleep(ACCEPT_POLL_INTERVAL); emit(Event::Failed(format!("accept error: {error}")));
continue; return;
}
Err(e) => {
if server_active() {
report_error(&sink, format!("accept error: {e}"));
}
break;
} }
}; };
let pid = match access::admit(&stream) {
if let Err(e) = platform::authorize_peer(&stream) { Ok(pid) => pid,
ipc_debug!("[IPC] rejected connection: {e}"); Err(error) => {
continue; if cfg!(debug_assertions) {
} eprintln!("[IPC] rejected connection: {error}");
}
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}"));
continue; continue;
} }
}; };
// The host may send as soon as it sees `Connected`.
let (sender, rx, pending_bytes) = MessageSender::channel(MAX_PENDING_MESSAGES); outbox.open();
match STATE.lock() { let listening = emit(Event::Connected { pid });
Ok(mut state) if server_active() => state.sender = Some(sender), let error = if listening {
Ok(_) => break, exchange(stream, emit, outbox).await
Err(e) => { } else {
report_error(&sink, format!("state lock error: {e}")); None
break; };
} outbox.close();
if !listening || !emit(Event::Disconnected { error }) {
return;
} }
}
}
if sink async fn exchange(stream: Stream, emit: &EventSink, outbox: &Outbox) -> Option<String> {
.add(frame::make_frame(TYPE_CONNECTED, &connected_payload)) let (reader, writer) = stream.split();
.is_err() let result = tokio::select! {
{ result = receive(reader, emit) => result,
break; 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(); async fn transmit(mut writer: SendHalf, outbox: &Outbox) -> io::Result<()> {
let connection_running = Arc::new(AtomicBool::new(true)); loop {
let writer_running = Arc::clone(&connection_running); let frame = outbox.next().await;
let writer = thread::spawn(move || { writer.write_all(&frame).await?;
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
});
let mut frame_reader = FrameReader::default(); fn is_peer_gone(error: &io::Error) -> bool {
while connection_running.load(Ordering::SeqCst) && server_active() { matches!(
#[cfg(windows)] error.kind(),
let poll_result = frame_reader.poll(&mut platform::PipeReader { io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
receiver: &mut receiver, )
}); }
#[cfg(not(windows))]
let poll_result = frame_reader.poll(&mut receiver);
match poll_result { #[cfg(all(test, unix))]
Ok(Some(data)) => { mod tests {
if sink.add(frame::make_frame(TYPE_DATA, &data)).is_err() { use super::*;
break; use std::io::{Read, Write};
} use std::os::unix::fs::PermissionsExt;
} use std::os::unix::net::{UnixListener, UnixStream};
Ok(None) => thread::sleep(IO_POLL_INTERVAL), use std::path::Path;
Err(e) => { use std::sync::atomic::{AtomicUsize, Ordering};
if !frame::is_expected_disconnect_error(&e) && server_active() { use std::sync::mpsc::{channel, Receiver};
ipc_debug!("[IPC] read error: {e}, raw={:?}", e.raw_os_error()); use std::time::Instant;
report_error(&sink, format!("read error: {e}"));
}
break;
}
}
}
connection_running.store(false, Ordering::SeqCst); const WAIT: Duration = Duration::from_secs(5);
if let Ok(mut state) = STATE.lock() {
state.sender = None; fn socket_path() -> String {
} static NEXT: AtomicUsize = AtomicUsize::new(0);
if let Ok(Some(message)) = writer.join() { let name = format!(
report_error(&sink, message); "flclash-ipc-{}-{}.sock",
} std::process::id(),
if server_active() && sink.add(frame::make_frame(TYPE_DISCONNECTED, &[])).is_err() { NEXT.fetch_add(1, Ordering::Relaxed),
break; );
std::env::temp_dir().join(name).to_str().unwrap().to_owned()
}
fn attach(server: &Server) -> Receiver<Event> {
let (tx, rx) = channel();
server.events(Box::new(move |event| tx.send(event).is_ok()));
rx
}
fn serve(path: &str) -> (Server, Receiver<Event>) {
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>) -> Event {
events.recv_timeout(WAIT).expect("no IPC event arrived")
}
fn expect_connected(events: &Receiver<Event>) {
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<u8> {
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<u8> = (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());
}
} }
+20 -12
View File
@@ -1,18 +1,26 @@
use crate::frb_generated::StreamSink; use super::EventSink;
use flutter_rust_bridge::for_generated::SseCodec; use std::io;
// Android loads the Core in-process; these entry points exist only so that one // Android loads the Core in-process; this keeps one set of bindings for every platform.
// set of bindings serves every platform. pub struct Server;
const UNSUPPORTED: &str = "IPC server is not available on this platform";
pub fn restart_server(_name: String, _sink: StreamSink<Vec<u8>, SseCodec>) -> Result<(), String> { fn unsupported() -> io::Error {
Err(UNSUPPORTED.into()) io::Error::new(
io::ErrorKind::Unsupported,
"IPC server is not available on this platform",
)
} }
pub fn stop_server() -> Result<(), String> { impl Server {
Ok(()) pub fn bind(_address: String) -> io::Result<Self> {
} Err(unsupported())
}
pub fn send_message(_data: Vec<u8>) -> Result<(), String> { pub fn events(&self, _sink: EventSink) {}
Err(UNSUPPORTED.into())
pub fn send(&self, _payload: &[u8]) -> io::Result<()> {
Err(unsupported())
}
pub fn close(&self) {}
} }
+7 -7
View File
@@ -226,7 +226,7 @@ mod tests {
assert_eq!(result["entries"], json!({ "a": 0, "b": 1 })); 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 script = include_str!("../../tests/fixtures/profile_script.js");
let config = include_str!("../../tests/fixtures/profile_config.json"); let config = include_str!("../../tests/fixtures/profile_config.json");
@@ -234,8 +234,8 @@ mod tests {
} }
#[test] #[test]
fn runs_a_profile_overwrite_end_to_end() { fn runs_a_profile_extension_end_to_end() {
let result = overwrite_fixture(); let result = extension_fixture();
let proxies = result["proxies"].as_array().unwrap(); let proxies = result["proxies"].as_array().unwrap();
let groups = result["proxy-groups"].as_array().unwrap(); let groups = result["proxy-groups"].as_array().unwrap();
let rules = result["rules"].as_array().unwrap(); let rules = result["rules"].as_array().unwrap();
@@ -247,8 +247,8 @@ mod tests {
} }
#[test] #[test]
fn the_overwrite_renames_nodes_and_rewrites_the_chains_through_them() { fn the_extension_renames_nodes_and_rewrites_the_chains_through_them() {
let result = overwrite_fixture(); let result = extension_fixture();
let proxies = result["proxies"].as_array().unwrap(); let proxies = result["proxies"].as_array().unwrap();
let named = |name: &str| { let named = |name: &str| {
proxies proxies
@@ -266,8 +266,8 @@ mod tests {
} }
#[test] #[test]
fn the_overwrite_merges_dns_instead_of_replacing_it() { fn the_extension_merges_dns_instead_of_replacing_it() {
let dns = overwrite_fixture()["dns"].clone(); let dns = extension_fixture()["dns"].clone();
assert_eq!(dns["enhanced-mode"], json!("fake-ip")); assert_eq!(dns["enhanced-mode"], json!("fake-ip"));
assert_eq!(dns["nameserver"], json!(["223.5.5.5", "119.29.29.29"])); assert_eq!(dns["nameserver"], json!(["223.5.5.5", "119.29.29.29"]));
+2 -2
View File
@@ -1,6 +1,6 @@
/** /**
* A profile overwrite script written for this test suite. It does the work a * A profile extension script written for this test suite. It does the work a
* real overwrite does — drop subscription notices, canonicalise node names, * real one does — drop subscription notices, canonicalise node names,
* group by region, rate and dedicated line, build policy groups and rules, * 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 * merge DNS — so the engine is measured against how such scripts are written
* rather than against a checklist of language features. * rather than against a checklist of language features.
@@ -14,6 +14,13 @@ File _resolveSource(String relativePath) {
return direct; 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() { void main() {
late String pluginSource; late String pluginSource;
@@ -42,16 +49,39 @@ void main() {
}); });
test('windows rejected show leaves the visible menu untouched', () { test('windows rejected show leaves the visible menu untouched', () {
expect(pluginSource, contains('bool applied = ApplyIcon(!visible_);')); final show = _body(pluginSource, 'bool TrayPlugin::Show(');
expect( final lastRejection = show.lastIndexOf('return false;');
pluginSource, final rebuild = show.indexOf('RebuildMenu(menu_, *items);');
contains('''
if (!applied) {
return false;
}
visible_ = true;
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')),
); );
}); });
} }
+37 -12
View File
@@ -10,6 +10,8 @@ namespace {
constexpr UINT kTrayCallbackMessage = WM_USER + 1; constexpr UINT kTrayCallbackMessage = WM_USER + 1;
constexpr UINT kTrayIconId = 1; constexpr UINT kTrayIconId = 1;
constexpr UINT_PTR kRestoreTimerId = 0x54524159; // "TRAY"
constexpr UINT kRestoreIntervalMilliseconds = 2000;
const flutter::EncodableValue* ValueAt(const flutter::EncodableMap& map, const flutter::EncodableValue* ValueAt(const flutter::EncodableMap& map,
const char* key) { const char* key) {
@@ -188,15 +190,6 @@ bool TrayPlugin::Show(const flutter::EncodableMap& arguments) {
tool_tip_ = Utf16FromUtf8(*tool_tip); 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"); const flutter::EncodableList* items = ListAt(arguments, "menu");
if (items != nullptr) { if (items != nullptr) {
if (menu_ == nullptr) { if (menu_ == nullptr) {
@@ -205,13 +198,40 @@ bool TrayPlugin::Show(const flutter::EncodableMap& arguments) {
RebuildMenu(menu_, *items); RebuildMenu(menu_, *items);
} }
icon_requested_ = true;
bool applied = ApplyIcon(!visible_);
if (!applied && visible_) {
applied = ApplyIcon(true);
}
if (applied) {
visible_ = true;
} else {
ScheduleRestore();
}
return true; 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() { void TrayPlugin::Hide() {
if (visible_) { ::KillTimer(MainWindow(), kRestoreTimerId);
if (icon_requested_) {
::Shell_NotifyIconW(NIM_DELETE, &icon_data_); ::Shell_NotifyIconW(NIM_DELETE, &icon_data_);
} }
icon_requested_ = false;
if (icon_data_.hIcon != nullptr) { if (icon_data_.hIcon != nullptr) {
::DestroyIcon(icon_data_.hIcon); ::DestroyIcon(icon_data_.hIcon);
} }
@@ -267,12 +287,17 @@ std::optional<LRESULT> TrayPlugin::HandleWindowProc(HWND window,
return std::nullopt; return std::nullopt;
} }
if (message == WM_TIMER && wparam == kRestoreTimerId) {
RestoreIcon();
return 0;
}
const bool should_restore = const bool should_restore =
(taskbar_created_message_ != 0 && message == taskbar_created_message_) || (taskbar_created_message_ != 0 && message == taskbar_created_message_) ||
(message == WM_POWERBROADCAST && (wparam == PBT_APMRESUMEAUTOMATIC || (message == WM_POWERBROADCAST && (wparam == PBT_APMRESUMEAUTOMATIC ||
wparam == PBT_APMRESUMESUSPEND)); wparam == PBT_APMRESUMESUSPEND));
if (should_restore && visible_) { if (should_restore && icon_requested_) {
ApplyIcon(true); RestoreIcon();
} }
return std::nullopt; return std::nullopt;
+3
View File
@@ -37,6 +37,8 @@ class TrayPlugin : public flutter::Plugin {
void Hide(); void Hide();
bool OpenMenu(); bool OpenMenu();
bool ApplyIcon(bool add); bool ApplyIcon(bool add);
void RestoreIcon();
void ScheduleRestore();
void RebuildMenu(HMENU menu, const flutter::EncodableList& items); void RebuildMenu(HMENU menu, const flutter::EncodableList& items);
void SendEvent(const char* name, const flutter::EncodableValue& arguments); void SendEvent(const char* name, const flutter::EncodableValue& arguments);
@@ -53,6 +55,7 @@ class TrayPlugin : public flutter::Plugin {
HMENU menu_ = nullptr; HMENU menu_ = nullptr;
std::wstring tool_tip_; std::wstring tool_tip_;
bool visible_ = false; bool visible_ = false;
bool icon_requested_ = false;
UINT taskbar_created_message_ = 0; UINT taskbar_created_message_ = 0;
int window_proc_id_ = -1; int window_proc_id_ = -1;
+3 -1
View File
@@ -46,7 +46,9 @@ int WindowController::TopBorderHeight() const {
} }
void WindowController::PlaceView() 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; return;
} }
RECT client{}; RECT client{};
+2 -2
View File
@@ -50,8 +50,8 @@ struct WindowStyle {
// Rows the controller keeps clear above the view and paints transparent. // Rows the controller keeps clear above the view and paints transparent.
// Windows 10 draws the top border only inside a frame extended over the // 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 // 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 // Apply() extends it there; Windows 11 draws the border into the non-client
// non-client strip WM_NCCALCSIZE leaves. Zero while an accent effect is on, // strip, one physical pixel tall, that WM_NCCALCSIZE leaves. Zero while an accent effect is on,
// maximized or full screen. // maximized or full screen.
int TopBorderHeight(HWND hwnd) const; int TopBorderHeight(HWND hwnd) const;
}; };
+90 -14
View File
@@ -37,8 +37,11 @@ use windows_sys::Win32::System::JobObjects::{
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
}; };
#[cfg(not(target_os = "linux"))] #[cfg(not(any(
pub(super) const LISTEN_PORT: u16 = 47890; all(feature = "windows-service", target_os = "windows"),
target_os = "linux"
)))]
const LISTEN_PORT: u16 = 47890;
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
const CORE_PIPE_PREFIX: &str = r"\\.\pipe\FlClashCore_"; const CORE_PIPE_PREFIX: &str = r"\\.\pipe\FlClashCore_";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -356,6 +359,36 @@ fn lock_surviving_poison<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
.unwrap_or_else(|poisoned| poisoned.into_inner()) .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<ManagedCore>) -> Result<(), Error> { fn release_managed_core(managed: &mut Option<ManagedCore>) -> Result<(), Error> {
let Some(core) = managed.as_mut() else { let Some(core) = managed.as_mut() else {
return Ok(()); 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) { if let Err(error) = super::linux::ensure_owner_socket(&start_params.address) {
return error_response("invalidRequest", error.to_string(), StatusCode::BAD_REQUEST); 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); let mut managed = lock_surviving_poison(&MANAGED_CORE);
if let Err(error) = release_managed_core(&mut managed) { if let Err(error) = release_managed_core(&mut managed) {
log_message(format!( log_message(format!(
@@ -446,6 +484,7 @@ fn start(start_params: StartParams) -> warp::reply::Response {
let process_id = child.id(); let process_id = child.id();
if let Some(stderr) = child.stderr.take() { if let Some(stderr) = child.stderr.take() {
let reader = io::BufReader::new(stderr); let reader = io::BufReader::new(stderr);
let session_id = start_params.session_id.clone();
thread::spawn(move || { thread::spawn(move || {
for line in reader.lines() { for line in reader.lines() {
match line { 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) { *managed = match ManagedCore::adopt(start_params.session_id.clone(), child) {
@@ -1077,7 +1117,7 @@ mod tests {
#[tokio::test] #[tokio::test]
#[allow(clippy::await_holding_lock)] #[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(); let _state = lock_process_state();
*lock_surviving_poison(&MANAGED_CORE) = Some( *lock_surviving_poison(&MANAGED_CORE) = Some(
ManagedCore::adopt( ManagedCore::adopt(
@@ -1087,20 +1127,56 @@ mod tests {
.unwrap(), .unwrap(),
); );
let response = warp::test::request() let response = launch(StartParams {
.method("POST") address: ALLOWED_CORE_ADDRESS.to_string(),
.path("/start") session_id: "0123456789abcdef0123456789abcdef".to_string(),
.json(&StartParams { });
address: ALLOWED_CORE_ADDRESS.to_string(),
session_id: "0123456789abcdef0123456789abcdef".to_string(),
})
.reply(&routes())
.await;
assert_eq!(response.status(), StatusCode::CONFLICT); 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()); 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] #[tokio::test]
+73 -4
View File
@@ -1,6 +1,5 @@
use crate::service::hub::{ use crate::service::hub::{
ensure_core_sha256_configured, log_message, release_managed_core_on_shutdown, routes, ensure_core_sha256_configured, log_message, release_managed_core_on_shutdown, routes,
LISTEN_PORT,
}; };
use crate::service::owner::{client_pid, same_sid, TcpConnection}; 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::io::{Error, Result as IoResult};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::pin::Pin; use std::pin::Pin;
use std::ptr::null_mut; use std::ptr::{null, null_mut};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::time::Duration; use std::time::Duration;
@@ -17,18 +16,26 @@ use tokio::net::{TcpListener, TcpStream};
use tokio::time::Sleep; use tokio::time::Sleep;
use tokio_stream::Stream; use tokio_stream::Stream;
use windows_sys::Win32::Foundation::{ 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::{ use windows_sys::Win32::NetworkManagement::IpHelper::{
GetExtendedTcpTable, MIB_TCPTABLE_OWNER_PID, TCP_TABLE_OWNER_PID_ALL, GetExtendedTcpTable, MIB_TCPTABLE_OWNER_PID, TCP_TABLE_OWNER_PID_ALL,
}; };
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW; use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows_sys::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER}; 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::{ use windows_sys::Win32::System::Threading::{
OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION, OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
}; };
const AF_INET: u32 = 2; 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 TABLE_ATTEMPTS: usize = 5;
const ACCEPT_RETRY_DELAY: Duration = Duration::from_secs(1); const ACCEPT_RETRY_DELAY: Duration = Duration::from_secs(1);
@@ -199,6 +206,61 @@ impl Stream for AuthorizedIncoming {
} }
} }
fn wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
struct PublishedPort;
impl PublishedPort {
fn publish(port: u16) -> IoResult<Self> {
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<F, S>( pub(super) async fn serve_until<F, S>(
owner_sid: String, owner_sid: String,
shutdown: F, shutdown: F,
@@ -210,9 +272,15 @@ where
{ {
ensure_core_sha256_configured()?; ensure_core_sha256_configured()?;
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, LISTEN_PORT)) let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
.await .await
.map_err(|error| anyhow::anyhow!("bind helper server: {error}"))?; .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()?; on_started()?;
let incoming = AuthorizedIncoming { let incoming = AuthorizedIncoming {
listener, listener,
@@ -222,6 +290,7 @@ where
warp::serve(routes()) warp::serve(routes())
.serve_incoming_with_graceful_shutdown(incoming, shutdown) .serve_incoming_with_graceful_shutdown(incoming, shutdown)
.await; .await;
drop(published);
release_managed_core_on_shutdown(); release_managed_core_on_shutdown();
Ok(()) Ok(())
+161
View File
@@ -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<String> 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']);
});
}
-2
View File
@@ -49,7 +49,6 @@ void main() {
const config = Config( const config = Config(
themeProps: defaultThemeProps, themeProps: defaultThemeProps,
currentProfileId: 42, currentProfileId: 42,
overrideDns: true,
excludeSSIDs: ['home'], excludeSSIDs: ['home'],
); );
@@ -58,7 +57,6 @@ void main() {
expect(restored, isNotNull); expect(restored, isNotNull);
expect(restored!.currentProfileId, 42); expect(restored!.currentProfileId, 42);
expect(restored.overrideDns, isTrue);
expect(restored.excludeSSIDs, ['home']); expect(restored.excludeSSIDs, ['home']);
}); });
+82 -6
View File
@@ -28,6 +28,8 @@ class _RecordedRun {
String get key => String get key =>
arguments.isEmpty ? executable : '$executable ${arguments.first}'; 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 /// Stands in for `Process.run`, keyed by executable plus its first argument so
@@ -47,6 +49,17 @@ class _FakeProcesses {
_failures.add(key); _failures.add(key);
} }
final List<String> inputs = [];
Future<ProcessResult> runWithInput(
String executable,
List<String> arguments,
String input,
) {
inputs.add(input);
return run(executable, arguments);
}
Future<ProcessResult> run(String executable, List<String> arguments) async { Future<ProcessResult> run(String executable, List<String> arguments) async {
final recorded = _RecordedRun(executable, arguments); final recorded = _RecordedRun(executable, arguments);
runs.add(recorded); runs.add(recorded);
@@ -55,8 +68,14 @@ class _FakeProcesses {
} }
return ProcessResult( return ProcessResult(
0, 0,
_exitCodes[recorded.key] ?? _exitCodes[executable] ?? 0, _exitCodes[recorded.commandLine] ??
_stdout[recorded.key] ?? _stdout[executable] ?? '', _exitCodes[recorded.key] ??
_exitCodes[executable] ??
0,
_stdout[recorded.commandLine] ??
_stdout[recorded.key] ??
_stdout[executable] ??
'',
'', '',
); );
} }
@@ -94,6 +113,7 @@ void main() {
late Directory root; late Directory root;
late _FakeProcesses processes; late _FakeProcesses processes;
String? password;
setUpAll(() { setUpAll(() {
root = Directory.systemTemp.createTempSync('system_test'); root = Directory.systemTemp.createTempSync('system_test');
@@ -112,13 +132,16 @@ void main() {
processes = _FakeProcesses(); processes = _FakeProcesses();
system.runProcess = processes.run; system.runProcess = processes.run;
MacOS().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(() { tearDown(() {
system.runProcess = Process.run; system.runProcess = Process.run;
MacOS().runProcess = Process.run; MacOS().runProcess = Process.run;
Linux().runProcess = Process.run; linuxElevation.run = Process.run;
}); });
group('statArguments', () { group('statArguments', () {
@@ -325,7 +348,10 @@ void main() {
test('Linux hands the Core to the requesting user\'s group', () { test('Linux hands the Core to the requesting user\'s group', () {
final shell = System.linuxElevationShell('/opt/FlClash/FlClashCore'); 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, contains(r'chown "root:$group" '));
expect(shell, endsWith("chmod 4750 '/opt/FlClash/FlClashCore'")); expect(shell, endsWith("chmod 4750 '/opt/FlClash/FlClashCore'"));
}); });
@@ -375,10 +401,60 @@ void main() {
expect(await Linux().installService(), isFalse); 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'); 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(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);
}); });
}); });
+13 -2
View File
@@ -326,11 +326,21 @@ void main() {
overrides: [ overrides: [
selectedProxyNameProvider('Proxy').overrideWithValue('A'), selectedProxyNameProvider('Proxy').overrideWithValue('A'),
trayDelaysProvider.overrideWithValue({ 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'); final submenu = _item(showCall(), 'Proxy');
expect(submenu['detail'], '120'); expect(submenu['detail'], '120');
@@ -338,6 +348,7 @@ void main() {
expect(children.map((item) => item['detail']), [ expect(children.map((item) => item['detail']), [
'120', '120',
currentAppLocalizations.timeout, currentAppLocalizations.timeout,
currentAppLocalizations.delayFailed,
null, null,
]); ]);
}, },
+117
View File
@@ -5,6 +5,7 @@ import 'package:fl_clash/common/window.dart';
import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/models/config.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:screen_retriever/screen_retriever.dart';
const _windowChannel = MethodChannel('window'); 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 { test('maximized geometry is not captured', () async {
isMaximized = true; isMaximized = true;
@@ -213,4 +297,37 @@ void main() {
isMinimized = true; isMinimized = true;
expect(await Window().captureNormalGeometry(const WindowProps()), isNull); 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',
);
});
} }
+2 -2
View File
@@ -127,7 +127,7 @@ final class FakeDesktopCoreTransport implements DesktopCoreTransport {
} }
@override @override
Future<void> send(String message) async { void send(List<int> frame) {
if (state == DesktopTransportState.closed) { if (state == DesktopTransportState.closed) {
throw StateError('IPC transport is closed'); throw StateError('IPC transport is closed');
} }
@@ -135,7 +135,7 @@ final class FakeDesktopCoreTransport implements DesktopCoreTransport {
if (error != null) { if (error != null) {
Error.throwWithStackTrace(error, StackTrace.current); Error.throwWithStackTrace(error, StackTrace.current);
} }
sentMessages.add(message); sentMessages.add(utf8.decode(frame));
} }
@override @override
@@ -16,6 +16,13 @@ const _coreSha256 =
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
void main() { 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( test(
'start returns a Helper lease identity with matching session and PID', 'start returns a Helper lease identity with matching session and PID',
() async { () async {
+100
View File
@@ -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<Socket> 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<TransportDisconnected>()
.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();
});
}
+209 -156
View File
@@ -4,42 +4,91 @@ import 'dart:typed_data';
import 'package:fl_clash/core/desktop/transport.dart'; import 'package:fl_clash/core/desktop/transport.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:rust_api/rust_api.dart';
import 'fakes.dart'; import 'fakes.dart';
Uint8List _frame(int type, [List<int> payload = const []]) { final class _FakeIpcServer implements IpcServer {
return Uint8List.fromList([type, ...payload]); final StreamController<IpcEvent> _events = StreamController<IpcEvent>();
} final List<List<int>> sent = [];
Object? sendError;
int closeCount = 0;
Uint8List _processIdPayload(int processId) { void _add(
return Uint8List(4) IpcEventKind kind, {
..buffer.asByteData().setUint32(0, processId, Endian.little); int? pid,
List<int> 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<int> 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<IpcEvent> events() => _events.stream;
@override
void send({required List<int> message}) {
final error = sendError;
if (error != null) {
Error.throwWithStackTrace(error, StackTrace.current);
}
sent.add(message);
}
@override
Future<void> close() async {
closeCount++;
end();
}
@override
void dispose() {}
@override
bool get isDisposed => false;
} }
void main() { void main() {
group('IPCCoreTransport typed events', () { group('IpcCoreTransport', () {
late StreamController<Uint8List> rawEvents; late _FakeIpcServer server;
late List<List<int>> sentMessages; late List<String> boundAddresses;
late int stopCount; late IpcCoreTransport transport;
late IPCCoreTransport transport;
setUp(() { setUp(() {
rawEvents = StreamController<Uint8List>(); server = _FakeIpcServer();
sentMessages = []; boundAddresses = [];
stopCount = 0; transport = IpcCoreTransport(
transport = IPCCoreTransport(
address: 'test-address', address: 'test-address',
startServer: (_) => rawEvents.stream, bind: (address) async {
sendMessage: (data) async => sentMessages.add(data), boundAddresses.add(address);
stopServer: () async => stopCount++, return server;
},
); );
}); });
tearDown(() async { tearDown(() async {
await transport.close(); await transport.close();
if (!rawEvents.isClosed) {
await rawEvents.close();
}
}); });
test( test(
@@ -47,99 +96,72 @@ void main() {
() async { () async {
final events = <DesktopTransportEvent>[]; final events = <DesktopTransportEvent>[];
final subscription = transport.events.listen(events.add); final subscription = transport.events.listen(events.add);
final open = transport.open(); await transport.open();
rawEvents.add(_frame(0x00)); expect(boundAddresses, ['test-address']);
await open; expect(transport.state, DesktopTransportState.ready);
final frame = transport.frames.first; final frame = transport.frames.first;
rawEvents.add(_frame(0x01, _processIdPayload(1234))); server.connect(1234);
rawEvents.add(_frame(0x03, utf8.encode('payload'))); server.message(utf8.encode('payload'));
expect(await frame, utf8.encode('payload')); expect(await frame, utf8.encode('payload'));
rawEvents.add(_frame(0x02)); expect(transport.state, DesktopTransportState.connected);
server.disconnect();
await pumpEventQueue(); await pumpEventQueue();
expect(events.whereType<TransportReady>(), hasLength(1)); expect(events.whereType<TransportReady>(), hasLength(1));
expect(events.whereType<TransportConnected>().single.pid, 1234); expect(events.whereType<TransportConnected>().single.pid, 1234);
expect(events.whereType<TransportConnected>().single.generation, 1); expect(events.whereType<TransportConnected>().single.generation, 1);
expect(events.whereType<TransportDisconnected>().single.generation, 1); final disconnected = events.whereType<TransportDisconnected>().single;
expect(disconnected.generation, 1);
expect(disconnected.error, isNull);
expect(transport.state, DesktopTransportState.ready); expect(transport.state, DesktopTransportState.ready);
await subscription.cancel(); await subscription.cancel();
}, },
); );
test('waitUntilConnected waits for a typed connected event', () async { test('opening twice binds once', () async {
final open = transport.open(); await Future.wait([transport.open(), transport.open()]);
rawEvents.add(_frame(0x00)); await transport.open();
await open;
expect(boundAddresses, hasLength(1));
});
test('waitUntilConnected waits for a connected event', () async {
await transport.open();
final connected = transport.waitUntilConnected( final connected = transport.waitUntilConnected(
const Duration(seconds: 1), const Duration(seconds: 1),
); );
rawEvents.add(_frame(0x01, _processIdPayload(4321))); server.connect(4321);
expect((await connected).pid, 4321); expect((await connected).pid, 4321);
expect(transport.state, DesktopTransportState.connected); expect(transport.state, DesktopTransportState.connected);
}); });
test('forwards outgoing messages after ready', () async { test('hands outgoing frames to the server', () async {
final open = transport.open(); await transport.open();
rawEvents.add(_frame(0x00));
await 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 { test('refuses to send before it is open and after it is closed', () async {
final failures = transport.events expect(() => transport.send(const [1]), throwsStateError);
.where((event) => event is TransportFailed)
.cast<TransportFailed>()
.first;
final open = transport.open();
rawEvents.add(_frame(0x04, utf8.encode('bind failed')));
await expectLater(open, throwsA(isA<StateError>())); await transport.open();
expect((await failures).error.toString(), contains('bind failed')); await transport.close();
expect(transport.state, DesktopTransportState.failed);
});
test('fails when the Rust event stream closes before ready', () async { expect(() => transport.send(const [1]), throwsStateError);
final open = transport.open(); expect(server.sent, isEmpty);
await rawEvents.close();
await expectLater(open, throwsA(isA<StateError>()));
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<TransportFailed>()
.first;
rawEvents.add(_frame(0x01, [0x01]));
expect((await failure).error.toString(), contains('Invalid IPC'));
expect(transport.state, DesktopTransportState.failed);
}); });
test('propagates send failures', () async { test('propagates send failures', () async {
final failingTransport = IPCCoreTransport( server.sendError = StateError('send failed');
address: 'test-address', await transport.open();
startServer: (_) => rawEvents.stream,
sendMessage: (_) async => throw StateError('send failed'),
stopServer: () async => stopCount++,
);
final open = failingTransport.open();
rawEvents.add(_frame(0x00));
await open;
await expectLater( expect(
failingTransport.send('hello'), () => transport.send(utf8.encode('hello')),
throwsA( throwsA(
isA<StateError>().having( isA<StateError>().having(
(error) => error.message, (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<TransportFailed>()
.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(); await failingTransport.close();
}); });
test('assigns a fresh generation after reconnecting', () async { test('a server failure is terminal for the transport', () async {
final open = transport.open(); await transport.open();
rawEvents.add(_frame(0x00)); server.connect();
await open; await pumpEventQueue();
final events = <DesktopTransportEvent>[];
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<TransportFailed>()
.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<TransportDisconnected>()
.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 = <TransportConnected>[]; final connections = <TransportConnected>[];
final subscription = transport.events.listen((event) { final subscription = transport.events.listen((event) {
if (event is TransportConnected) { if (event is TransportConnected) {
connections.add(event); connections.add(event);
} }
}); });
rawEvents.add(_frame(0x01));
rawEvents.add(_frame(0x02)); server.connect();
rawEvents.add(_frame(0x01, _processIdPayload(4321))); server.disconnect();
server.connect(4321);
await pumpEventQueue(); await pumpEventQueue();
expect(connections.map((event) => event.generation), [1, 2]); expect(connections.map((event) => event.generation), [1, 2]);
@@ -172,78 +259,47 @@ void main() {
await subscription.cancel(); await subscription.cancel();
}); });
test('a data frame hands out a view rather than a copy', () async { test('close is idempotent', () async {
final open = transport.open(); await transport.open();
rawEvents.add(_frame(0x00));
await open;
final frame = transport.frames.first; await transport.close();
final source = _frame(0x03, utf8.encode('payload')); await transport.close();
rawEvents.add(source);
final payload = await frame; expect(server.closeCount, 1);
expect(payload, utf8.encode('payload')); expect(transport.state, DesktopTransportState.closed);
expect(payload.offsetInBytes, 1); await expectLater(transport.open(), throwsStateError);
source[1] = 0x5a;
expect(payload[0], 0x5a);
}); });
test( test('closing while binding closes the server that arrives', () async {
'a recovered transport stops answering with the old failure', final bound = Completer<IpcServer>();
() async { final slowTransport = IpcCoreTransport(
final open = transport.open(); address: 'test-address',
rawEvents.add(_frame(0x00)); bind: (_) => bound.future,
await open; );
final open = slowTransport.open();
final rejected = expectLater(open, throwsStateError);
rawEvents.add(_frame(0x04, utf8.encode('boom'))); await slowTransport.close();
await pumpEventQueue(); bound.complete(server);
expect(transport.state, DesktopTransportState.failed);
await expectLater(
transport.waitUntilConnected(const Duration(seconds: 1)),
throwsA(isA<StateError>()),
);
rawEvents.add(_frame(0x00)); await rejected;
rawEvents.add(_frame(0x01, _processIdPayload(4321))); expect(server.closeCount, 1);
await pumpEventQueue(); expect(slowTransport.state, DesktopTransportState.closed);
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);
}); });
}); });
test( test(
'transport binding forwards events after replacing its transport', 'transport binding forwards events after replacing its transport',
() async { () async {
final firstEvents = StreamController<Uint8List>(); final firstServer = _FakeIpcServer();
final secondEvents = StreamController<Uint8List>(); final secondServer = _FakeIpcServer();
var firstStopCount = 0; final first = IpcCoreTransport(
final first = IPCCoreTransport(
address: 'first', address: 'first',
startServer: (_) => firstEvents.stream, bind: (_) async => firstServer,
sendMessage: (_) async {},
stopServer: () async => firstStopCount++,
); );
final second = IPCCoreTransport( final second = IpcCoreTransport(
address: 'second', address: 'second',
startServer: (_) => secondEvents.stream, bind: (_) async => secondServer,
sendMessage: (_) async {},
stopServer: () async {},
); );
final binding = DesktopCoreTransportBinding(first); final binding = DesktopCoreTransportBinding(first);
final connectedEvents = <TransportConnected>[]; final connectedEvents = <TransportConnected>[];
@@ -253,24 +309,21 @@ void main() {
} }
}); });
final firstOpen = binding.open(); await binding.open();
firstEvents.add(_frame(0x00));
await firstOpen;
await binding.replace(second); await binding.replace(second);
final secondOpen = binding.open(); await binding.open();
secondEvents.add(_frame(0x00)); secondServer.connect(5678);
await secondOpen;
secondEvents.add(_frame(0x01, _processIdPayload(5678)));
await pumpEventQueue(); 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(binding.address, 'second');
expect(connectedEvents.single.pid, 5678); expect(connectedEvents.single.pid, 5678);
await subscription.cancel(); await subscription.cancel();
await binding.close(); await binding.close();
await firstEvents.close();
await secondEvents.close();
}, },
); );
-1
View File
@@ -15,7 +15,6 @@ const _platformModules = [
'lib/common/window.dart', 'lib/common/window.dart',
'lib/common/launch.dart', 'lib/common/launch.dart',
'lib/common/system_dns.dart', 'lib/common/system_dns.dart',
'lib/common/permission.dart',
]; ];
final _platformImport = RegExp( final _platformImport = RegExp(
+27 -2
View File
@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fl_clash/common/app_localizations.dart'; import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/app_ports.dart'; import 'package:fl_clash/common/app_ports.dart';
@@ -142,11 +143,19 @@ void main() {
container.dispose(); container.dispose();
}); });
Future<WindowListener> pumpWindowManager(WidgetTester tester) async { Future<WindowListener> pumpWindowManager(
WidgetTester tester, {
Stream<ProcessSignal>? terminateSignals,
}) async {
await tester.pumpWidget( await tester.pumpWidget(
UncontrolledProviderScope( UncontrolledProviderScope(
container: container, container: container,
child: const MaterialApp(home: WindowManager(child: SizedBox.shrink())), child: MaterialApp(
home: WindowManager(
terminateSignals: terminateSignals,
child: const SizedBox.shrink(),
),
),
), ),
); );
await tester.pump(); await tester.pump();
@@ -186,6 +195,22 @@ void main() {
expect(_RecordingSystemAction.calls, ['exit']); expect(_RecordingSystemAction.calls, ['exit']);
}); });
testWidgets('a terminate signal exits like a terminate request', (
tester,
) async {
final signals = StreamController<ProcessSignal>();
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 { testWidgets('moving the window records its new position', (tester) async {
final listener = await pumpWindowManager(tester); final listener = await pumpWindowManager(tester);
window.bounds = const Rect.fromLTWH(120, 64, 1000, 800); window.bounds = const Rect.fromLTWH(120, 64, 1000, 800);
+18
View File
@@ -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', () { test('rpm keeps the Core bytes the Helper was built against', () {
final config = final config =
loadYaml( loadYaml(
+58
View File
@@ -45,6 +45,47 @@ begin
end; end;
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; function PrepareToInstall(var NeedsRestart: Boolean): String;
begin begin
UnregisterHelperService; UnregisterHelperService;
@@ -52,13 +93,30 @@ begin
Result := ''; Result := '';
end; end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
ClearStaleProxy;
end;
end;
function InitializeUninstall(): Boolean; function InitializeUninstall(): Boolean;
begin begin
UnregisterHelperService; UnregisterHelperService;
KillProcesses; KillProcesses;
ClearStaleProxy;
Result := True; Result := True;
end; end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usPostUninstall then
begin
RemoveUserRegistrations;
end;
end;
[Languages] [Languages]
{% for locale in LOCALES %} {% for locale in LOCALES %}
{% if locale.lang == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %} {% if locale.lang == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %}
+14 -3
View File
@@ -3,13 +3,27 @@
#include <windows.h> #include <windows.h>
#include <app_links/app_links_plugin_c_api.h> #include <app_links/app_links_plugin_c_api.h>
#include <proxy/proxy_plugin_c_api.h>
#include <window/window_plugin_c_api.h> #include <window/window_plugin_c_api.h>
#include <algorithm>
#include "flutter_window.h" #include "flutter_window.h"
#include "utils.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, int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) { _In_ wchar_t *command_line, _In_ int show_command) {
std::vector<std::string> 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()) { if (HWND running = WindowPluginFindRunningWindow()) {
SendAppLink(running); SendAppLink(running);
WindowPluginActivateWindow(running); WindowPluginActivateWindow(running);
@@ -28,9 +42,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
flutter::DartProject project(L"data"); flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project); FlutterWindow window(project);