fix(desktop): fix the system proxy, tray and Helper on Windows and Linux
Changelog: Windows and Linux desktop fixes
This commit is contained in:
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
-49
@@ -106,7 +106,8 @@ class System {
|
||||
@visibleForTesting
|
||||
static String linuxElevationShell(String corePath) {
|
||||
final path = _shellEscape(corePath);
|
||||
return 'group=\$(id -g "\$PKEXEC_UID") && chown "root:\$group" $path && '
|
||||
return 'group=\$(id -g "\${PKEXEC_UID:-\$SUDO_UID}") && '
|
||||
'chown "root:\$group" $path && '
|
||||
'chmod 4750 $path';
|
||||
}
|
||||
|
||||
@@ -248,28 +249,12 @@ class System {
|
||||
}
|
||||
return AuthorizeCode.success;
|
||||
} else if (system.isLinux) {
|
||||
final ProcessResult result;
|
||||
try {
|
||||
result = await runProcess('pkexec', [
|
||||
final elevated = await linuxElevation.elevate([
|
||||
'/bin/sh',
|
||||
'-c',
|
||||
linuxElevationShell(appPath.corePath),
|
||||
]);
|
||||
} on ProcessException catch (error) {
|
||||
commonPrint.log(
|
||||
'pkexec is unavailable: ${compactError(error)}',
|
||||
logLevel: LogLevel.error,
|
||||
);
|
||||
return AuthorizeCode.error;
|
||||
}
|
||||
if (result.exitCode != 0) {
|
||||
commonPrint.log(
|
||||
'pkexec refused to elevate the Core: ${result.exitCode}',
|
||||
logLevel: LogLevel.error,
|
||||
);
|
||||
return AuthorizeCode.error;
|
||||
}
|
||||
return AuthorizeCode.success;
|
||||
return elevated ? AuthorizeCode.success : AuthorizeCode.error;
|
||||
}
|
||||
return AuthorizeCode.error;
|
||||
}
|
||||
@@ -289,6 +274,8 @@ final system = System();
|
||||
|
||||
class Windows {
|
||||
static Windows? _instance;
|
||||
static const _swHide = 0;
|
||||
static const _swShowNormal = 1;
|
||||
late DynamicLibrary _shell32;
|
||||
|
||||
Windows._internal() {
|
||||
@@ -300,7 +287,7 @@ class Windows {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
bool runas(String command, String arguments) {
|
||||
bool runas(String command, String arguments, {bool showWindow = true}) {
|
||||
final commandPtr = command.toNativeUtf16();
|
||||
final argumentsPtr = arguments.toNativeUtf16();
|
||||
final operationPtr = 'runas'.toNativeUtf16();
|
||||
@@ -331,7 +318,7 @@ class Windows {
|
||||
commandPtr,
|
||||
argumentsPtr,
|
||||
nullptr,
|
||||
1,
|
||||
showWindow ? _swShowNormal : _swHide,
|
||||
);
|
||||
|
||||
calloc.free(commandPtr);
|
||||
@@ -356,7 +343,8 @@ class Windows {
|
||||
|
||||
Future<AuthorizeCode> registerService() {
|
||||
return registerHelperService(
|
||||
() async => runas(appPath.helperPath, installArguments(pid)),
|
||||
() async =>
|
||||
runas(appPath.helperPath, installArguments(pid), showWindow: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -433,9 +421,6 @@ final windows = system.isWindows ? Windows() : null;
|
||||
class Linux {
|
||||
static Linux? _instance;
|
||||
|
||||
@visibleForTesting
|
||||
ProcessRunner runProcess = Process.run;
|
||||
|
||||
Linux._internal();
|
||||
|
||||
factory Linux() {
|
||||
@@ -447,31 +432,11 @@ class Linux {
|
||||
return registerHelperService(installService);
|
||||
}
|
||||
|
||||
/// pkexec raises the system polkit prompt and names the requesting user to
|
||||
/// the installer, which is where the unit takes the account it grants the
|
||||
/// Helper socket to.
|
||||
/// pkexec and sudo both name the requesting user to the installer, which is
|
||||
/// where the unit takes the account it grants the Helper socket to.
|
||||
@visibleForTesting
|
||||
Future<bool> installService() async {
|
||||
try {
|
||||
final result = await runProcess('pkexec', [
|
||||
appPath.helperPath,
|
||||
'install',
|
||||
]);
|
||||
if (result.exitCode == 0) {
|
||||
return true;
|
||||
}
|
||||
commonPrint.log(
|
||||
'pkexec helper install exited with ${result.exitCode}: '
|
||||
'${result.stderr.toString().trim()}',
|
||||
logLevel: LogLevel.error,
|
||||
);
|
||||
} catch (error) {
|
||||
commonPrint.log(
|
||||
'pkexec is unavailable: ${compactError(error)}',
|
||||
logLevel: LogLevel.error,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
Future<bool> installService() {
|
||||
return linuxElevation.elevate([appPath.helperPath, 'install']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,9 @@ class AppTray implements TrayPort {
|
||||
if (delay == null) {
|
||||
return null;
|
||||
}
|
||||
return delay > 0 ? '$delay' : currentAppLocalizations.timeout;
|
||||
return delay > 0
|
||||
? '$delay'
|
||||
: delayFailureText(delay, currentAppLocalizations);
|
||||
}
|
||||
|
||||
List<TrayMenuItem> _buildGroupMenu({
|
||||
|
||||
+67
-36
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:screen_retriever/screen_retriever.dart';
|
||||
import 'package:window/window.dart';
|
||||
@@ -31,11 +32,6 @@ class Window implements WindowPort {
|
||||
}
|
||||
|
||||
Future<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 (system.isWindows) {
|
||||
for (final scheme in protocolSchemes) {
|
||||
@@ -61,38 +57,25 @@ class Window implements WindowPort {
|
||||
}
|
||||
|
||||
Future<void> _windowPosition(WindowProps props) async {
|
||||
if (_supportsPosition) {
|
||||
final left = props.left;
|
||||
final top = props.top;
|
||||
if (left == null || top == null) {
|
||||
await desktopWindow.setAlignment(Alignment.center);
|
||||
} else {
|
||||
final size = props.size;
|
||||
final right = left + size.width;
|
||||
final bottom = top + size.height;
|
||||
final displays = await screenRetriever.getAllDisplays();
|
||||
final isPositionValid = displays.any((display) {
|
||||
final visiblePosition = display.visiblePosition;
|
||||
if (visiblePosition == null) {
|
||||
return false;
|
||||
if (!_supportsPosition) {
|
||||
return;
|
||||
}
|
||||
final displayBounds = Rect.fromLTWH(
|
||||
visiblePosition.dx,
|
||||
visiblePosition.dy,
|
||||
display.size.width,
|
||||
display.size.height,
|
||||
final position = props.left == null || props.top == null
|
||||
? null
|
||||
: restoredWindowPosition(
|
||||
props,
|
||||
displays: await screenRetriever.getAllDisplays(),
|
||||
currentScale: system.isWindows ? _windowScale : null,
|
||||
);
|
||||
return displayBounds.contains(Offset(left, top)) ||
|
||||
displayBounds.contains(Offset(right, bottom));
|
||||
});
|
||||
if (isPositionValid) {
|
||||
await desktopWindow.setPosition(Offset(left, top));
|
||||
} else {
|
||||
if (position == null) {
|
||||
await desktopWindow.setAlignment(Alignment.center);
|
||||
} else {
|
||||
await desktopWindow.setPosition(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double get _windowScale =>
|
||||
PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 1;
|
||||
|
||||
@override
|
||||
Future<WindowProps?> captureNormalGeometry(WindowProps current) async {
|
||||
@@ -119,6 +102,9 @@ class Window implements WindowPort {
|
||||
height: bounds.height,
|
||||
left: hasValidPosition ? bounds.left : current.left,
|
||||
top: hasValidPosition ? bounds.top : current.top,
|
||||
scale: hasValidPosition && system.isWindows
|
||||
? _windowScale
|
||||
: current.scale,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,9 +149,12 @@ class Window implements WindowPort {
|
||||
|
||||
/// Every desktop runner leaves the window hidden until [init] reveals it, so
|
||||
/// a failure before that point would leave the error screen with no window.
|
||||
Future<void> showInitFailure() async {
|
||||
Future<void> showInitFailure({required AsyncCallback onExit}) async {
|
||||
desktopWindow.addListener(_InitFailureWindowListener(onExit));
|
||||
try {
|
||||
await desktopWindow.ensureInitialized();
|
||||
await desktopWindow.setPreventClose(true);
|
||||
await desktopWindow.setTitleBarStyle(TitleBarStyle.normal);
|
||||
if (await desktopWindow.isVisible()) {
|
||||
return;
|
||||
}
|
||||
@@ -200,9 +189,7 @@ class Window implements WindowPort {
|
||||
}
|
||||
|
||||
Future<bool> _isWindowVisible() async {
|
||||
final value = await desktopWindow.isVisible();
|
||||
commonPrint.log('window visible check: $value');
|
||||
return value;
|
||||
return desktopWindow.isVisible();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -216,6 +203,50 @@ class Window implements WindowPort {
|
||||
}
|
||||
}
|
||||
|
||||
/// On Windows the plugin scales by the window's monitor and screen_retriever by
|
||||
/// each display's own, so only physical pixels compare across mixed scales.
|
||||
@visibleForTesting
|
||||
Offset? restoredWindowPosition(
|
||||
WindowProps props, {
|
||||
required List<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
|
||||
/// order, and holds back the Dock-hiding activation policy switch while a
|
||||
/// preceding regular switch settles: flipping regular → accessory → regular
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/common/print.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:win32_registry/win32_registry.dart';
|
||||
|
||||
import 'core_manifest.dart';
|
||||
import 'launcher.dart';
|
||||
@@ -54,7 +55,7 @@ final class HelperClient {
|
||||
final Dio _dio;
|
||||
final String Function() _expectedHelperPath;
|
||||
final Future<String> Function() _readCoreSha256;
|
||||
final String baseUrl;
|
||||
final String Function() _baseUrl;
|
||||
String? _coreSha256Cache;
|
||||
|
||||
HelperClient({
|
||||
@@ -63,7 +64,7 @@ final class HelperClient {
|
||||
Future<String> Function()? readCoreSha256,
|
||||
String? baseUrl,
|
||||
}) : _dio = dio ?? _createDio(),
|
||||
baseUrl = baseUrl ?? _defaultBaseUrl(),
|
||||
_baseUrl = baseUrl == null ? _defaultBaseUrl : (() => baseUrl),
|
||||
_expectedHelperPath = expectedHelperPath ?? _defaultHelperPath,
|
||||
_readCoreSha256 = readCoreSha256 ?? _readBundledCoreSha256;
|
||||
|
||||
@@ -111,10 +112,29 @@ final class HelperClient {
|
||||
);
|
||||
}
|
||||
|
||||
String get baseUrl => _baseUrl();
|
||||
|
||||
static String _defaultBaseUrl() {
|
||||
return Platform.isLinux
|
||||
? 'http://$appHelperService'
|
||||
: 'http://$localhost:$helperPort';
|
||||
: 'http://$localhost:${_readHelperPort()}';
|
||||
}
|
||||
|
||||
/// Helpers installed before the port was published listen on [helperPort].
|
||||
static int _readHelperPort() {
|
||||
if (!Platform.isWindows) {
|
||||
return helperPort;
|
||||
}
|
||||
try {
|
||||
final key = LOCAL_MACHINE.open(helperPortKey);
|
||||
try {
|
||||
return key.getInt(helperPortValue) ?? helperPort;
|
||||
} finally {
|
||||
key.close();
|
||||
}
|
||||
} catch (_) {
|
||||
return helperPort;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> _readBundledCoreSha256() async {
|
||||
|
||||
@@ -710,11 +710,12 @@ final class DesktopCoreLifecycle implements DesktopCoreLifecycleController {
|
||||
return;
|
||||
}
|
||||
switch (event) {
|
||||
case TransportDisconnected(:final generation)
|
||||
case TransportDisconnected(:final generation, :final error)
|
||||
when generation == running.session.connectionGeneration:
|
||||
_handleUnexpectedDisconnect(
|
||||
running.session,
|
||||
code: 'unexpected_disconnect',
|
||||
cause: error,
|
||||
);
|
||||
case TransportFailed(:final error, :final stackTrace):
|
||||
_handleUnexpectedDisconnect(
|
||||
|
||||
@@ -18,6 +18,9 @@ const _maxExtensionGrace = Duration(seconds: 30);
|
||||
|
||||
const _connectTimeout = Duration(seconds: 10);
|
||||
|
||||
final _requestEncoder = JsonUtf8Encoder();
|
||||
final _frameDecoder = utf8.decoder.fuse(json.decoder);
|
||||
|
||||
abstract interface class CoreRpcChannel {
|
||||
Future<T?> invoke<T>({
|
||||
required CoreMethod method,
|
||||
@@ -83,17 +86,14 @@ final class CoreRpcClient implements CoreRpcChannel {
|
||||
if (completer.isCompleted) {
|
||||
return await completer.future as T?;
|
||||
}
|
||||
final sendTimeout = requestTimeout - stopwatch.elapsed;
|
||||
if (sendTimeout <= Duration.zero) {
|
||||
if (stopwatch.elapsed >= requestTimeout) {
|
||||
throw TimeoutException('Core method ${method.name} timed out');
|
||||
}
|
||||
await transport
|
||||
.send(
|
||||
json.encode(
|
||||
transport.send(
|
||||
_requestEncoder.convert(
|
||||
CoreMethodCall(id: id, method: method, arguments: arguments),
|
||||
),
|
||||
)
|
||||
.timeout(sendTimeout);
|
||||
);
|
||||
return await _awaitResponse(
|
||||
method: method,
|
||||
completer: completer,
|
||||
@@ -170,7 +170,7 @@ final class CoreRpcClient implements CoreRpcChannel {
|
||||
|
||||
void _handleFrame(Uint8List frame) {
|
||||
try {
|
||||
final decoded = json.decode(utf8.decode(frame));
|
||||
final decoded = _frameDecoder.convert(frame);
|
||||
if (decoded is! Map) {
|
||||
throw const FormatException('Core transport data is not an object');
|
||||
}
|
||||
|
||||
+90
-150
@@ -1,20 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:rust_api/rust_api.dart';
|
||||
|
||||
const _typeReady = 0x00;
|
||||
const _typeConnected = 0x01;
|
||||
const _typeDisconnected = 0x02;
|
||||
const _typeData = 0x03;
|
||||
const _typeError = 0x04;
|
||||
|
||||
typedef IpcServerStarter = Stream<Uint8List> Function(String address);
|
||||
typedef IpcMessageSender = Future<void> Function(List<int> data);
|
||||
typedef IpcServerStopper = Future<void> Function();
|
||||
typedef IpcServerBinder = Future<IpcServer> Function(String address);
|
||||
|
||||
enum DesktopTransportState { idle, starting, ready, connected, failed, closed }
|
||||
|
||||
@@ -35,8 +26,9 @@ final class TransportConnected extends DesktopTransportEvent {
|
||||
|
||||
final class TransportDisconnected extends DesktopTransportEvent {
|
||||
final int generation;
|
||||
final Object? error;
|
||||
|
||||
const TransportDisconnected(this.generation);
|
||||
const TransportDisconnected(this.generation, {this.error});
|
||||
}
|
||||
|
||||
final class TransportFailed extends DesktopTransportEvent {
|
||||
@@ -59,7 +51,7 @@ abstract interface class DesktopCoreTransport {
|
||||
|
||||
Future<TransportConnected> waitUntilConnected(Duration timeout);
|
||||
|
||||
Future<void> send(String message);
|
||||
void send(List<int> frame);
|
||||
|
||||
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
|
||||
String get address => _transport.address;
|
||||
|
||||
@@ -107,8 +106,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
|
||||
throw StateError('IPC transport binding is closed');
|
||||
}
|
||||
final previous = _transport;
|
||||
if (previous.state == DesktopTransportState.connected &&
|
||||
!_eventController.isClosed) {
|
||||
if (previous.state == DesktopTransportState.connected) {
|
||||
_eventController.add(
|
||||
TransportFailed(
|
||||
StateError('Connected IPC transport was replaced'),
|
||||
@@ -116,22 +114,12 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
|
||||
),
|
||||
);
|
||||
}
|
||||
await _eventSubscription?.cancel();
|
||||
await _frameSubscription?.cancel();
|
||||
_eventSubscription = null;
|
||||
_frameSubscription = null;
|
||||
Object? closeError;
|
||||
StackTrace? closeStackTrace;
|
||||
await _unbind();
|
||||
try {
|
||||
await previous.close();
|
||||
} catch (error, stackTrace) {
|
||||
closeError = error;
|
||||
closeStackTrace = stackTrace;
|
||||
}
|
||||
} finally {
|
||||
_transport = next;
|
||||
_bind(next);
|
||||
if (closeError != null) {
|
||||
Error.throwWithStackTrace(closeError, closeStackTrace!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +132,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send(String message) => _transport.send(message);
|
||||
void send(List<int> frame) => _transport.send(frame);
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
@@ -152,10 +140,7 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
|
||||
}
|
||||
|
||||
Future<void> _close() async {
|
||||
await _eventSubscription?.cancel();
|
||||
await _frameSubscription?.cancel();
|
||||
_eventSubscription = null;
|
||||
_frameSubscription = null;
|
||||
await _unbind();
|
||||
try {
|
||||
await _transport.close();
|
||||
} finally {
|
||||
@@ -165,21 +150,18 @@ final class DesktopCoreTransportBinding implements DesktopCoreTransport {
|
||||
}
|
||||
}
|
||||
|
||||
final class IPCCoreTransport implements DesktopCoreTransport {
|
||||
final class IpcCoreTransport implements DesktopCoreTransport {
|
||||
@override
|
||||
final String address;
|
||||
|
||||
final Duration readyTimeout;
|
||||
final IpcServerStarter _startServer;
|
||||
final IpcMessageSender _sendMessage;
|
||||
final IpcServerStopper _stopServer;
|
||||
|
||||
final IpcServerBinder _bind;
|
||||
final StreamController<DesktopTransportEvent> _eventController =
|
||||
StreamController<DesktopTransportEvent>.broadcast();
|
||||
final StreamController<Uint8List> _frameController =
|
||||
StreamController<Uint8List>.broadcast();
|
||||
|
||||
StreamSubscription<Uint8List>? _subscription;
|
||||
IpcServer? _server;
|
||||
StreamSubscription<IpcEvent>? _subscription;
|
||||
Future<void>? _openOperation;
|
||||
Future<void>? _closeOperation;
|
||||
DesktopTransportState _state = DesktopTransportState.idle;
|
||||
@@ -187,22 +169,11 @@ final class IPCCoreTransport implements DesktopCoreTransport {
|
||||
TransportFailed? _failure;
|
||||
int _connectionGeneration = 0;
|
||||
|
||||
IPCCoreTransport({
|
||||
required this.address,
|
||||
this.readyTimeout = const Duration(seconds: 10),
|
||||
IpcServerStarter? startServer,
|
||||
IpcMessageSender? sendMessage,
|
||||
IpcServerStopper? stopServer,
|
||||
}) : _startServer = startServer ?? _restartIpcServer,
|
||||
_sendMessage = sendMessage ?? _sendIpcMessage,
|
||||
_stopServer = stopServer ?? stopIpcServer;
|
||||
IpcCoreTransport({required this.address, IpcServerBinder? bind})
|
||||
: _bind = bind ?? _bindIpcServer;
|
||||
|
||||
static Stream<Uint8List> _restartIpcServer(String address) {
|
||||
return restartIpcServer(name: address);
|
||||
}
|
||||
|
||||
static Future<void> _sendIpcMessage(List<int> data) {
|
||||
return sendIpcMessage(data: data);
|
||||
static Future<IpcServer> _bindIpcServer(String address) {
|
||||
return IpcServer.bind(address: address);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -216,134 +187,103 @@ final class IPCCoreTransport implements DesktopCoreTransport {
|
||||
|
||||
@override
|
||||
Future<void> open() {
|
||||
if (_state == DesktopTransportState.ready ||
|
||||
_state == DesktopTransportState.connected) {
|
||||
return Future.value();
|
||||
}
|
||||
if (_state == DesktopTransportState.closed) {
|
||||
return Future.error(StateError('IPC transport is closed'));
|
||||
}
|
||||
final failure = _failure;
|
||||
if (failure != null) {
|
||||
return Future.error(failure.error, failure.stackTrace);
|
||||
}
|
||||
return _openOperation ??= _open();
|
||||
}
|
||||
|
||||
Future<void> _open() async {
|
||||
_state = DesktopTransportState.starting;
|
||||
final readiness = events.firstWhere(
|
||||
(event) => event is TransportReady || event is TransportFailed,
|
||||
);
|
||||
try {
|
||||
_subscription = _startServer(address).listen(
|
||||
_handleFrame,
|
||||
onError: _handleStreamError,
|
||||
onDone: _handleStreamDone,
|
||||
cancelOnError: false,
|
||||
final server = await _bind(address);
|
||||
if (_state == DesktopTransportState.closed) {
|
||||
await server.close();
|
||||
throw StateError('IPC transport is closed');
|
||||
}
|
||||
_server = server;
|
||||
_state = DesktopTransportState.ready;
|
||||
commonPrint.log('IPC Ready');
|
||||
_eventController.add(const TransportReady());
|
||||
_subscription = server.events().listen(
|
||||
_handleEvent,
|
||||
onError: _fail,
|
||||
onDone: _handleDone,
|
||||
);
|
||||
final event = await readiness.timeout(readyTimeout);
|
||||
if (event case TransportFailed(:final error, :final stackTrace)) {
|
||||
Error.throwWithStackTrace(error, stackTrace);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
if (_state != DesktopTransportState.failed &&
|
||||
_state != DesktopTransportState.closed) {
|
||||
_fail(error, stackTrace);
|
||||
}
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleFrame(Uint8List data) {
|
||||
if (data.isEmpty || _state == DesktopTransportState.closed) {
|
||||
bool get _isTerminal =>
|
||||
_state == DesktopTransportState.failed ||
|
||||
_state == DesktopTransportState.closed;
|
||||
|
||||
void _handleEvent(IpcEvent event) {
|
||||
if (_isTerminal) {
|
||||
return;
|
||||
}
|
||||
final type = data[0];
|
||||
final payload = data.length > 1
|
||||
? Uint8List.sublistView(data, 1)
|
||||
: Uint8List(0);
|
||||
switch (type) {
|
||||
case _typeReady:
|
||||
commonPrint.log('IPC Ready');
|
||||
_failure = null;
|
||||
_state = DesktopTransportState.ready;
|
||||
_eventController.add(const TransportReady());
|
||||
case _typeConnected:
|
||||
final processId = _decodeConnectedProcessId(payload);
|
||||
if (payload.isNotEmpty && processId == null) {
|
||||
_fail(StateError('Invalid IPC connected frame'), StackTrace.current);
|
||||
return;
|
||||
switch (event.kind) {
|
||||
case IpcEventKind.connected:
|
||||
_connect(event.pid);
|
||||
case IpcEventKind.message:
|
||||
_frameController.add(event.payload);
|
||||
case IpcEventKind.disconnected:
|
||||
_disconnect(event.error);
|
||||
case IpcEventKind.failed:
|
||||
_fail(StateError('IPC error: ${event.error}'), StackTrace.current);
|
||||
}
|
||||
commonPrint.log(
|
||||
'IPC Connected${processId == null ? '' : ': $processId'}',
|
||||
);
|
||||
_failure = null;
|
||||
_connectionGeneration++;
|
||||
_connection = TransportConnected(
|
||||
pid: processId,
|
||||
generation: _connectionGeneration,
|
||||
}
|
||||
|
||||
void _connect(int? pid) {
|
||||
commonPrint.log('IPC Connected${pid == null ? '' : ': $pid'}');
|
||||
final connection = TransportConnected(
|
||||
pid: pid,
|
||||
generation: ++_connectionGeneration,
|
||||
);
|
||||
_connection = connection;
|
||||
_state = DesktopTransportState.connected;
|
||||
_eventController.add(_connection!);
|
||||
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,
|
||||
);
|
||||
}
|
||||
_eventController.add(connection);
|
||||
}
|
||||
|
||||
int? _decodeConnectedProcessId(Uint8List payload) {
|
||||
if (payload.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (payload.length != Uint32List.bytesPerElement) {
|
||||
return null;
|
||||
}
|
||||
final processId = ByteData.sublistView(payload).getUint32(0, Endian.little);
|
||||
return processId == 0 ? null : processId;
|
||||
}
|
||||
|
||||
void _disconnect() {
|
||||
void _disconnect(String? error) {
|
||||
final connection = _connection;
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
commonPrint.log('IPC Disconnected');
|
||||
commonPrint.log(
|
||||
'IPC Disconnected${error == null ? '' : ': $error'}',
|
||||
logLevel: error == null ? LogLevel.info : LogLevel.warning,
|
||||
);
|
||||
_connection = null;
|
||||
_state = DesktopTransportState.ready;
|
||||
_eventController.add(TransportDisconnected(connection.generation));
|
||||
_eventController.add(
|
||||
TransportDisconnected(
|
||||
connection.generation,
|
||||
error: error == null ? null : StateError('IPC error: $error'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleStreamError(Object error, StackTrace stackTrace) {
|
||||
_fail(error, stackTrace);
|
||||
}
|
||||
|
||||
void _handleStreamDone() {
|
||||
if (_state == DesktopTransportState.closed) {
|
||||
return;
|
||||
}
|
||||
_disconnect();
|
||||
void _handleDone() {
|
||||
_fail(StateError('IPC server stopped unexpectedly'), StackTrace.current);
|
||||
}
|
||||
|
||||
void _fail(Object error, StackTrace stackTrace) {
|
||||
if (_state == DesktopTransportState.failed ||
|
||||
_state == DesktopTransportState.closed) {
|
||||
if (_isTerminal) {
|
||||
return;
|
||||
}
|
||||
commonPrint.log('IPC error: $error', logLevel: LogLevel.debug);
|
||||
final failure = TransportFailed(error, stackTrace);
|
||||
_connection = null;
|
||||
_failure = failure;
|
||||
_state = DesktopTransportState.failed;
|
||||
_failure = TransportFailed(error, stackTrace);
|
||||
_eventController.add(_failure!);
|
||||
_eventController.add(failure);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -371,11 +311,15 @@ final class IPCCoreTransport implements DesktopCoreTransport {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send(String message) {
|
||||
void send(List<int> frame) {
|
||||
if (_state == DesktopTransportState.closed) {
|
||||
return Future.error(StateError('IPC transport is closed'));
|
||||
throw StateError('IPC transport is closed');
|
||||
}
|
||||
return _sendMessage(utf8.encode(message));
|
||||
final server = _server;
|
||||
if (server == null) {
|
||||
throw StateError('IPC transport is not open');
|
||||
}
|
||||
server.send(message: frame);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -384,16 +328,12 @@ final class IPCCoreTransport implements DesktopCoreTransport {
|
||||
}
|
||||
|
||||
Future<void> _close() async {
|
||||
if (_state == DesktopTransportState.closed) {
|
||||
return;
|
||||
}
|
||||
_state = DesktopTransportState.closed;
|
||||
_connection = null;
|
||||
try {
|
||||
await _stopServer();
|
||||
await _server?.close();
|
||||
} finally {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
await _eventController.close();
|
||||
await _frameController.close();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/launch.dart';
|
||||
@@ -16,7 +17,11 @@ const _windowGeometryDelay = Duration(milliseconds: 120);
|
||||
class WindowManager extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const WindowManager({super.key, required this.child});
|
||||
/// A request to quit from outside the window, such as SIGTERM from `kill`
|
||||
/// or the session ending; exits like the tray's quit.
|
||||
final Stream<ProcessSignal>? terminateSignals;
|
||||
|
||||
const WindowManager({super.key, this.terminateSignals, required this.child});
|
||||
|
||||
@override
|
||||
ConsumerState<WindowManager> createState() => _WindowContainerState();
|
||||
@@ -25,6 +30,7 @@ class WindowManager extends ConsumerStatefulWidget {
|
||||
class _WindowContainerState extends ConsumerState<WindowManager>
|
||||
with WindowListener {
|
||||
Timer? _windowGeometryTimer;
|
||||
StreamSubscription<ProcessSignal>? _terminateSubscription;
|
||||
int _windowGeometryRevision = 0;
|
||||
int _windowBlurRevision = 0;
|
||||
|
||||
@@ -52,6 +58,9 @@ class _WindowContainerState extends ConsumerState<WindowManager>
|
||||
}
|
||||
}, fireImmediately: true);
|
||||
desktopWindow.addListener(this);
|
||||
_terminateSubscription = widget.terminateSignals?.listen(
|
||||
(_) => unawaited(onWindowShouldTerminate()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _applyWindowBlur(WindowBlurRequest request) async {
|
||||
@@ -86,12 +95,6 @@ class _WindowContainerState extends ConsumerState<WindowManager>
|
||||
super.onWindowClose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
super.onWindowFocus();
|
||||
commonPrint.log('focus');
|
||||
}
|
||||
|
||||
/// Another launch, or a Dock reopen, asked for the window.
|
||||
@override
|
||||
void onWindowActivate() {
|
||||
@@ -182,13 +185,11 @@ class _WindowContainerState extends ConsumerState<WindowManager>
|
||||
void onWindowMinimize() async {
|
||||
_invalidateWindowGeometryCapture();
|
||||
ref.read(storeActionProvider.notifier).savePreferencesDebounce();
|
||||
commonPrint.log('minimize');
|
||||
super.onWindowMinimize();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowRestore() {
|
||||
commonPrint.log('restore');
|
||||
super.onWindowRestore();
|
||||
_scheduleWindowGeometryCapture();
|
||||
}
|
||||
@@ -196,6 +197,7 @@ class _WindowContainerState extends ConsumerState<WindowManager>
|
||||
@override
|
||||
void dispose() {
|
||||
_invalidateWindowGeometryCapture();
|
||||
unawaited(_terminateSubscription?.cancel());
|
||||
desktopWindow.removeListener(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
@@ -20,4 +20,6 @@ categories:
|
||||
|
||||
startup_notify: true
|
||||
|
||||
startup_wm_class: com.follow.clash
|
||||
|
||||
include: []
|
||||
@@ -39,3 +39,5 @@ categories:
|
||||
- Network
|
||||
|
||||
startup_notify: true
|
||||
|
||||
startup_wm_class: com.follow.clash
|
||||
|
||||
@@ -46,3 +46,5 @@ generic_name: FlClash
|
||||
group: Applications/Internet
|
||||
|
||||
startup_notify: true
|
||||
|
||||
startup_wm_class: com.follow.clash
|
||||
|
||||
@@ -22,5 +22,6 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::X11)
|
||||
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
// libX11 enables its own locking only from 1.8 on; on older releases the
|
||||
// tray indicator reaching X11 from another thread aborts the process.
|
||||
XInitThreads();
|
||||
#endif
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,21 @@ static void my_application_activate(GApplication* application) {
|
||||
|
||||
gtk_window_set_default_size(window, 680, 580);
|
||||
|
||||
// X11 panels that do not match the window to a desktop entry read its icon;
|
||||
// GDK leaves one over 512x512 out of _NET_WM_ICON without a word.
|
||||
g_autofree gchar* executable = g_file_read_link("/proc/self/exe", nullptr);
|
||||
if (executable != nullptr) {
|
||||
g_autofree gchar* directory = g_path_get_dirname(executable);
|
||||
g_autofree gchar* path = g_build_filename(
|
||||
directory, "data", "flutter_assets", "assets", "images", "icon.png",
|
||||
nullptr);
|
||||
g_autoptr(GdkPixbuf) icon =
|
||||
gdk_pixbuf_new_from_file_at_size(path, 256, 256, nullptr);
|
||||
if (icon != nullptr) {
|
||||
gtk_window_set_icon(window, icon);
|
||||
}
|
||||
}
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
@@ -97,8 +112,9 @@ static void my_application_before_emit(GApplication* application, GVariant* plat
|
||||
}
|
||||
|
||||
// Implements GApplication::after_emit. A relaunch reaches the primary as a
|
||||
// command-line signal that the gtk plugin consumes first, so the window is
|
||||
// raised here, while the launcher's activation token is still current.
|
||||
// command-line signal whose activate returns early on the existing window, so
|
||||
// the window is raised here, while the launcher's activation token is still
|
||||
// current.
|
||||
static void my_application_after_emit(GApplication* application, GVariant* platform_data) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
if (self->had_window_before_emit) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'proxy_platform_interface.dart';
|
||||
import 'src/linux_proxy.dart';
|
||||
import 'src/macos_proxy.dart';
|
||||
import 'src/proxy_command.dart';
|
||||
import 'src/windows_proxy.dart';
|
||||
|
||||
export 'src/proxy_command.dart' show ProxyExecutableChecker, ProxyProcessRunner;
|
||||
|
||||
@@ -41,7 +42,10 @@ class Proxy {
|
||||
desktop: Platform.environment['XDG_CURRENT_DESKTOP'],
|
||||
homeDir: Platform.environment['HOME'],
|
||||
),
|
||||
'windows' => await ProxyPlatform.instance.startProxy(port, bypassDomain),
|
||||
'windows' => await ProxyPlatform.instance.startProxy(
|
||||
port,
|
||||
windowsBypassList(bypassDomain),
|
||||
),
|
||||
String() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ class LinuxProxyCommands {
|
||||
'Proxy Settings',
|
||||
'--key',
|
||||
'${type.name}Proxy',
|
||||
'${type.name}://$proxyHost:$port',
|
||||
'${type == _ProxyType.socks ? 'socks' : 'http'}://$proxyHost:$port',
|
||||
]),
|
||||
ProxyCommand(executable, [
|
||||
'--file',
|
||||
@@ -336,6 +336,7 @@ class LinuxProxyCommands {
|
||||
'ProxyType',
|
||||
'1',
|
||||
]),
|
||||
_kdeReparseConfiguration,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -353,9 +354,20 @@ class LinuxProxyCommands {
|
||||
'ProxyType',
|
||||
'0',
|
||||
]),
|
||||
_kdeReparseConfiguration,
|
||||
];
|
||||
}
|
||||
|
||||
/// Running KIO programs keep the settings they read at startup until this
|
||||
/// signal, which is what the KDE proxy settings page sends after saving.
|
||||
static final _kdeReparseConfiguration = ProxyCommand('dbus-send', [
|
||||
'--session',
|
||||
'--type=signal',
|
||||
'/KIO/Scheduler',
|
||||
'org.kde.KIO.Scheduler.reparseSlaveConfiguration',
|
||||
'string:',
|
||||
], optional: true);
|
||||
|
||||
static String _formatGSettingsStringList(List<String> values) {
|
||||
final escaped = values.map((value) {
|
||||
return value.replaceAll(r'\', r'\\').replaceAll("'", r"\'");
|
||||
|
||||
@@ -18,9 +18,14 @@ class ProxyCommand {
|
||||
final String executable;
|
||||
final List<String> args;
|
||||
final bool runInShell;
|
||||
final bool optional;
|
||||
|
||||
ProxyCommand(this.executable, List<String> args, {this.runInShell = false})
|
||||
: args = List.unmodifiable(args);
|
||||
ProxyCommand(
|
||||
this.executable,
|
||||
List<String> args, {
|
||||
this.runInShell = false,
|
||||
this.optional = false,
|
||||
}) : args = List.unmodifiable(args);
|
||||
}
|
||||
|
||||
class ProxyCommandRunner {
|
||||
@@ -38,21 +43,25 @@ class ProxyCommandRunner {
|
||||
|
||||
Future<bool> run(Iterable<ProxyCommand> commands) async {
|
||||
var executed = false;
|
||||
try {
|
||||
for (final command in commands) {
|
||||
executed = true;
|
||||
if (!await _succeeds(command) && !command.optional) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return executed;
|
||||
}
|
||||
|
||||
Future<bool> _succeeds(ProxyCommand command) async {
|
||||
try {
|
||||
final result = await process(
|
||||
command.executable,
|
||||
command.args,
|
||||
runInShell: command.runInShell,
|
||||
);
|
||||
if (result.exitCode != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return result.exitCode == 0;
|
||||
} on ProcessException {
|
||||
return false;
|
||||
}
|
||||
return executed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:proxy/proxy_method_channel.dart';
|
||||
import 'package:proxy/src/linux_proxy.dart';
|
||||
import 'package:proxy/src/macos_proxy.dart';
|
||||
import 'package:proxy/src/proxy_command.dart';
|
||||
import 'package:proxy/src/windows_proxy.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -68,6 +69,30 @@ void main() {
|
||||
|
||||
expect(await runner.run([ProxyCommand('missing', const [])]), isFalse);
|
||||
});
|
||||
|
||||
test('carries on past an optional command that fails', () async {
|
||||
final started = <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', () {
|
||||
@@ -104,7 +129,7 @@ void main() {
|
||||
'kwriteconfig6',
|
||||
'kwriteconfig5',
|
||||
]);
|
||||
expect(executedCommands, everyElement('kwriteconfig5'));
|
||||
expect(executedCommands.toSet(), {'kwriteconfig5', 'dbus-send'});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -237,6 +262,52 @@ void main() {
|
||||
|
||||
expect(commands.map((command) => command.executable).toSet(), {
|
||||
'kwriteconfig6',
|
||||
'dbus-send',
|
||||
});
|
||||
});
|
||||
|
||||
test('points every KDE proxy at the plain HTTP or SOCKS listener and '
|
||||
'tells running KIO programs to reload', () {
|
||||
for (final commands in [
|
||||
LinuxProxyCommands.buildStart(
|
||||
port: 7890,
|
||||
bypassDomain: ['localhost'],
|
||||
desktop: 'KDE',
|
||||
homeDir: '/home/user',
|
||||
availableExecutables: {'kwriteconfig6'},
|
||||
),
|
||||
LinuxProxyCommands.buildStop(
|
||||
desktop: 'KDE',
|
||||
homeDir: '/home/user',
|
||||
availableExecutables: {'kwriteconfig6'},
|
||||
),
|
||||
]) {
|
||||
final reload = commands.last;
|
||||
expect(reload.executable, 'dbus-send');
|
||||
expect(reload.args, [
|
||||
'--session',
|
||||
'--type=signal',
|
||||
'/KIO/Scheduler',
|
||||
'org.kde.KIO.Scheduler.reparseSlaveConfiguration',
|
||||
'string:',
|
||||
]);
|
||||
expect(reload.optional, isTrue);
|
||||
}
|
||||
final proxies = {
|
||||
for (final command in LinuxProxyCommands.buildStart(
|
||||
port: 7890,
|
||||
bypassDomain: ['localhost'],
|
||||
desktop: 'KDE',
|
||||
homeDir: '/home/user',
|
||||
availableExecutables: {'kwriteconfig6'},
|
||||
))
|
||||
if (command.args.length > 6 && command.args[5].endsWith('Proxy'))
|
||||
command.args[5]: command.args[6],
|
||||
};
|
||||
expect(proxies, {
|
||||
'httpProxy': 'http://127.0.0.1:7890',
|
||||
'httpsProxy': 'http://127.0.0.1:7890',
|
||||
'socksProxy': 'socks://127.0.0.1:7890',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -253,6 +324,7 @@ void main() {
|
||||
|
||||
expect(commands.map((command) => command.executable).toSet(), {
|
||||
'kwriteconfig5',
|
||||
'dbus-send',
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -268,6 +340,7 @@ void main() {
|
||||
|
||||
expect(commands.map((command) => command.executable).toSet(), {
|
||||
'kwriteconfig5',
|
||||
'dbus-send',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -357,6 +430,24 @@ USB 10/100/1000 LAN
|
||||
});
|
||||
});
|
||||
|
||||
group('windowsBypassList', () {
|
||||
test('adds the intranet token once', () {
|
||||
expect(windowsBypassList(['localhost', '127.*', '<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', () {
|
||||
final proxy = MethodChannelProxy();
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
|
||||
# dependencies here.
|
||||
target_include_directories(${PLUGIN_NAME} INTERFACE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin wininet rasapi32)
|
||||
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin wininet rasapi32 iphlpapi)
|
||||
|
||||
# List of absolute paths to libraries that should be bundled with the plugin.
|
||||
# This list could contain prebuilt libraries, or libraries created by an
|
||||
@@ -86,7 +86,7 @@ add_executable(${TEST_RUNNER}
|
||||
)
|
||||
apply_standard_settings(${TEST_RUNNER})
|
||||
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin wininet rasapi32)
|
||||
target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin wininet rasapi32 iphlpapi)
|
||||
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
|
||||
# flutter_wrapper_plugin has link dependencies on the Flutter DLL.
|
||||
add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD
|
||||
|
||||
@@ -16,6 +16,9 @@ extern "C" {
|
||||
FLUTTER_PLUGIN_EXPORT void ProxyPluginCApiRegisterWithRegistrar(
|
||||
FlutterDesktopPluginRegistrarRef registrar);
|
||||
|
||||
// Safe pre-engine.
|
||||
FLUTTER_PLUGIN_EXPORT bool ProxyPluginClearStaleProxy();
|
||||
|
||||
#if defined(__cplusplus)
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
@@ -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"
|
||||
|
||||
// This must be included before many other Windows headers.
|
||||
@@ -6,12 +11,17 @@
|
||||
#include <WinInet.h>
|
||||
#include <Ras.h>
|
||||
#include <RasError.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <stdlib.h>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#pragma comment(lib, "wininet")
|
||||
#pragma comment(lib, "Rasapi32")
|
||||
#pragma comment(lib, "iphlpapi")
|
||||
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/plugin_registrar_windows.h>
|
||||
@@ -24,6 +34,7 @@ namespace
|
||||
|
||||
constexpr int kMinProxyPort = 1;
|
||||
constexpr int kMaxProxyPort = 65535;
|
||||
constexpr wchar_t kProxyServerPrefix[] = L"127.0.0.1:";
|
||||
|
||||
std::wstring Utf8ToWide(const std::string& value)
|
||||
{
|
||||
@@ -80,9 +91,9 @@ bool SetOptionsForConnection(
|
||||
sizeof(list)) != FALSE;
|
||||
}
|
||||
|
||||
bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list)
|
||||
bool ForEachConnection(const std::function<bool(LPTSTR)>& visit)
|
||||
{
|
||||
bool success = SetOptionsForConnection(list, nullptr);
|
||||
bool success = visit(nullptr);
|
||||
|
||||
DWORD size = 0;
|
||||
DWORD count = 0;
|
||||
@@ -99,7 +110,7 @@ bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list)
|
||||
{
|
||||
for (DWORD i = 0; i < count; i++)
|
||||
{
|
||||
success = SetOptionsForConnection(list, entries[i].szEntryName) && success;
|
||||
success = visit(entries[i].szEntryName) && success;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -115,6 +126,14 @@ bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list)
|
||||
return success;
|
||||
}
|
||||
|
||||
bool ApplyOptionsToConnections(INTERNET_PER_CONN_OPTION_LIST& list)
|
||||
{
|
||||
return ForEachConnection([&list](LPTSTR connection)
|
||||
{
|
||||
return SetOptionsForConnection(list, connection);
|
||||
});
|
||||
}
|
||||
|
||||
bool NotifySettingsChanged()
|
||||
{
|
||||
const bool changed = InternetSetOption(
|
||||
@@ -126,7 +145,7 @@ bool NotifySettingsChanged()
|
||||
|
||||
bool startProxy(const int port, const flutter::EncodableList& bypassDomain)
|
||||
{
|
||||
auto url = Utf8ToWide("127.0.0.1:" + std::to_string(port));
|
||||
auto url = kProxyServerPrefix + std::to_wstring(port);
|
||||
auto bypassList = BuildBypassList(bypassDomain);
|
||||
std::vector<INTERNET_PER_CONN_OPTION> options(3);
|
||||
|
||||
@@ -166,11 +185,165 @@ bool stopProxy()
|
||||
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 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
|
||||
void ProxyPlugin::RegisterWithRegistrar(
|
||||
flutter::PluginRegistrarWindows *registrar)
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
|
||||
namespace proxy {
|
||||
|
||||
// Turns off a system proxy that still points at FlClash's address while
|
||||
// nothing listens on its port any more, the state a killed app leaves behind.
|
||||
bool ClearStaleProxy();
|
||||
|
||||
class ProxyPlugin : public flutter::Plugin {
|
||||
public:
|
||||
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
|
||||
|
||||
@@ -10,3 +10,7 @@ void ProxyPluginCApiRegisterWithRegistrar(
|
||||
flutter::PluginRegistrarManager::GetInstance()
|
||||
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
|
||||
}
|
||||
|
||||
bool ProxyPluginClearStaleProxy() {
|
||||
return proxy::ClearStaleProxy();
|
||||
}
|
||||
|
||||
@@ -6,10 +6,46 @@
|
||||
import '../frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
|
||||
Stream<Uint8List> restartIpcServer({required String name}) =>
|
||||
RustLib.instance.api.crateApiIpcRestartIpcServer(name: name);
|
||||
// These functions are ignored because they are not marked as `pub`: `bridge`
|
||||
|
||||
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}) =>
|
||||
RustLib.instance.api.crateApiIpcSendIpcMessage(data: data);
|
||||
Future<void> close();
|
||||
|
||||
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 }
|
||||
|
||||
@@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
String get codegenVersion => '2.13.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => -1283005629;
|
||||
int get rustContentHash => 736531895;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
@@ -81,6 +81,17 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
}
|
||||
|
||||
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({
|
||||
required RopeBridge that,
|
||||
required BigInt startLine,
|
||||
@@ -154,18 +165,20 @@ abstract class RustLibApi extends BaseApi {
|
||||
|
||||
Future<void> crateApiInitInitApp();
|
||||
|
||||
Stream<Uint8List> crateApiIpcRestartIpcServer({required String name});
|
||||
|
||||
Future<void> crateApiIpcSendIpcMessage({required List<int> data});
|
||||
|
||||
Future<List<HotKeyFailure>> crateApiHotkeySetHotKeys({
|
||||
required List<HotKeySpec> specs,
|
||||
});
|
||||
|
||||
Future<void> crateApiIpcStopIpcServer();
|
||||
|
||||
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
|
||||
get rust_arc_increment_strong_count_RopeBridge;
|
||||
|
||||
@@ -183,6 +196,136 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
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
|
||||
List<String> crateApiEditorRopeBridgeCachedLinesRange({
|
||||
required RopeBridge that,
|
||||
@@ -199,7 +342,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
sse_encode_usize(startLine, serializer);
|
||||
sse_encode_usize(endLine, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_list_String,
|
||||
@@ -232,7 +375,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
serializer,
|
||||
);
|
||||
sse_encode_usize(charIdx, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_usize,
|
||||
@@ -258,7 +401,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_String(initialText, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData:
|
||||
@@ -288,7 +431,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
that,
|
||||
serializer,
|
||||
);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_String,
|
||||
@@ -320,7 +463,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
sse_encode_usize(charIdx, serializer);
|
||||
sse_encode_String(text, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
@@ -349,7 +492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
that,
|
||||
serializer,
|
||||
);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_usize,
|
||||
@@ -378,7 +521,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
that,
|
||||
serializer,
|
||||
);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_usize,
|
||||
@@ -411,7 +554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
serializer,
|
||||
);
|
||||
sse_encode_usize(lineIdx, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_String,
|
||||
@@ -444,7 +587,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
serializer,
|
||||
);
|
||||
sse_encode_usize(lineIdx, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_usize,
|
||||
@@ -479,7 +622,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
sse_encode_usize(start, serializer);
|
||||
sse_encode_usize(end, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
@@ -514,7 +657,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
sse_encode_usize(start, serializer);
|
||||
sse_encode_usize(end, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_String,
|
||||
@@ -547,7 +690,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 12,
|
||||
funcId: 16,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -585,7 +728,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 13,
|
||||
funcId: 17,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -620,7 +763,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
serializer,
|
||||
);
|
||||
sse_encode_usize(targetOffset, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_opt_box_autoadd_usize,
|
||||
@@ -657,7 +800,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_usize(firstVisible, serializer);
|
||||
sse_encode_usize(lastVisible, serializer);
|
||||
sse_encode_usize(tabSize, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_list_guide_block,
|
||||
@@ -688,7 +831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 16,
|
||||
funcId: 20,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -717,7 +860,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 17,
|
||||
funcId: 21,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -735,70 +878,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiInitInitAppConstMeta =>
|
||||
const TaskConstMeta(debugName: "init_app", argNames: []);
|
||||
|
||||
@override
|
||||
Stream<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
|
||||
Future<List<HotKeyFailure>> crateApiHotkeySetHotKeys({
|
||||
required List<HotKeySpec> specs,
|
||||
@@ -811,7 +890,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 20,
|
||||
funcId: 22,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -829,33 +908,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiHotkeySetHotKeysConstMeta =>
|
||||
const TaskConstMeta(debugName: "set_hot_keys", argNames: ["specs"]);
|
||||
|
||||
@override
|
||||
Future<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
|
||||
Future<List<String>> crateApiEditorWordsExtract({required RopeBridge rope}) {
|
||||
return handler.executeNormal(
|
||||
@@ -869,7 +921,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 22,
|
||||
funcId: 23,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -887,6 +939,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiEditorWordsExtractConstMeta =>
|
||||
const TaskConstMeta(debugName: "words_extract", argNames: ["rope"]);
|
||||
|
||||
RustArcIncrementStrongCountFnType
|
||||
get rust_arc_increment_strong_count_IpcServer => wire
|
||||
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer;
|
||||
|
||||
RustArcDecrementStrongCountFnType
|
||||
get rust_arc_decrement_strong_count_IpcServer => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer;
|
||||
|
||||
RustArcIncrementStrongCountFnType
|
||||
get rust_arc_increment_strong_count_RopeBridge => wire
|
||||
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge;
|
||||
@@ -901,6 +961,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return AnyhowException(raw as String);
|
||||
}
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
dynamic raw,
|
||||
) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return IpcServerImpl.frbInternalDcoDecode(raw as List<dynamic>);
|
||||
}
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -910,6 +979,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return RopeBridgeImpl.frbInternalDcoDecode(raw as List<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
|
||||
RopeBridge
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -919,6 +997,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return RopeBridgeImpl.frbInternalDcoDecode(raw as List<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
|
||||
RopeBridge
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -929,9 +1016,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
dynamic raw,
|
||||
) {
|
||||
RustStreamSink<IpcEvent> dco_decode_StreamSink_ipc_event_Sse(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
throw UnimplementedError();
|
||||
}
|
||||
@@ -948,6 +1033,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return raw as String;
|
||||
}
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_u_32(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return raw as int;
|
||||
}
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_box_autoadd_usize(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -1005,6 +1096,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return raw as int;
|
||||
}
|
||||
|
||||
@protected
|
||||
IpcEvent dco_decode_ipc_event(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<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
|
||||
List<String> dco_decode_list_String(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -1053,6 +1164,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return (raw as List<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
|
||||
BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -1102,6 +1225,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return AnyhowException(inner);
|
||||
}
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
return IpcServerImpl.frbInternalSseDecode(
|
||||
sse_decode_usize(deserializer),
|
||||
sse_decode_i_32(deserializer),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -1114,6 +1249,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
return IpcServerImpl.frbInternalSseDecode(
|
||||
sse_decode_usize(deserializer),
|
||||
sse_decode_i_32(deserializer),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -1126,6 +1273,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
return IpcServerImpl.frbInternalSseDecode(
|
||||
sse_decode_usize(deserializer),
|
||||
sse_decode_i_32(deserializer),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -1139,7 +1298,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
RustStreamSink<IpcEvent> sse_decode_StreamSink_ipc_event_Sse(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -1161,6 +1320,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return utf8.decoder.convert(inner);
|
||||
}
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
return (sse_decode_u_32(deserializer));
|
||||
}
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -1212,6 +1377,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return deserializer.buffer.getInt32();
|
||||
}
|
||||
|
||||
@protected
|
||||
IpcEvent sse_decode_ipc_event(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
var var_kind = sse_decode_ipc_event_kind(deserializer);
|
||||
var var_pid = sse_decode_opt_box_autoadd_u_32(deserializer);
|
||||
var var_payload = sse_decode_list_prim_u_8_strict(deserializer);
|
||||
var var_error = sse_decode_opt_String(deserializer);
|
||||
return IpcEvent(
|
||||
kind: var_kind,
|
||||
pid: var_pid,
|
||||
payload: var_payload,
|
||||
error: var_error,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
IpcEventKind sse_decode_ipc_event_kind(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
var inner = sse_decode_i_32(deserializer);
|
||||
return IpcEventKind.values[inner];
|
||||
}
|
||||
|
||||
@protected
|
||||
List<String> sse_decode_list_String(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -1304,6 +1491,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return ans_;
|
||||
}
|
||||
|
||||
@protected
|
||||
String? sse_decode_opt_String(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
if (sse_decode_bool(deserializer)) {
|
||||
return (sse_decode_String(deserializer));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
if (sse_decode_bool(deserializer)) {
|
||||
return (sse_decode_box_autoadd_u_32(deserializer));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -1361,6 +1570,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_String(self.message, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_usize(
|
||||
(self as IpcServerImpl).frbInternalSseEncode(move: true),
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -1374,6 +1596,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_usize(
|
||||
(self as IpcServerImpl).frbInternalSseEncode(move: false),
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -1387,6 +1622,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_usize(
|
||||
(self as IpcServerImpl).frbInternalSseEncode(move: null),
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -1401,15 +1649,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
RustStreamSink<Uint8List> self,
|
||||
void sse_encode_StreamSink_ipc_event_Sse(
|
||||
RustStreamSink<IpcEvent> self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_String(
|
||||
self.setupAndSerialize(
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_list_prim_u_8_strict,
|
||||
decodeSuccessData: sse_decode_ipc_event,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
),
|
||||
@@ -1440,6 +1688,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_u_32(self, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -1488,6 +1742,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
serializer.buffer.putInt32(self);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_ipc_event(IpcEvent self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_ipc_event_kind(self.kind, serializer);
|
||||
sse_encode_opt_box_autoadd_u_32(self.pid, serializer);
|
||||
sse_encode_list_prim_u_8_strict(self.payload, serializer);
|
||||
sse_encode_opt_String(self.error, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_ipc_event_kind(IpcEventKind self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_i_32(self.index, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -1579,6 +1848,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_String(String? self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
sse_encode_bool(self != null, serializer);
|
||||
if (self != null) {
|
||||
sse_encode_String(self, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
sse_encode_bool(self != null, serializer);
|
||||
if (self != null) {
|
||||
sse_encode_box_autoadd_u_32(self, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_usize(
|
||||
BigInt? self,
|
||||
@@ -1632,6 +1921,35 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
}
|
||||
|
||||
@sealed
|
||||
class IpcServerImpl extends RustOpaque implements IpcServer {
|
||||
// Not to be used by end users
|
||||
IpcServerImpl.frbInternalDcoDecode(List<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
|
||||
class RopeBridgeImpl extends RustOpaque implements RopeBridge {
|
||||
// Not to be used by end users
|
||||
|
||||
@@ -21,6 +21,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
required super.portManager,
|
||||
});
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_IpcServerPtr => wire
|
||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServerPtr;
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_RopeBridgePtr => wire
|
||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridgePtr;
|
||||
@@ -28,18 +32,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -47,9 +69,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
dynamic raw,
|
||||
);
|
||||
RustStreamSink<IpcEvent> dco_decode_StreamSink_ipc_event_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustStreamSink<int> dco_decode_StreamSink_u_32_Sse(dynamic raw);
|
||||
@@ -57,6 +77,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_box_autoadd_usize(dynamic raw);
|
||||
|
||||
@@ -75,6 +98,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
int dco_decode_i_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
IpcEvent dco_decode_ipc_event(dynamic raw);
|
||||
|
||||
@protected
|
||||
IpcEventKind dco_decode_ipc_event_kind(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<String> dco_decode_list_String(dynamic raw);
|
||||
|
||||
@@ -99,6 +128,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
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
|
||||
BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw);
|
||||
|
||||
@@ -120,18 +155,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -139,7 +192,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
RustStreamSink<IpcEvent> sse_decode_StreamSink_ipc_event_Sse(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@@ -151,6 +204,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer);
|
||||
|
||||
@@ -169,6 +225,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
int sse_decode_i_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
IpcEvent sse_decode_ipc_event(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
IpcEventKind sse_decode_ipc_event_kind(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||
|
||||
@@ -199,6 +261,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer);
|
||||
|
||||
@@ -226,6 +294,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -233,6 +308,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -240,6 +322,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -248,8 +337,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
RustStreamSink<Uint8List> self,
|
||||
void sse_encode_StreamSink_ipc_event_Sse(
|
||||
RustStreamSink<IpcEvent> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@@ -262,6 +351,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer);
|
||||
|
||||
@@ -283,6 +375,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_ipc_event(IpcEvent self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_ipc_event_kind(IpcEventKind self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||
|
||||
@@ -325,6 +423,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer);
|
||||
|
||||
@@ -361,6 +465,40 @@ class RustLibWire implements BaseWire {
|
||||
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
|
||||
: _lookup = dynamicLibrary.lookup;
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
ffi.Pointer<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
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
|
||||
@@ -23,6 +23,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
required super.portManager,
|
||||
});
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_IpcServerPtr => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer;
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_RopeBridgePtr => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge;
|
||||
@@ -30,18 +34,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -49,9 +71,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
dynamic raw,
|
||||
);
|
||||
RustStreamSink<IpcEvent> dco_decode_StreamSink_ipc_event_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustStreamSink<int> dco_decode_StreamSink_u_32_Sse(dynamic raw);
|
||||
@@ -59,6 +79,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_box_autoadd_usize(dynamic raw);
|
||||
|
||||
@@ -77,6 +100,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
int dco_decode_i_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
IpcEvent dco_decode_ipc_event(dynamic raw);
|
||||
|
||||
@protected
|
||||
IpcEventKind dco_decode_ipc_event_kind(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<String> dco_decode_list_String(dynamic raw);
|
||||
|
||||
@@ -101,6 +130,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
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
|
||||
BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw);
|
||||
|
||||
@@ -122,18 +157,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
IpcServer
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RopeBridge
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -141,7 +194,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
RustStreamSink<IpcEvent> sse_decode_StreamSink_ipc_event_Sse(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@@ -153,6 +206,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer);
|
||||
|
||||
@@ -171,6 +227,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
int sse_decode_i_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
IpcEvent sse_decode_ipc_event(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
IpcEventKind sse_decode_ipc_event_kind(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||
|
||||
@@ -201,6 +263,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer);
|
||||
|
||||
@@ -228,6 +296,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -235,6 +310,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -242,6 +324,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
IpcServer self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
@@ -250,8 +339,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(
|
||||
RustStreamSink<Uint8List> self,
|
||||
void sse_encode_StreamSink_ipc_event_Sse(
|
||||
RustStreamSink<IpcEvent> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@@ -264,6 +353,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer);
|
||||
|
||||
@@ -285,6 +377,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_ipc_event(IpcEvent self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_ipc_event_kind(IpcEventKind self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||
|
||||
@@ -327,6 +425,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer);
|
||||
|
||||
@@ -354,6 +458,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
class RustLibWire implements BaseWire {
|
||||
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
int ptr,
|
||||
) => wasmModule
|
||||
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
ptr,
|
||||
);
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
int ptr,
|
||||
) => wasmModule
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
ptr,
|
||||
);
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
int ptr,
|
||||
@@ -377,6 +497,16 @@ external RustLibWasmModule get wasmModule;
|
||||
@JS()
|
||||
@anonymous
|
||||
extension type RustLibWasmModule._(JSObject _) implements JSObject {
|
||||
external void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
int ptr,
|
||||
);
|
||||
|
||||
external void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
int ptr,
|
||||
);
|
||||
|
||||
external void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
int ptr,
|
||||
|
||||
@@ -3,17 +3,59 @@ use crate::ipc;
|
||||
use flutter_rust_bridge::for_generated::SseCodec;
|
||||
use flutter_rust_bridge::frb;
|
||||
|
||||
#[frb]
|
||||
pub fn restart_ipc_server(name: String, sink: StreamSink<Vec<u8>, SseCodec>) -> Result<(), String> {
|
||||
ipc::restart_server(name, sink)
|
||||
pub enum IpcEventKind {
|
||||
Connected,
|
||||
Message,
|
||||
Disconnected,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[frb]
|
||||
pub fn stop_ipc_server() -> Result<(), String> {
|
||||
ipc::stop_server()
|
||||
pub struct IpcEvent {
|
||||
pub kind: IpcEventKind,
|
||||
pub pid: Option<u32>,
|
||||
pub payload: Vec<u8>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[frb]
|
||||
pub fn send_ipc_message(data: Vec<u8>) -> Result<(), String> {
|
||||
ipc::send_message(data)
|
||||
fn bridge(event: ipc::Event) -> IpcEvent {
|
||||
let (kind, pid, payload, error) = match event {
|
||||
ipc::Event::Connected { pid } => (IpcEventKind::Connected, pid, Vec::new(), None),
|
||||
ipc::Event::Message(payload) => (IpcEventKind::Message, None, payload, None),
|
||||
ipc::Event::Disconnected { error } => (IpcEventKind::Disconnected, None, Vec::new(), error),
|
||||
ipc::Event::Failed(error) => (IpcEventKind::Failed, None, Vec::new(), Some(error)),
|
||||
};
|
||||
IpcEvent {
|
||||
kind,
|
||||
pid,
|
||||
payload,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
#[frb(opaque)]
|
||||
pub struct IpcServer {
|
||||
server: ipc::Server,
|
||||
}
|
||||
|
||||
impl IpcServer {
|
||||
pub fn bind(address: String) -> Result<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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
// Section: imports
|
||||
|
||||
use crate::api::editor::*;
|
||||
use crate::api::ipc::*;
|
||||
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
|
||||
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||
@@ -40,7 +41,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.13.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1283005629;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 736531895;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -48,6 +49,192 @@ flutter_rust_bridge::frb_generated_default_handler!();
|
||||
|
||||
// Section: wire_funcs
|
||||
|
||||
fn wire__crate__api__ipc__IpcServer_bind_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::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(
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
@@ -839,76 +1026,6 @@ fn wire__crate__api__init__init_app_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__ipc__restart_ipc_server_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -942,38 +1059,6 @@ fn wire__crate__api__hotkey__set_hot_keys_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__ipc__stop_ipc_server_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -1027,6 +1112,9 @@ fn wire__crate__api__editor__words_extract_impl(
|
||||
|
||||
// Section: related_funcs
|
||||
|
||||
flutter_rust_bridge::frb_generated_moi_arc_impl_value!(
|
||||
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<IpcServer>
|
||||
);
|
||||
flutter_rust_bridge::frb_generated_moi_arc_impl_value!(
|
||||
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 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1051,6 +1149,16 @@ impl SseDecode for RopeBridge {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode
|
||||
for RustOpaqueMoi<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
|
||||
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
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
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> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1240,6 +1380,28 @@ impl SseDecode for Vec<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> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1305,15 +1467,15 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
) {
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
12 => wire__crate__api__script__evaluate_script_impl(port, ptr, rust_vec_len, data_len),
|
||||
13 => wire__crate__api__editor__folds_compute_all_impl(port, ptr, rust_vec_len, data_len),
|
||||
16 => wire__crate__api__hotkey__hot_key_events_impl(port, ptr, rust_vec_len, data_len),
|
||||
17 => wire__crate__api__init__init_app_impl(port, ptr, rust_vec_len, data_len),
|
||||
18 => wire__crate__api__ipc__restart_ipc_server_impl(port, ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__api__ipc__send_ipc_message_impl(port, ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__api__hotkey__set_hot_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__api__ipc__stop_ipc_server_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__editor__words_extract_impl(port, ptr, rust_vec_len, data_len),
|
||||
1 => wire__crate__api__ipc__IpcServer_bind_impl(port, ptr, rust_vec_len, data_len),
|
||||
2 => wire__crate__api__ipc__IpcServer_close_impl(port, ptr, rust_vec_len, data_len),
|
||||
3 => wire__crate__api__ipc__IpcServer_events_impl(port, ptr, rust_vec_len, data_len),
|
||||
16 => wire__crate__api__script__evaluate_script_impl(port, ptr, rust_vec_len, data_len),
|
||||
17 => wire__crate__api__editor__folds_compute_all_impl(port, ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__api__hotkey__hot_key_events_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__api__init__init_app_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__hotkey__set_hot_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__editor__words_extract_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -1326,31 +1488,47 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
1 => wire__crate__api__editor__RopeBridge_cached_lines_range_impl(
|
||||
4 => wire__crate__api__ipc__IpcServer_send_impl(ptr, rust_vec_len, data_len),
|
||||
5 => wire__crate__api__editor__RopeBridge_cached_lines_range_impl(
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
2 => wire__crate__api__editor__RopeBridge_char_to_line_impl(ptr, rust_vec_len, data_len),
|
||||
3 => wire__crate__api__editor__RopeBridge_create_impl(ptr, rust_vec_len, data_len),
|
||||
4 => wire__crate__api__editor__RopeBridge_get_text_impl(ptr, rust_vec_len, data_len),
|
||||
5 => wire__crate__api__editor__RopeBridge_insert_impl(ptr, rust_vec_len, data_len),
|
||||
6 => wire__crate__api__editor__RopeBridge_len_chars_impl(ptr, rust_vec_len, data_len),
|
||||
7 => wire__crate__api__editor__RopeBridge_len_lines_impl(ptr, rust_vec_len, data_len),
|
||||
8 => wire__crate__api__editor__RopeBridge_line_impl(ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__editor__RopeBridge_line_to_char_impl(ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__editor__RopeBridge_remove_impl(ptr, rust_vec_len, data_len),
|
||||
11 => wire__crate__api__editor__RopeBridge_slice_impl(ptr, rust_vec_len, data_len),
|
||||
14 => {
|
||||
6 => wire__crate__api__editor__RopeBridge_char_to_line_impl(ptr, rust_vec_len, data_len),
|
||||
7 => wire__crate__api__editor__RopeBridge_create_impl(ptr, rust_vec_len, data_len),
|
||||
8 => wire__crate__api__editor__RopeBridge_get_text_impl(ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__editor__RopeBridge_insert_impl(ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__editor__RopeBridge_len_chars_impl(ptr, rust_vec_len, data_len),
|
||||
11 => wire__crate__api__editor__RopeBridge_len_lines_impl(ptr, rust_vec_len, data_len),
|
||||
12 => wire__crate__api__editor__RopeBridge_line_impl(ptr, rust_vec_len, data_len),
|
||||
13 => wire__crate__api__editor__RopeBridge_line_to_char_impl(ptr, rust_vec_len, data_len),
|
||||
14 => wire__crate__api__editor__RopeBridge_remove_impl(ptr, rust_vec_len, data_len),
|
||||
15 => wire__crate__api__editor__RopeBridge_slice_impl(ptr, rust_vec_len, data_len),
|
||||
18 => {
|
||||
wire__crate__api__editor__folds_find_matching_bracket_impl(ptr, rust_vec_len, data_len)
|
||||
}
|
||||
15 => wire__crate__api__editor__guides_compute_viewport_impl(ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__api__editor__guides_compute_viewport_impl(ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Section: rust2dart
|
||||
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for FrbWrapper<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
|
||||
impl flutter_rust_bridge::IntoDart for FrbWrapper<RopeBridge> {
|
||||
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
|
||||
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 {
|
||||
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 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -1493,6 +1716,17 @@ impl SseEncode for RopeBridge {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode
|
||||
for RustOpaqueMoi<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
|
||||
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
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
unimplemented!("")
|
||||
@@ -1579,6 +1815,34 @@ impl SseEncode for i32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::ipc::IpcEvent {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<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> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -1649,6 +1913,26 @@ impl SseEncode for Vec<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> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -1712,6 +1996,7 @@ mod io {
|
||||
|
||||
use super::*;
|
||||
use crate::api::editor::*;
|
||||
use crate::api::ipc::*;
|
||||
use flutter_rust_bridge::for_generated::byteorder::{
|
||||
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||
};
|
||||
@@ -1722,6 +2007,20 @@ mod io {
|
||||
|
||||
flutter_rust_bridge::frb_generated_boilerplate_io!();
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn frbgen_rust_api_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
ptr: *const std::ffi::c_void,
|
||||
) {
|
||||
MoiArc::<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)]
|
||||
pub extern "C" fn frbgen_rust_api_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
ptr: *const std::ffi::c_void,
|
||||
@@ -1749,6 +2048,7 @@ mod web {
|
||||
|
||||
use super::*;
|
||||
use crate::api::editor::*;
|
||||
use crate::api::ipc::*;
|
||||
use flutter_rust_bridge::for_generated::byteorder::{
|
||||
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||
};
|
||||
@@ -1761,6 +2061,20 @@ mod web {
|
||||
|
||||
flutter_rust_bridge::frb_generated_boilerplate_web!();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerIpcServer(
|
||||
ptr: *const std::ffi::c_void,
|
||||
) {
|
||||
MoiArc::<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]
|
||||
pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRopeBridge(
|
||||
ptr: *const std::ffi::c_void,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,283 +1,94 @@
|
||||
use std::io::{self, Read, Write};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub const TYPE_READY: u8 = 0x00;
|
||||
pub const TYPE_CONNECTED: u8 = 0x01;
|
||||
pub const TYPE_DISCONNECTED: u8 = 0x02;
|
||||
pub const TYPE_DATA: u8 = 0x03;
|
||||
pub const TYPE_ERROR: u8 = 0x04;
|
||||
use std::io;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
|
||||
pub const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024;
|
||||
pub const IO_POLL_INTERVAL: Duration = Duration::from_millis(20);
|
||||
const MIN_WRITE_BACKOFF: Duration = Duration::from_millis(1);
|
||||
const HEADER_SIZE: usize = 4;
|
||||
|
||||
pub fn make_frame(ty: u8, payload: &[u8]) -> Vec<u8> {
|
||||
let mut frame = Vec::with_capacity(1 + payload.len());
|
||||
frame.push(ty);
|
||||
frame.extend_from_slice(payload);
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn validate_frame_len(len: usize) -> io::Result<u32> {
|
||||
if len > MAX_FRAME_SIZE {
|
||||
return Err(io::Error::new(
|
||||
fn too_large() -> io::Error {
|
||||
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))]
|
||||
fn normalize_windows_pipe_write(result: io::Result<usize>) -> io::Result<usize> {
|
||||
match result {
|
||||
Ok(0) => Err(io::ErrorKind::WouldBlock.into()),
|
||||
result => result,
|
||||
/// Prefixes `payload` with its little-endian length, the layout `readFrame`
|
||||
/// in `core/server.go` expects.
|
||||
pub fn encode(payload: &[u8]) -> io::Result<Vec<u8>> {
|
||||
if payload.len() > MAX_FRAME_SIZE {
|
||||
return Err(too_large());
|
||||
}
|
||||
let mut frame = Vec::with_capacity(HEADER_SIZE + payload.len());
|
||||
frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
|
||||
frame.extend_from_slice(payload);
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn write_all_interruptible(
|
||||
writer: &mut impl Write,
|
||||
mut data: &[u8],
|
||||
is_running: &impl Fn() -> bool,
|
||||
) -> io::Result<()> {
|
||||
let mut backoff = MIN_WRITE_BACKOFF;
|
||||
while !data.is_empty() {
|
||||
if !is_running() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"IPC connection stopped",
|
||||
));
|
||||
}
|
||||
let result = writer.write(data);
|
||||
#[cfg(windows)]
|
||||
let result = normalize_windows_pipe_write(result);
|
||||
match result {
|
||||
Ok(0) => return Err(io::ErrorKind::WriteZero.into()),
|
||||
Ok(written) => {
|
||||
data = &data[written..];
|
||||
backoff = MIN_WRITE_BACKOFF;
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
// A Windows PIPE_NOWAIT handle reports a full buffer through a
|
||||
// zero-byte write, and every frame costs two writes. A flat
|
||||
// 20ms wait there parks the whole queue behind one frame, so
|
||||
// back off from 1ms instead.
|
||||
thread::sleep(backoff);
|
||||
backoff = (backoff * 2).min(IO_POLL_INTERVAL);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_frame(
|
||||
writer: &mut impl Write,
|
||||
data: &[u8],
|
||||
is_running: &impl Fn() -> bool,
|
||||
) -> io::Result<()> {
|
||||
let len = validate_frame_len(data.len())?;
|
||||
write_all_interruptible(writer, &len.to_le_bytes(), is_running)?;
|
||||
write_all_interruptible(writer, data, is_running)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FrameReader {
|
||||
header: [u8; 4],
|
||||
header_read: usize,
|
||||
payload: Vec<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;
|
||||
pub async fn read(reader: &mut (impl AsyncRead + Unpin)) -> io::Result<Vec<u8>> {
|
||||
let mut header = [0; HEADER_SIZE];
|
||||
reader.read_exact(&mut header).await?;
|
||||
let len = u32::from_le_bytes(header) as usize;
|
||||
if len > MAX_FRAME_SIZE {
|
||||
return Err(too_large());
|
||||
}
|
||||
let mut payload = vec![0; len];
|
||||
reader.read_exact(&mut payload).await?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Cursor;
|
||||
|
||||
enum ReadStep {
|
||||
Data(Vec<u8>),
|
||||
WouldBlock,
|
||||
}
|
||||
|
||||
struct StepReader {
|
||||
steps: VecDeque<ReadStep>,
|
||||
}
|
||||
|
||||
impl Read for StepReader {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
match self.steps.pop_front() {
|
||||
Some(ReadStep::Data(mut data)) => {
|
||||
let read = data.len().min(buffer.len());
|
||||
buffer[..read].copy_from_slice(&data[..read]);
|
||||
if read < data.len() {
|
||||
data.drain(..read);
|
||||
self.steps.push_front(ReadStep::Data(data));
|
||||
}
|
||||
Ok(read)
|
||||
}
|
||||
Some(ReadStep::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()),
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
fn read_all(bytes: &[u8]) -> io::Result<Vec<u8>> {
|
||||
let mut reader = bytes;
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(read(&mut reader))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_reader_reads_complete_frame() {
|
||||
let payload = b"hello";
|
||||
let mut bytes = (payload.len() as u32).to_le_bytes().to_vec();
|
||||
bytes.extend_from_slice(payload);
|
||||
let mut reader = Cursor::new(bytes);
|
||||
let mut frame_reader = FrameReader::default();
|
||||
fn 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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_frame_is_a_frame() {
|
||||
assert_eq!(read_all(&encode(b"").unwrap()).unwrap(), b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_truncated_frame_reads_as_end_of_stream() {
|
||||
let frame = encode(b"hello").unwrap();
|
||||
|
||||
for len in [0, 2, HEADER_SIZE, frame.len() - 1] {
|
||||
assert_eq!(
|
||||
frame_reader.poll(&mut reader).unwrap(),
|
||||
Some(payload.to_vec())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_reader_preserves_partial_nonblocking_reads() {
|
||||
let mut reader = StepReader {
|
||||
steps: VecDeque::from([
|
||||
ReadStep::Data(vec![5, 0]),
|
||||
ReadStep::WouldBlock,
|
||||
ReadStep::Data(vec![0, 0]),
|
||||
ReadStep::Data(b"he".to_vec()),
|
||||
ReadStep::WouldBlock,
|
||||
ReadStep::Data(b"llo".to_vec()),
|
||||
]),
|
||||
};
|
||||
let mut frame_reader = FrameReader::default();
|
||||
|
||||
assert_eq!(frame_reader.poll(&mut reader).unwrap(), None);
|
||||
assert_eq!(frame_reader.poll(&mut reader).unwrap(), None);
|
||||
assert_eq!(
|
||||
frame_reader.poll(&mut reader).unwrap(),
|
||||
Some(b"hello".to_vec())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_reader_rejects_oversized_frame_before_allocation() {
|
||||
let len = (MAX_FRAME_SIZE as u32 + 1).to_le_bytes();
|
||||
let mut reader = Cursor::new(len);
|
||||
let mut frame_reader = FrameReader::default();
|
||||
|
||||
assert_eq!(
|
||||
frame_reader.poll(&mut reader).unwrap_err().kind(),
|
||||
io::ErrorKind::InvalidData
|
||||
);
|
||||
assert!(frame_reader.payload.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_zero_write_is_treated_as_pending() {
|
||||
assert_eq!(
|
||||
normalize_windows_pipe_write(Ok(0)).unwrap_err().kind(),
|
||||
io::ErrorKind::WouldBlock,
|
||||
);
|
||||
assert_eq!(normalize_windows_pipe_write(Ok(2)).unwrap(), 2);
|
||||
|
||||
let error = io::Error::from(io::ErrorKind::BrokenPipe);
|
||||
assert_eq!(
|
||||
normalize_windows_pipe_write(Err(error)).unwrap_err().kind(),
|
||||
io::ErrorKind::BrokenPipe,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_backoff_starts_short_and_is_capped() {
|
||||
let mut backoff = MIN_WRITE_BACKOFF;
|
||||
assert_eq!(backoff, Duration::from_millis(1));
|
||||
for _ in 0..10 {
|
||||
backoff = (backoff * 2).min(IO_POLL_INTERVAL);
|
||||
}
|
||||
assert_eq!(backoff, IO_POLL_INTERVAL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_frame_stops_when_the_connection_is_gone() {
|
||||
let mut sink = Vec::new();
|
||||
|
||||
assert_eq!(
|
||||
write_frame(&mut sink, b"payload", &|| false)
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
io::ErrorKind::Interrupted,
|
||||
);
|
||||
assert!(sink.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expected_disconnect_errors_are_not_reported() {
|
||||
for kind in [
|
||||
read_all(&frame[..len]).unwrap_err().kind(),
|
||||
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(
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_oversized_header_is_rejected_before_its_payload_is_read() {
|
||||
let header = (MAX_FRAME_SIZE as u32 + 1).to_le_bytes();
|
||||
|
||||
assert_eq!(
|
||||
read_all(&header).unwrap_err().kind(),
|
||||
io::ErrorKind::InvalidData,
|
||||
)));
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_oversized_payload_is_not_encoded() {
|
||||
let payload = vec![0; MAX_FRAME_SIZE + 1];
|
||||
|
||||
assert_eq!(
|
||||
encode(&payload).unwrap_err().kind(),
|
||||
io::ErrorKind::InvalidData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod access;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod frame;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod platform;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod queue;
|
||||
mod outbox;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod server;
|
||||
#[cfg(target_os = "android")]
|
||||
mod unsupported;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub use server::{restart_server, send_message, stop_server};
|
||||
pub use server::Server;
|
||||
#[cfg(target_os = "android")]
|
||||
pub use unsupported::{restart_server, send_message, stop_server};
|
||||
pub use unsupported::Server;
|
||||
|
||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
Connected { pid: Option<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>;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,265 +1,471 @@
|
||||
use super::frame::{
|
||||
self, FrameReader, IO_POLL_INTERVAL, TYPE_CONNECTED, TYPE_DATA, TYPE_DISCONNECTED, TYPE_ERROR,
|
||||
TYPE_READY,
|
||||
};
|
||||
use super::platform;
|
||||
use super::queue::{enqueue_message, MessageSender, MAX_PENDING_MESSAGES, SEND_QUEUE_TIMEOUT};
|
||||
use crate::frb_generated::StreamSink;
|
||||
use flutter_rust_bridge::for_generated::SseCodec;
|
||||
use interprocess::local_socket::prelude::*;
|
||||
use interprocess::local_socket::{GenericFilePath, ListenerNonblockingMode, ListenerOptions};
|
||||
use super::outbox::Outbox;
|
||||
use super::{access, frame, Event, EventSink};
|
||||
use interprocess::local_socket::tokio::{prelude::*, Listener, RecvHalf, SendHalf, Stream};
|
||||
use interprocess::local_socket::{GenericFilePath, ListenerOptions};
|
||||
use std::io;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::sync::{mpsc, Arc, Mutex, MutexGuard, PoisonError};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncWriteExt, BufReader};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
macro_rules! ipc_debug {
|
||||
($($arg:tt)*) => {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!($($arg)*);
|
||||
}
|
||||
};
|
||||
const STALE_SOCKET_RETRY_LIMIT: Duration = Duration::from_secs(1);
|
||||
|
||||
struct Control {
|
||||
events: Option<oneshot::Sender<EventSink>>,
|
||||
shutdown: Option<oneshot::Sender<()>>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
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<()>>,
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
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> {
|
||||
let _lifecycle = LIFECYCLE
|
||||
.lock()
|
||||
.map_err(|e| format!("Lifecycle lock poisoned: {e}"))?;
|
||||
|
||||
stop_server_thread()?;
|
||||
platform::cleanup_socket(&name).map_err(|e| format!("Failed to remove stale socket: {e}"))?;
|
||||
|
||||
RUNNING.store(true, Ordering::SeqCst);
|
||||
let handle = thread::Builder::new()
|
||||
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 || 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(())
|
||||
.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 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 events(&self, sink: EventSink) {
|
||||
if let Some(events) = self.control().events.take() {
|
||||
let _ = events.send(sink);
|
||||
}
|
||||
}
|
||||
|
||||
fn io_loop(name: String, sink: StreamSink<Vec<u8>, SseCodec>) {
|
||||
let fs_name = match name.clone().to_fs_name::<GenericFilePath>() {
|
||||
Ok(name) => name,
|
||||
Err(e) => {
|
||||
report_error(&sink, format!("name error: {e}"));
|
||||
finish_server(&name);
|
||||
return;
|
||||
pub fn send(&self, payload: &[u8]) -> io::Result<()> {
|
||||
self.outbox.push(frame::encode(payload)?)
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let worker = {
|
||||
let mut control = self.control();
|
||||
control.events = None;
|
||||
control.shutdown = None;
|
||||
control.worker.take()
|
||||
};
|
||||
|
||||
let options = platform::restrict_listener_mode(ListenerOptions::new().name(fs_name));
|
||||
let listener = match options.create_sync() {
|
||||
Ok(listener) => listener,
|
||||
Err(e) => {
|
||||
report_error(&sink, format!("bind error: {e}"));
|
||||
finish_server(&name);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = platform::restrict_socket_to_owner(&name) {
|
||||
report_error(&sink, format!("socket permission error: {e}"));
|
||||
finish_server(&name);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = listener.set_nonblocking(ListenerNonblockingMode::Accept) {
|
||||
report_error(&sink, format!("listener nonblocking error: {e}"));
|
||||
finish_server(&name);
|
||||
return;
|
||||
}
|
||||
|
||||
if sink.add(frame::make_frame(TYPE_READY, &[])).is_err() {
|
||||
finish_server(&name);
|
||||
return;
|
||||
}
|
||||
|
||||
while server_active() {
|
||||
let stream = match listener.accept() {
|
||||
Ok(stream) => stream,
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(ACCEPT_POLL_INTERVAL);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
if server_active() {
|
||||
report_error(&sink, format!("accept error: {e}"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = platform::authorize_peer(&stream) {
|
||||
ipc_debug!("[IPC] rejected connection: {e}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = stream.set_nonblocking(true) {
|
||||
report_error(&sink, format!("stream nonblocking error: {e}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
let connected_payload = match platform::connected_payload(&stream) {
|
||||
Ok(payload) => payload,
|
||||
Err(e) => {
|
||||
report_error(&sink, format!("peer credentials error: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let (sender, rx, pending_bytes) = MessageSender::channel(MAX_PENDING_MESSAGES);
|
||||
match STATE.lock() {
|
||||
Ok(mut state) if server_active() => state.sender = Some(sender),
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
report_error(&sink, format!("state lock error: {e}"));
|
||||
break;
|
||||
if let Some(worker) = worker {
|
||||
let _ = worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
if sink
|
||||
.add(frame::make_frame(TYPE_CONNECTED, &connected_payload))
|
||||
.is_err()
|
||||
fn control(&self) -> MutexGuard<'_, Control> {
|
||||
self.control.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
address: String,
|
||||
bound: mpsc::SyncSender<io::Result<()>>,
|
||||
events: oneshot::Receiver<EventSink>,
|
||||
shutdown: oneshot::Receiver<()>,
|
||||
outbox: Arc<Outbox>,
|
||||
) {
|
||||
let runtime = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
break;
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
let _ = bound.send(Err(error));
|
||||
return;
|
||||
}
|
||||
|
||||
let (mut receiver, mut sender) = stream.split();
|
||||
let connection_running = Arc::new(AtomicBool::new(true));
|
||||
let writer_running = Arc::clone(&connection_running);
|
||||
let writer = thread::spawn(move || {
|
||||
let mut error = None;
|
||||
let is_running = || writer_running.load(Ordering::SeqCst) && server_active();
|
||||
while is_running() {
|
||||
match rx.recv_timeout(IO_POLL_INTERVAL) {
|
||||
Ok(data) => {
|
||||
pending_bytes.fetch_sub(data.len(), Ordering::SeqCst);
|
||||
if let Err(e) = frame::write_frame(&mut sender, &data, &is_running) {
|
||||
if e.kind() != io::ErrorKind::Interrupted {
|
||||
error = Some(format!("write error: {e}"));
|
||||
};
|
||||
runtime.block_on(async {
|
||||
let listener = match listen(&address) {
|
||||
Ok(listener) => listener,
|
||||
Err(error) => {
|
||||
let _ = bound.send(Err(error));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
};
|
||||
let _ = bound.send(Ok(()));
|
||||
let serve = async {
|
||||
if let Ok(emit) = events.await {
|
||||
accept_peers(&listener, &emit, &outbox).await;
|
||||
}
|
||||
};
|
||||
tokio::select! {
|
||||
_ = serve => {}
|
||||
_ = shutdown => {}
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => continue,
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
writer_running.store(false, Ordering::SeqCst);
|
||||
error
|
||||
outbox.close();
|
||||
});
|
||||
}
|
||||
|
||||
let mut frame_reader = FrameReader::default();
|
||||
while connection_running.load(Ordering::SeqCst) && server_active() {
|
||||
#[cfg(windows)]
|
||||
let poll_result = frame_reader.poll(&mut platform::PipeReader {
|
||||
receiver: &mut receiver,
|
||||
});
|
||||
#[cfg(not(windows))]
|
||||
let poll_result = frame_reader.poll(&mut receiver);
|
||||
fn listen(address: &str) -> io::Result<Listener> {
|
||||
let options = ListenerOptions::new()
|
||||
.name(address.to_fs_name::<GenericFilePath>()?)
|
||||
.try_overwrite(true)
|
||||
.max_spin_time(STALE_SOCKET_RETRY_LIMIT);
|
||||
let listener = access::owner_only(options).create_tokio()?;
|
||||
access::restrict_socket(address)?;
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
match poll_result {
|
||||
Ok(Some(data)) => {
|
||||
if sink.add(frame::make_frame(TYPE_DATA, &data)).is_err() {
|
||||
break;
|
||||
async fn accept_peers(listener: &Listener, emit: &EventSink, outbox: &Outbox) {
|
||||
loop {
|
||||
let stream = match listener.accept().await {
|
||||
Ok(stream) => stream,
|
||||
Err(error) => {
|
||||
emit(Event::Failed(format!("accept error: {error}")));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let pid = match access::admit(&stream) {
|
||||
Ok(pid) => pid,
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!("[IPC] rejected connection: {error}");
|
||||
}
|
||||
Ok(None) => thread::sleep(IO_POLL_INTERVAL),
|
||||
Err(e) => {
|
||||
if !frame::is_expected_disconnect_error(&e) && server_active() {
|
||||
ipc_debug!("[IPC] read error: {e}, raw={:?}", e.raw_os_error());
|
||||
report_error(&sink, format!("read error: {e}"));
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
};
|
||||
// The host may send as soon as it sees `Connected`.
|
||||
outbox.open();
|
||||
let listening = emit(Event::Connected { pid });
|
||||
let error = if listening {
|
||||
exchange(stream, emit, outbox).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
outbox.close();
|
||||
if !listening || !emit(Event::Disconnected { error }) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connection_running.store(false, Ordering::SeqCst);
|
||||
if let Ok(mut state) = STATE.lock() {
|
||||
state.sender = None;
|
||||
}
|
||||
if let Ok(Some(message)) = writer.join() {
|
||||
report_error(&sink, message);
|
||||
}
|
||||
if server_active() && sink.add(frame::make_frame(TYPE_DISCONNECTED, &[])).is_err() {
|
||||
break;
|
||||
async fn exchange(stream: Stream, emit: &EventSink, outbox: &Outbox) -> Option<String> {
|
||||
let (reader, writer) = stream.split();
|
||||
let result = tokio::select! {
|
||||
result = receive(reader, emit) => result,
|
||||
result = transmit(writer, outbox) => result,
|
||||
};
|
||||
match result {
|
||||
Err(error) if !is_peer_gone(&error) => Some(error.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
finish_server(&name);
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn transmit(mut writer: SendHalf, outbox: &Outbox) -> io::Result<()> {
|
||||
loop {
|
||||
let frame = outbox.next().await;
|
||||
writer.write_all(&frame).await?;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_peer_gone(error: &io::Error) -> bool {
|
||||
matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::mpsc::{channel, Receiver};
|
||||
use std::time::Instant;
|
||||
|
||||
const WAIT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn socket_path() -> String {
|
||||
static NEXT: AtomicUsize = AtomicUsize::new(0);
|
||||
let name = format!(
|
||||
"flclash-ipc-{}-{}.sock",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed),
|
||||
);
|
||||
std::env::temp_dir().join(name).to_str().unwrap().to_owned()
|
||||
}
|
||||
|
||||
fn attach(server: &Server) -> Receiver<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));
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
use crate::frb_generated::StreamSink;
|
||||
use flutter_rust_bridge::for_generated::SseCodec;
|
||||
use super::EventSink;
|
||||
use std::io;
|
||||
|
||||
// Android loads the Core in-process; these entry points exist only so that one
|
||||
// set of bindings serves every platform.
|
||||
const UNSUPPORTED: &str = "IPC server is not available on this platform";
|
||||
// Android loads the Core in-process; this keeps one set of bindings for every platform.
|
||||
pub struct Server;
|
||||
|
||||
pub fn restart_server(_name: String, _sink: StreamSink<Vec<u8>, SseCodec>) -> Result<(), String> {
|
||||
Err(UNSUPPORTED.into())
|
||||
fn unsupported() -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"IPC server is not available on this platform",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn stop_server() -> Result<(), String> {
|
||||
Ok(())
|
||||
impl Server {
|
||||
pub fn bind(_address: String) -> io::Result<Self> {
|
||||
Err(unsupported())
|
||||
}
|
||||
|
||||
pub fn send_message(_data: Vec<u8>) -> Result<(), String> {
|
||||
Err(UNSUPPORTED.into())
|
||||
pub fn events(&self, _sink: EventSink) {}
|
||||
|
||||
pub fn send(&self, _payload: &[u8]) -> io::Result<()> {
|
||||
Err(unsupported())
|
||||
}
|
||||
|
||||
pub fn close(&self) {}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ mod tests {
|
||||
assert_eq!(result["entries"], json!({ "a": 0, "b": 1 }));
|
||||
}
|
||||
|
||||
fn overwrite_fixture() -> Json {
|
||||
fn extension_fixture() -> Json {
|
||||
let script = include_str!("../../tests/fixtures/profile_script.js");
|
||||
let config = include_str!("../../tests/fixtures/profile_config.json");
|
||||
|
||||
@@ -234,8 +234,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_a_profile_overwrite_end_to_end() {
|
||||
let result = overwrite_fixture();
|
||||
fn runs_a_profile_extension_end_to_end() {
|
||||
let result = extension_fixture();
|
||||
let proxies = result["proxies"].as_array().unwrap();
|
||||
let groups = result["proxy-groups"].as_array().unwrap();
|
||||
let rules = result["rules"].as_array().unwrap();
|
||||
@@ -247,8 +247,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_overwrite_renames_nodes_and_rewrites_the_chains_through_them() {
|
||||
let result = overwrite_fixture();
|
||||
fn the_extension_renames_nodes_and_rewrites_the_chains_through_them() {
|
||||
let result = extension_fixture();
|
||||
let proxies = result["proxies"].as_array().unwrap();
|
||||
let named = |name: &str| {
|
||||
proxies
|
||||
@@ -266,8 +266,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_overwrite_merges_dns_instead_of_replacing_it() {
|
||||
let dns = overwrite_fixture()["dns"].clone();
|
||||
fn the_extension_merges_dns_instead_of_replacing_it() {
|
||||
let dns = extension_fixture()["dns"].clone();
|
||||
|
||||
assert_eq!(dns["enhanced-mode"], json!("fake-ip"));
|
||||
assert_eq!(dns["nameserver"], json!(["223.5.5.5", "119.29.29.29"]));
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* A profile overwrite script written for this test suite. It does the work a
|
||||
* real overwrite does — drop subscription notices, canonicalise node names,
|
||||
* A profile extension script written for this test suite. It does the work a
|
||||
* real one does — drop subscription notices, canonicalise node names,
|
||||
* group by region, rate and dedicated line, build policy groups and rules,
|
||||
* merge DNS — so the engine is measured against how such scripts are written
|
||||
* rather than against a checklist of language features.
|
||||
|
||||
@@ -14,6 +14,13 @@ File _resolveSource(String relativePath) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
String _body(String source, String signature) {
|
||||
final start = source.indexOf(signature);
|
||||
expect(start, isNonNegative, reason: signature);
|
||||
final end = source.indexOf('\n}\n', start);
|
||||
return source.substring(start, end);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late String pluginSource;
|
||||
|
||||
@@ -42,16 +49,39 @@ void main() {
|
||||
});
|
||||
|
||||
test('windows rejected show leaves the visible menu untouched', () {
|
||||
expect(pluginSource, contains('bool applied = ApplyIcon(!visible_);'));
|
||||
expect(
|
||||
pluginSource,
|
||||
contains('''
|
||||
if (!applied) {
|
||||
return false;
|
||||
}
|
||||
visible_ = true;
|
||||
final show = _body(pluginSource, 'bool TrayPlugin::Show(');
|
||||
final lastRejection = show.lastIndexOf('return false;');
|
||||
final rebuild = show.indexOf('RebuildMenu(menu_, *items);');
|
||||
|
||||
const flutter::EncodableList* items'''),
|
||||
expect(lastRejection, isNonNegative);
|
||||
expect(rebuild, greaterThan(lastRejection));
|
||||
});
|
||||
|
||||
test('windows retries an icon the shell refused at sign-in', () {
|
||||
final show = _body(pluginSource, 'bool TrayPlugin::Show(');
|
||||
|
||||
expect(show, contains('ScheduleRestore();'));
|
||||
expect(show.trimRight(), endsWith('return true;'));
|
||||
expect(
|
||||
_body(pluginSource, 'void TrayPlugin::RestoreIcon('),
|
||||
contains('ApplyIcon(false) || ApplyIcon(true)'),
|
||||
);
|
||||
expect(pluginSource, contains('message == WM_TIMER'));
|
||||
});
|
||||
|
||||
test('windows hide deletes an icon whose add may have landed', () {
|
||||
final hide = _body(pluginSource, 'void TrayPlugin::Hide(');
|
||||
|
||||
expect(
|
||||
hide,
|
||||
contains('''
|
||||
if (icon_requested_) {
|
||||
::Shell_NotifyIconW(NIM_DELETE, &icon_data_);
|
||||
}'''),
|
||||
);
|
||||
expect(
|
||||
hide.indexOf('icon_requested_ = false;'),
|
||||
greaterThan(hide.indexOf('NIM_DELETE')),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace {
|
||||
|
||||
constexpr UINT kTrayCallbackMessage = WM_USER + 1;
|
||||
constexpr UINT kTrayIconId = 1;
|
||||
constexpr UINT_PTR kRestoreTimerId = 0x54524159; // "TRAY"
|
||||
constexpr UINT kRestoreIntervalMilliseconds = 2000;
|
||||
|
||||
const flutter::EncodableValue* ValueAt(const flutter::EncodableMap& map,
|
||||
const char* key) {
|
||||
@@ -188,15 +190,6 @@ bool TrayPlugin::Show(const flutter::EncodableMap& arguments) {
|
||||
tool_tip_ = Utf16FromUtf8(*tool_tip);
|
||||
}
|
||||
|
||||
bool applied = ApplyIcon(!visible_);
|
||||
if (!applied && visible_) {
|
||||
applied = ApplyIcon(true);
|
||||
}
|
||||
if (!applied) {
|
||||
return false;
|
||||
}
|
||||
visible_ = true;
|
||||
|
||||
const flutter::EncodableList* items = ListAt(arguments, "menu");
|
||||
if (items != nullptr) {
|
||||
if (menu_ == nullptr) {
|
||||
@@ -205,13 +198,40 @@ bool TrayPlugin::Show(const flutter::EncodableMap& arguments) {
|
||||
RebuildMenu(menu_, *items);
|
||||
}
|
||||
|
||||
icon_requested_ = true;
|
||||
bool applied = ApplyIcon(!visible_);
|
||||
if (!applied && visible_) {
|
||||
applied = ApplyIcon(true);
|
||||
}
|
||||
if (applied) {
|
||||
visible_ = true;
|
||||
} else {
|
||||
ScheduleRestore();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void TrayPlugin::ScheduleRestore() {
|
||||
::SetTimer(MainWindow(), kRestoreTimerId, kRestoreIntervalMilliseconds,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
// An add that timed out may still have landed, so modify goes first.
|
||||
void TrayPlugin::RestoreIcon() {
|
||||
if (ApplyIcon(false) || ApplyIcon(true)) {
|
||||
visible_ = true;
|
||||
::KillTimer(MainWindow(), kRestoreTimerId);
|
||||
} else {
|
||||
ScheduleRestore();
|
||||
}
|
||||
}
|
||||
|
||||
void TrayPlugin::Hide() {
|
||||
if (visible_) {
|
||||
::KillTimer(MainWindow(), kRestoreTimerId);
|
||||
if (icon_requested_) {
|
||||
::Shell_NotifyIconW(NIM_DELETE, &icon_data_);
|
||||
}
|
||||
icon_requested_ = false;
|
||||
if (icon_data_.hIcon != nullptr) {
|
||||
::DestroyIcon(icon_data_.hIcon);
|
||||
}
|
||||
@@ -267,12 +287,17 @@ std::optional<LRESULT> TrayPlugin::HandleWindowProc(HWND window,
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (message == WM_TIMER && wparam == kRestoreTimerId) {
|
||||
RestoreIcon();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const bool should_restore =
|
||||
(taskbar_created_message_ != 0 && message == taskbar_created_message_) ||
|
||||
(message == WM_POWERBROADCAST && (wparam == PBT_APMRESUMEAUTOMATIC ||
|
||||
wparam == PBT_APMRESUMESUSPEND));
|
||||
if (should_restore && visible_) {
|
||||
ApplyIcon(true);
|
||||
if (should_restore && icon_requested_) {
|
||||
RestoreIcon();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
|
||||
@@ -37,6 +37,8 @@ class TrayPlugin : public flutter::Plugin {
|
||||
void Hide();
|
||||
bool OpenMenu();
|
||||
bool ApplyIcon(bool add);
|
||||
void RestoreIcon();
|
||||
void ScheduleRestore();
|
||||
void RebuildMenu(HMENU menu, const flutter::EncodableList& items);
|
||||
void SendEvent(const char* name, const flutter::EncodableValue& arguments);
|
||||
|
||||
@@ -53,6 +55,7 @@ class TrayPlugin : public flutter::Plugin {
|
||||
HMENU menu_ = nullptr;
|
||||
std::wstring tool_tip_;
|
||||
bool visible_ = false;
|
||||
bool icon_requested_ = false;
|
||||
|
||||
UINT taskbar_created_message_ = 0;
|
||||
int window_proc_id_ = -1;
|
||||
|
||||
@@ -46,7 +46,9 @@ int WindowController::TopBorderHeight() const {
|
||||
}
|
||||
|
||||
void WindowController::PlaceView() const {
|
||||
if (hwnd_ == nullptr || view_ == nullptr) {
|
||||
// Minimizing changes the frame too. Fitting the view to the sliver of client
|
||||
// area left would lay the app out at mobile width, which a restore then shows.
|
||||
if (hwnd_ == nullptr || view_ == nullptr || IsMinimized()) {
|
||||
return;
|
||||
}
|
||||
RECT client{};
|
||||
|
||||
@@ -50,8 +50,8 @@ struct WindowStyle {
|
||||
// Rows the controller keeps clear above the view and paints transparent.
|
||||
// Windows 10 draws the top border only inside a frame extended over the
|
||||
// whole caption (a shorter non-client strip makes DWM draw the caption), so
|
||||
// Apply() extends it there; Windows 11 draws the border into the 1 px
|
||||
// non-client strip WM_NCCALCSIZE leaves. Zero while an accent effect is on,
|
||||
// Apply() extends it there; Windows 11 draws the border into the non-client
|
||||
// strip, one physical pixel tall, that WM_NCCALCSIZE leaves. Zero while an accent effect is on,
|
||||
// maximized or full screen.
|
||||
int TopBorderHeight(HWND hwnd) const;
|
||||
};
|
||||
|
||||
@@ -37,8 +37,11 @@ use windows_sys::Win32::System::JobObjects::{
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(super) const LISTEN_PORT: u16 = 47890;
|
||||
#[cfg(not(any(
|
||||
all(feature = "windows-service", target_os = "windows"),
|
||||
target_os = "linux"
|
||||
)))]
|
||||
const LISTEN_PORT: u16 = 47890;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const CORE_PIPE_PREFIX: &str = r"\\.\pipe\FlClashCore_";
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -356,6 +359,36 @@ fn lock_surviving_poison<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
|
||||
/// A Core whose app went away exits on its own, and stays a zombie until it is
|
||||
/// waited on; without this nothing waits before the next start or stop.
|
||||
fn reap_exited_core(session_id: &str) {
|
||||
let deadline = Instant::now() + CORE_EXIT_TIMEOUT;
|
||||
loop {
|
||||
{
|
||||
let mut managed = lock_surviving_poison(&MANAGED_CORE);
|
||||
let Some(core) = managed
|
||||
.as_mut()
|
||||
.filter(|core| core.session_id == session_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match core.child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
log_message(format!("Core exited on its own: {status}"));
|
||||
*managed = None;
|
||||
return;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(_) => return,
|
||||
}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return;
|
||||
}
|
||||
thread::sleep(CORE_EXIT_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn release_managed_core(managed: &mut Option<ManagedCore>) -> Result<(), Error> {
|
||||
let Some(core) = managed.as_mut() else {
|
||||
return Ok(());
|
||||
@@ -418,7 +451,12 @@ fn start(start_params: StartParams) -> warp::reply::Response {
|
||||
if let Err(error) = super::linux::ensure_owner_socket(&start_params.address) {
|
||||
return error_response("invalidRequest", error.to_string(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
launch(start_params)
|
||||
}
|
||||
|
||||
/// Releases the managed Core before anything else, so a request reaching this
|
||||
/// point must already be accepted: a rejected one has to leave it running.
|
||||
fn launch(start_params: StartParams) -> warp::reply::Response {
|
||||
let mut managed = lock_surviving_poison(&MANAGED_CORE);
|
||||
if let Err(error) = release_managed_core(&mut managed) {
|
||||
log_message(format!(
|
||||
@@ -446,6 +484,7 @@ fn start(start_params: StartParams) -> warp::reply::Response {
|
||||
let process_id = child.id();
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
let reader = io::BufReader::new(stderr);
|
||||
let session_id = start_params.session_id.clone();
|
||||
thread::spawn(move || {
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
@@ -457,6 +496,7 @@ fn start(start_params: StartParams) -> warp::reply::Response {
|
||||
}
|
||||
}
|
||||
}
|
||||
reap_exited_core(&session_id);
|
||||
});
|
||||
}
|
||||
*managed = match ManagedCore::adopt(start_params.session_id.clone(), child) {
|
||||
@@ -1077,7 +1117,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
#[allow(clippy::await_holding_lock)]
|
||||
async fn start_releases_the_managed_core_before_rejecting_an_unverified_core() {
|
||||
async fn launch_releases_the_managed_core_before_rejecting_an_unverified_core() {
|
||||
let _state = lock_process_state();
|
||||
*lock_surviving_poison(&MANAGED_CORE) = Some(
|
||||
ManagedCore::adopt(
|
||||
@@ -1087,20 +1127,56 @@ mod tests {
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let response = warp::test::request()
|
||||
.method("POST")
|
||||
.path("/start")
|
||||
.json(&StartParams {
|
||||
let response = launch(StartParams {
|
||||
address: ALLOWED_CORE_ADDRESS.to_string(),
|
||||
session_id: "0123456789abcdef0123456789abcdef".to_string(),
|
||||
})
|
||||
.reply(&routes())
|
||||
.await;
|
||||
});
|
||||
|
||||
assert_eq!(response.status(), StatusCode::CONFLICT);
|
||||
let body: serde_json::Value = serde_json::from_slice(response.body()).unwrap();
|
||||
assert_eq!(body["code"], "coreVerificationFailed");
|
||||
assert!(lock_surviving_poison(&MANAGED_CORE).is_none());
|
||||
let body: serde_json::Value = serde_json::from_slice(
|
||||
&warp::hyper::body::to_bytes(response.into_body())
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(body["code"], "coreVerificationFailed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_core_that_exits_on_its_own_is_reaped() {
|
||||
let _state = lock_process_state();
|
||||
let session_id = "0123456789abcdef0123456789abcdef";
|
||||
*lock_surviving_poison(&MANAGED_CORE) =
|
||||
Some(ManagedCore::adopt(session_id.to_string(), spawn_placeholder_core()).unwrap());
|
||||
|
||||
reap_exited_core(session_id);
|
||||
|
||||
assert!(lock_surviving_poison(&MANAGED_CORE).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reaping_leaves_a_running_or_newer_core_owned() {
|
||||
let _state = lock_process_state();
|
||||
let session_id = "0123456789abcdef0123456789abcdef";
|
||||
*lock_surviving_poison(&MANAGED_CORE) =
|
||||
Some(ManagedCore::adopt(session_id.to_string(), spawn_placeholder_core()).unwrap());
|
||||
|
||||
reap_exited_core("fedcba9876543210fedcba9876543210");
|
||||
assert!(lock_surviving_poison(&MANAGED_CORE).is_some());
|
||||
|
||||
adopt_core(session_id);
|
||||
reap_exited_core(session_id);
|
||||
|
||||
let mut managed = lock_surviving_poison(&MANAGED_CORE);
|
||||
assert!(managed
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.child
|
||||
.try_wait()
|
||||
.unwrap()
|
||||
.is_none());
|
||||
release_managed_core(&mut managed).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::service::hub::{
|
||||
ensure_core_sha256_configured, log_message, release_managed_core_on_shutdown, routes,
|
||||
LISTEN_PORT,
|
||||
};
|
||||
use crate::service::owner::{client_pid, same_sid, TcpConnection};
|
||||
|
||||
@@ -9,7 +8,7 @@ use std::future::Future;
|
||||
use std::io::{Error, Result as IoResult};
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::pin::Pin;
|
||||
use std::ptr::null_mut;
|
||||
use std::ptr::{null, null_mut};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
@@ -17,18 +16,26 @@ use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::time::Sleep;
|
||||
use tokio_stream::Stream;
|
||||
use windows_sys::Win32::Foundation::{
|
||||
CloseHandle, LocalFree, ERROR_INSUFFICIENT_BUFFER, HANDLE, NO_ERROR,
|
||||
CloseHandle, LocalFree, ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS, HANDLE, NO_ERROR,
|
||||
};
|
||||
use windows_sys::Win32::NetworkManagement::IpHelper::{
|
||||
GetExtendedTcpTable, MIB_TCPTABLE_OWNER_PID, TCP_TABLE_OWNER_PID_ALL,
|
||||
};
|
||||
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
|
||||
use windows_sys::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER};
|
||||
use windows_sys::Win32::System::Registry::{
|
||||
RegCloseKey, RegCreateKeyExW, RegDeleteKeyW, RegSetValueExW, HKEY, HKEY_LOCAL_MACHINE,
|
||||
KEY_SET_VALUE, REG_DWORD, REG_OPTION_VOLATILE,
|
||||
};
|
||||
use windows_sys::Win32::System::Threading::{
|
||||
OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
};
|
||||
|
||||
const AF_INET: u32 = 2;
|
||||
/// Hyper-V and WinNAT reserve ranges that can cover any fixed port; only admins
|
||||
/// write this volatile key under the service's own, and a reboot clears it.
|
||||
const PORT_KEY: &str = r"SYSTEM\CurrentControlSet\Services\FlClashHelperService\Runtime";
|
||||
const PORT_VALUE: &str = "Port";
|
||||
const TABLE_ATTEMPTS: usize = 5;
|
||||
const ACCEPT_RETRY_DELAY: Duration = Duration::from_secs(1);
|
||||
|
||||
@@ -199,6 +206,61 @@ impl Stream for AuthorizedIncoming {
|
||||
}
|
||||
}
|
||||
|
||||
fn wide(value: &str) -> Vec<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>(
|
||||
owner_sid: String,
|
||||
shutdown: F,
|
||||
@@ -210,9 +272,15 @@ where
|
||||
{
|
||||
ensure_core_sha256_configured()?;
|
||||
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, LISTEN_PORT))
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("bind helper server: {error}"))?;
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.map_err(|error| anyhow::anyhow!("read helper server port: {error}"))?
|
||||
.port();
|
||||
let published = PublishedPort::publish(port)
|
||||
.map_err(|error| anyhow::anyhow!("publish helper server port: {error}"))?;
|
||||
on_started()?;
|
||||
let incoming = AuthorizedIncoming {
|
||||
listener,
|
||||
@@ -222,6 +290,7 @@ where
|
||||
warp::serve(routes())
|
||||
.serve_incoming_with_graceful_shutdown(incoming, shutdown)
|
||||
.await;
|
||||
drop(published);
|
||||
release_managed_core_on_shutdown();
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
@@ -49,7 +49,6 @@ void main() {
|
||||
const config = Config(
|
||||
themeProps: defaultThemeProps,
|
||||
currentProfileId: 42,
|
||||
overrideDns: true,
|
||||
excludeSSIDs: ['home'],
|
||||
);
|
||||
|
||||
@@ -58,7 +57,6 @@ void main() {
|
||||
|
||||
expect(restored, isNotNull);
|
||||
expect(restored!.currentProfileId, 42);
|
||||
expect(restored.overrideDns, isTrue);
|
||||
expect(restored.excludeSSIDs, ['home']);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ class _RecordedRun {
|
||||
|
||||
String get key =>
|
||||
arguments.isEmpty ? executable : '$executable ${arguments.first}';
|
||||
|
||||
String get commandLine => [executable, ...arguments].join(' ');
|
||||
}
|
||||
|
||||
/// Stands in for `Process.run`, keyed by executable plus its first argument so
|
||||
@@ -47,6 +49,17 @@ class _FakeProcesses {
|
||||
_failures.add(key);
|
||||
}
|
||||
|
||||
final List<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 {
|
||||
final recorded = _RecordedRun(executable, arguments);
|
||||
runs.add(recorded);
|
||||
@@ -55,8 +68,14 @@ class _FakeProcesses {
|
||||
}
|
||||
return ProcessResult(
|
||||
0,
|
||||
_exitCodes[recorded.key] ?? _exitCodes[executable] ?? 0,
|
||||
_stdout[recorded.key] ?? _stdout[executable] ?? '',
|
||||
_exitCodes[recorded.commandLine] ??
|
||||
_exitCodes[recorded.key] ??
|
||||
_exitCodes[executable] ??
|
||||
0,
|
||||
_stdout[recorded.commandLine] ??
|
||||
_stdout[recorded.key] ??
|
||||
_stdout[executable] ??
|
||||
'',
|
||||
'',
|
||||
);
|
||||
}
|
||||
@@ -94,6 +113,7 @@ void main() {
|
||||
|
||||
late Directory root;
|
||||
late _FakeProcesses processes;
|
||||
String? password;
|
||||
|
||||
setUpAll(() {
|
||||
root = Directory.systemTemp.createTempSync('system_test');
|
||||
@@ -112,13 +132,16 @@ void main() {
|
||||
processes = _FakeProcesses();
|
||||
system.runProcess = processes.run;
|
||||
MacOS().runProcess = processes.run;
|
||||
Linux().runProcess = processes.run;
|
||||
linuxElevation.run = processes.run;
|
||||
linuxElevation.runWithInput = processes.runWithInput;
|
||||
linuxElevation.askPassword = () async => password;
|
||||
password = null;
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
system.runProcess = Process.run;
|
||||
MacOS().runProcess = Process.run;
|
||||
Linux().runProcess = Process.run;
|
||||
linuxElevation.run = Process.run;
|
||||
});
|
||||
|
||||
group('statArguments', () {
|
||||
@@ -325,7 +348,10 @@ void main() {
|
||||
test('Linux hands the Core to the requesting user\'s group', () {
|
||||
final shell = System.linuxElevationShell('/opt/FlClash/FlClashCore');
|
||||
|
||||
expect(shell, startsWith(r'group=$(id -g "$PKEXEC_UID") && '));
|
||||
expect(
|
||||
shell,
|
||||
startsWith(r'group=$(id -g "${PKEXEC_UID:-$SUDO_UID}") && '),
|
||||
);
|
||||
expect(shell, contains(r'chown "root:$group" '));
|
||||
expect(shell, endsWith("chmod 4750 '/opt/FlClash/FlClashCore'"));
|
||||
});
|
||||
@@ -375,10 +401,60 @@ void main() {
|
||||
expect(await Linux().installService(), isFalse);
|
||||
});
|
||||
|
||||
test('reports a host with no pkexec at all', () async {
|
||||
test('installs through cached sudo credentials without pkexec', () async {
|
||||
processes.stubThrow('pkexec');
|
||||
|
||||
expect(await Linux().installService(), isTrue);
|
||||
expect(processes.argumentsFor('sudo'), [
|
||||
'-n',
|
||||
'--',
|
||||
appPath.helperPath,
|
||||
'install',
|
||||
]);
|
||||
expect(processes.inputs, isEmpty);
|
||||
});
|
||||
|
||||
test('asks for the sudo password when no polkit agent answers', () async {
|
||||
processes.stub('pkexec', '', exitCode: 127);
|
||||
processes.stub('sudo -n', '', exitCode: 1);
|
||||
password = 'secret';
|
||||
|
||||
expect(await Linux().installService(), isTrue);
|
||||
expect(processes.runs.last.arguments, [
|
||||
'-S',
|
||||
'-p',
|
||||
'',
|
||||
'--',
|
||||
appPath.helperPath,
|
||||
'install',
|
||||
]);
|
||||
expect(processes.inputs, ['secret\n']);
|
||||
});
|
||||
|
||||
test('reports a command that failed under cached sudo', () async {
|
||||
processes.stubThrow('pkexec');
|
||||
processes.stub('sudo -n', '', exitCode: 1);
|
||||
processes.stub('sudo -n true', '');
|
||||
password = 'secret';
|
||||
|
||||
expect(await Linux().installService(), isFalse);
|
||||
expect(processes.ran('sudo -S'), isFalse);
|
||||
expect(processes.inputs, isEmpty);
|
||||
});
|
||||
|
||||
test('gives up when the sudo prompt is cancelled', () async {
|
||||
processes.stubThrow('pkexec');
|
||||
processes.stub('sudo -n', '', exitCode: 1);
|
||||
|
||||
expect(await Linux().installService(), isFalse);
|
||||
expect(processes.ran('sudo -S'), isFalse);
|
||||
});
|
||||
|
||||
test('does not ask sudo after the polkit dialog is dismissed', () async {
|
||||
processes.stub('pkexec', '', exitCode: 126);
|
||||
|
||||
expect(await Linux().installService(), isFalse);
|
||||
expect(processes.ran('sudo'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -326,11 +326,21 @@ void main() {
|
||||
overrides: [
|
||||
selectedProxyNameProvider('Proxy').overrideWithValue('A'),
|
||||
trayDelaysProvider.overrideWithValue({
|
||||
'Proxy': {'A': 120, 'B': -1},
|
||||
'Proxy': {'A': 120, 'B': -1, 'C': -2},
|
||||
}),
|
||||
],
|
||||
);
|
||||
await update(_trayState(groups: [_proxyGroup]));
|
||||
const group = Group(
|
||||
name: 'Proxy',
|
||||
type: GroupType.Selector,
|
||||
all: [
|
||||
Proxy(name: 'A', type: 'Direct'),
|
||||
Proxy(name: 'B', type: 'Direct'),
|
||||
Proxy(name: 'C', type: 'Direct'),
|
||||
Proxy(name: 'D', type: 'Direct'),
|
||||
],
|
||||
);
|
||||
await update(_trayState(groups: [group]));
|
||||
|
||||
final submenu = _item(showCall(), 'Proxy');
|
||||
expect(submenu['detail'], '120');
|
||||
@@ -338,6 +348,7 @@ void main() {
|
||||
expect(children.map((item) => item['detail']), [
|
||||
'120',
|
||||
currentAppLocalizations.timeout,
|
||||
currentAppLocalizations.delayFailed,
|
||||
null,
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:fl_clash/common/window.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:screen_retriever/screen_retriever.dart';
|
||||
|
||||
const _windowChannel = MethodChannel('window');
|
||||
|
||||
@@ -198,6 +199,89 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
group('restoredWindowPosition', () {
|
||||
const primary = Display(
|
||||
id: 'primary',
|
||||
size: Size(1920, 1080),
|
||||
visiblePosition: Offset.zero,
|
||||
scaleFactor: 1,
|
||||
);
|
||||
// 2560x1440 physical at 150%, right of the primary: screen_retriever
|
||||
// divides its origin by its own scale.
|
||||
const secondary = Display(
|
||||
id: 'secondary',
|
||||
size: Size(1707, 960),
|
||||
visiblePosition: Offset(1280, 0),
|
||||
scaleFactor: 1.5,
|
||||
);
|
||||
|
||||
test('places a window saved on a 150% monitor back on it', () {
|
||||
const props = WindowProps(
|
||||
width: 800,
|
||||
height: 600,
|
||||
left: 2200 / 1.5,
|
||||
top: 150 / 1.5,
|
||||
scale: 1.5,
|
||||
);
|
||||
|
||||
final position = restoredWindowPosition(
|
||||
props,
|
||||
displays: const [primary, secondary],
|
||||
currentScale: 1,
|
||||
);
|
||||
|
||||
expect(position!.dx, closeTo(2200, 0.001));
|
||||
expect(position.dy, closeTo(150, 0.001));
|
||||
});
|
||||
|
||||
test('rejects a position no display shows any more', () {
|
||||
const props = WindowProps(
|
||||
width: 800,
|
||||
height: 600,
|
||||
left: 2200,
|
||||
top: 100,
|
||||
scale: 1,
|
||||
);
|
||||
|
||||
expect(
|
||||
restoredWindowPosition(
|
||||
props,
|
||||
displays: const [primary],
|
||||
currentScale: 1,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('reads a position saved without a scale at the current one', () {
|
||||
const props = WindowProps(width: 800, height: 600, left: 100, top: 50);
|
||||
|
||||
expect(
|
||||
restoredWindowPosition(
|
||||
props,
|
||||
displays: const [primary],
|
||||
currentScale: 1.25,
|
||||
),
|
||||
const Offset(100, 50),
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps logical coordinates where the platform has them', () {
|
||||
const props = WindowProps(
|
||||
width: 800,
|
||||
height: 600,
|
||||
left: 1500,
|
||||
top: 100,
|
||||
scale: 2,
|
||||
);
|
||||
|
||||
expect(
|
||||
restoredWindowPosition(props, displays: const [primary, secondary]),
|
||||
const Offset(1500, 100),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('maximized geometry is not captured', () async {
|
||||
isMaximized = true;
|
||||
|
||||
@@ -213,4 +297,37 @@ void main() {
|
||||
isMinimized = true;
|
||||
expect(await Window().captureNormalGeometry(const WindowProps()), isNull);
|
||||
});
|
||||
|
||||
test('the init failure window closes and quits through onExit', () async {
|
||||
var exits = 0;
|
||||
|
||||
await Window().showInitFailure(onExit: () async => exits++);
|
||||
|
||||
MethodCall callOf(String method) =>
|
||||
methodCalls.firstWhere((call) => call.method == method);
|
||||
expect(callOf('setPreventClose').arguments, {'value': true});
|
||||
expect(
|
||||
(callOf('setTitleBarStyle').arguments as Map)['style'],
|
||||
'normal',
|
||||
reason: 'the error screen draws no window header of its own',
|
||||
);
|
||||
|
||||
for (final event in ['close', 'should-terminate']) {
|
||||
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.handlePlatformMessage(
|
||||
_windowChannel.name,
|
||||
_windowChannel.codec.encodeMethodCall(
|
||||
MethodCall('onEvent', {'name': event}),
|
||||
),
|
||||
null,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
exits,
|
||||
2,
|
||||
reason:
|
||||
'the macOS runner cancels every quit and closing its last window '
|
||||
'leaves the process running, so both have to end in onExit',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ final class FakeDesktopCoreTransport implements DesktopCoreTransport {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send(String message) async {
|
||||
void send(List<int> frame) {
|
||||
if (state == DesktopTransportState.closed) {
|
||||
throw StateError('IPC transport is closed');
|
||||
}
|
||||
@@ -135,7 +135,7 @@ final class FakeDesktopCoreTransport implements DesktopCoreTransport {
|
||||
if (error != null) {
|
||||
Error.throwWithStackTrace(error, StackTrace.current);
|
||||
}
|
||||
sentMessages.add(message);
|
||||
sentMessages.add(utf8.decode(frame));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -16,6 +16,13 @@ const _coreSha256 =
|
||||
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
|
||||
void main() {
|
||||
test('reads the port from the key the Windows service publishes it in', () {
|
||||
final peer = File('services/helper/src/service/peer.rs').readAsStringSync();
|
||||
|
||||
expect(peer, contains('const PORT_KEY: &str = r"$helperPortKey";'));
|
||||
expect(peer, contains('const PORT_VALUE: &str = "$helperPortValue";'));
|
||||
});
|
||||
|
||||
test(
|
||||
'start returns a Helper lease identity with matching session and PID',
|
||||
() async {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -4,42 +4,91 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/core/desktop/transport.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rust_api/rust_api.dart';
|
||||
|
||||
import 'fakes.dart';
|
||||
|
||||
Uint8List _frame(int type, [List<int> payload = const []]) {
|
||||
return Uint8List.fromList([type, ...payload]);
|
||||
final class _FakeIpcServer implements IpcServer {
|
||||
final StreamController<IpcEvent> _events = StreamController<IpcEvent>();
|
||||
final List<List<int>> sent = [];
|
||||
Object? sendError;
|
||||
int closeCount = 0;
|
||||
|
||||
void _add(
|
||||
IpcEventKind kind, {
|
||||
int? pid,
|
||||
List<int> payload = const [],
|
||||
String? error,
|
||||
}) {
|
||||
_events.add(
|
||||
IpcEvent(
|
||||
kind: kind,
|
||||
pid: pid,
|
||||
payload: Uint8List.fromList(payload),
|
||||
error: error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List _processIdPayload(int processId) {
|
||||
return Uint8List(4)
|
||||
..buffer.asByteData().setUint32(0, processId, Endian.little);
|
||||
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() {
|
||||
group('IPCCoreTransport typed events', () {
|
||||
late StreamController<Uint8List> rawEvents;
|
||||
late List<List<int>> sentMessages;
|
||||
late int stopCount;
|
||||
late IPCCoreTransport transport;
|
||||
group('IpcCoreTransport', () {
|
||||
late _FakeIpcServer server;
|
||||
late List<String> boundAddresses;
|
||||
late IpcCoreTransport transport;
|
||||
|
||||
setUp(() {
|
||||
rawEvents = StreamController<Uint8List>();
|
||||
sentMessages = [];
|
||||
stopCount = 0;
|
||||
transport = IPCCoreTransport(
|
||||
server = _FakeIpcServer();
|
||||
boundAddresses = [];
|
||||
transport = IpcCoreTransport(
|
||||
address: 'test-address',
|
||||
startServer: (_) => rawEvents.stream,
|
||||
sendMessage: (data) async => sentMessages.add(data),
|
||||
stopServer: () async => stopCount++,
|
||||
bind: (address) async {
|
||||
boundAddresses.add(address);
|
||||
return server;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await transport.close();
|
||||
if (!rawEvents.isClosed) {
|
||||
await rawEvents.close();
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -47,99 +96,72 @@ void main() {
|
||||
() async {
|
||||
final events = <DesktopTransportEvent>[];
|
||||
final subscription = transport.events.listen(events.add);
|
||||
final open = transport.open();
|
||||
rawEvents.add(_frame(0x00));
|
||||
await open;
|
||||
await transport.open();
|
||||
expect(boundAddresses, ['test-address']);
|
||||
expect(transport.state, DesktopTransportState.ready);
|
||||
|
||||
final frame = transport.frames.first;
|
||||
rawEvents.add(_frame(0x01, _processIdPayload(1234)));
|
||||
rawEvents.add(_frame(0x03, utf8.encode('payload')));
|
||||
server.connect(1234);
|
||||
server.message(utf8.encode('payload'));
|
||||
expect(await frame, utf8.encode('payload'));
|
||||
rawEvents.add(_frame(0x02));
|
||||
expect(transport.state, DesktopTransportState.connected);
|
||||
server.disconnect();
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(events.whereType<TransportReady>(), hasLength(1));
|
||||
expect(events.whereType<TransportConnected>().single.pid, 1234);
|
||||
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);
|
||||
await subscription.cancel();
|
||||
},
|
||||
);
|
||||
|
||||
test('waitUntilConnected waits for a typed connected event', () async {
|
||||
final open = transport.open();
|
||||
rawEvents.add(_frame(0x00));
|
||||
await open;
|
||||
test('opening twice binds once', () async {
|
||||
await Future.wait([transport.open(), transport.open()]);
|
||||
await transport.open();
|
||||
|
||||
expect(boundAddresses, hasLength(1));
|
||||
});
|
||||
|
||||
test('waitUntilConnected waits for a connected event', () async {
|
||||
await transport.open();
|
||||
|
||||
final connected = transport.waitUntilConnected(
|
||||
const Duration(seconds: 1),
|
||||
);
|
||||
rawEvents.add(_frame(0x01, _processIdPayload(4321)));
|
||||
server.connect(4321);
|
||||
|
||||
expect((await connected).pid, 4321);
|
||||
expect(transport.state, DesktopTransportState.connected);
|
||||
});
|
||||
|
||||
test('forwards outgoing messages after ready', () async {
|
||||
final open = transport.open();
|
||||
rawEvents.add(_frame(0x00));
|
||||
await open;
|
||||
test('hands outgoing frames to the server', () async {
|
||||
await transport.open();
|
||||
|
||||
await transport.send('hello');
|
||||
transport.send(utf8.encode('hello'));
|
||||
|
||||
expect(sentMessages, [utf8.encode('hello')]);
|
||||
expect(server.sent, [utf8.encode('hello')]);
|
||||
});
|
||||
|
||||
test('emits startup failure and fails open', () async {
|
||||
final failures = transport.events
|
||||
.where((event) => event is TransportFailed)
|
||||
.cast<TransportFailed>()
|
||||
.first;
|
||||
final open = transport.open();
|
||||
rawEvents.add(_frame(0x04, utf8.encode('bind failed')));
|
||||
test('refuses to send before it is open and after it is closed', () async {
|
||||
expect(() => transport.send(const [1]), throwsStateError);
|
||||
|
||||
await expectLater(open, throwsA(isA<StateError>()));
|
||||
expect((await failures).error.toString(), contains('bind failed'));
|
||||
expect(transport.state, DesktopTransportState.failed);
|
||||
});
|
||||
await transport.open();
|
||||
await transport.close();
|
||||
|
||||
test('fails when the Rust event stream closes before ready', () async {
|
||||
final open = transport.open();
|
||||
await rawEvents.close();
|
||||
|
||||
await expectLater(open, throwsA(isA<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);
|
||||
expect(() => transport.send(const [1]), throwsStateError);
|
||||
expect(server.sent, isEmpty);
|
||||
});
|
||||
|
||||
test('propagates send failures', () async {
|
||||
final failingTransport = IPCCoreTransport(
|
||||
address: 'test-address',
|
||||
startServer: (_) => rawEvents.stream,
|
||||
sendMessage: (_) async => throw StateError('send failed'),
|
||||
stopServer: () async => stopCount++,
|
||||
);
|
||||
final open = failingTransport.open();
|
||||
rawEvents.add(_frame(0x00));
|
||||
await open;
|
||||
server.sendError = StateError('send failed');
|
||||
await transport.open();
|
||||
|
||||
await expectLater(
|
||||
failingTransport.send('hello'),
|
||||
expect(
|
||||
() => transport.send(utf8.encode('hello')),
|
||||
throwsA(
|
||||
isA<StateError>().having(
|
||||
(error) => error.message,
|
||||
@@ -148,23 +170,88 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('a failed bind fails open and the transport', () async {
|
||||
final failingTransport = IpcCoreTransport(
|
||||
address: 'test-address',
|
||||
bind: (_) async => throw StateError('bind failed'),
|
||||
);
|
||||
final failure = failingTransport.events
|
||||
.where((event) => event is TransportFailed)
|
||||
.cast<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();
|
||||
});
|
||||
|
||||
test('assigns a fresh generation after reconnecting', () async {
|
||||
final open = transport.open();
|
||||
rawEvents.add(_frame(0x00));
|
||||
await open;
|
||||
test('a server failure is terminal for the transport', () async {
|
||||
await transport.open();
|
||||
server.connect();
|
||||
await pumpEventQueue();
|
||||
final events = <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 subscription = transport.events.listen((event) {
|
||||
if (event is TransportConnected) {
|
||||
connections.add(event);
|
||||
}
|
||||
});
|
||||
rawEvents.add(_frame(0x01));
|
||||
rawEvents.add(_frame(0x02));
|
||||
rawEvents.add(_frame(0x01, _processIdPayload(4321)));
|
||||
|
||||
server.connect();
|
||||
server.disconnect();
|
||||
server.connect(4321);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(connections.map((event) => event.generation), [1, 2]);
|
||||
@@ -172,78 +259,47 @@ void main() {
|
||||
await subscription.cancel();
|
||||
});
|
||||
|
||||
test('a data frame hands out a view rather than a copy', () async {
|
||||
final open = transport.open();
|
||||
rawEvents.add(_frame(0x00));
|
||||
await open;
|
||||
test('close is idempotent', () async {
|
||||
await transport.open();
|
||||
|
||||
final frame = transport.frames.first;
|
||||
final source = _frame(0x03, utf8.encode('payload'));
|
||||
rawEvents.add(source);
|
||||
await transport.close();
|
||||
await transport.close();
|
||||
|
||||
final payload = await frame;
|
||||
expect(payload, utf8.encode('payload'));
|
||||
expect(payload.offsetInBytes, 1);
|
||||
source[1] = 0x5a;
|
||||
expect(payload[0], 0x5a);
|
||||
expect(server.closeCount, 1);
|
||||
expect(transport.state, DesktopTransportState.closed);
|
||||
await expectLater(transport.open(), throwsStateError);
|
||||
});
|
||||
|
||||
test(
|
||||
'a recovered transport stops answering with the old failure',
|
||||
() async {
|
||||
final open = transport.open();
|
||||
rawEvents.add(_frame(0x00));
|
||||
await open;
|
||||
|
||||
rawEvents.add(_frame(0x04, utf8.encode('boom')));
|
||||
await pumpEventQueue();
|
||||
expect(transport.state, DesktopTransportState.failed);
|
||||
await expectLater(
|
||||
transport.waitUntilConnected(const Duration(seconds: 1)),
|
||||
throwsA(isA<StateError>()),
|
||||
test('closing while binding closes the server that arrives', () async {
|
||||
final bound = Completer<IpcServer>();
|
||||
final slowTransport = IpcCoreTransport(
|
||||
address: 'test-address',
|
||||
bind: (_) => bound.future,
|
||||
);
|
||||
final open = slowTransport.open();
|
||||
final rejected = expectLater(open, throwsStateError);
|
||||
|
||||
rawEvents.add(_frame(0x00));
|
||||
rawEvents.add(_frame(0x01, _processIdPayload(4321)));
|
||||
await pumpEventQueue();
|
||||
await slowTransport.close();
|
||||
bound.complete(server);
|
||||
|
||||
final connected = await transport.waitUntilConnected(
|
||||
const Duration(seconds: 1),
|
||||
);
|
||||
expect(connected.pid, 4321);
|
||||
},
|
||||
);
|
||||
|
||||
test('close is idempotent', () async {
|
||||
final open = transport.open();
|
||||
rawEvents.add(_frame(0x00));
|
||||
await open;
|
||||
|
||||
await transport.close();
|
||||
await transport.close();
|
||||
|
||||
expect(stopCount, 1);
|
||||
expect(transport.state, DesktopTransportState.closed);
|
||||
await rejected;
|
||||
expect(server.closeCount, 1);
|
||||
expect(slowTransport.state, DesktopTransportState.closed);
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'transport binding forwards events after replacing its transport',
|
||||
() async {
|
||||
final firstEvents = StreamController<Uint8List>();
|
||||
final secondEvents = StreamController<Uint8List>();
|
||||
var firstStopCount = 0;
|
||||
final first = IPCCoreTransport(
|
||||
final firstServer = _FakeIpcServer();
|
||||
final secondServer = _FakeIpcServer();
|
||||
final first = IpcCoreTransport(
|
||||
address: 'first',
|
||||
startServer: (_) => firstEvents.stream,
|
||||
sendMessage: (_) async {},
|
||||
stopServer: () async => firstStopCount++,
|
||||
bind: (_) async => firstServer,
|
||||
);
|
||||
final second = IPCCoreTransport(
|
||||
final second = IpcCoreTransport(
|
||||
address: 'second',
|
||||
startServer: (_) => secondEvents.stream,
|
||||
sendMessage: (_) async {},
|
||||
stopServer: () async {},
|
||||
bind: (_) async => secondServer,
|
||||
);
|
||||
final binding = DesktopCoreTransportBinding(first);
|
||||
final connectedEvents = <TransportConnected>[];
|
||||
@@ -253,24 +309,21 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
final firstOpen = binding.open();
|
||||
firstEvents.add(_frame(0x00));
|
||||
await firstOpen;
|
||||
await binding.open();
|
||||
await binding.replace(second);
|
||||
final secondOpen = binding.open();
|
||||
secondEvents.add(_frame(0x00));
|
||||
await secondOpen;
|
||||
secondEvents.add(_frame(0x01, _processIdPayload(5678)));
|
||||
await binding.open();
|
||||
secondServer.connect(5678);
|
||||
await pumpEventQueue();
|
||||
binding.send(utf8.encode('hello'));
|
||||
|
||||
expect(firstStopCount, 1);
|
||||
expect(firstServer.closeCount, 1);
|
||||
expect(firstServer.sent, isEmpty);
|
||||
expect(secondServer.sent, [utf8.encode('hello')]);
|
||||
expect(binding.address, 'second');
|
||||
expect(connectedEvents.single.pid, 5678);
|
||||
|
||||
await subscription.cancel();
|
||||
await binding.close();
|
||||
await firstEvents.close();
|
||||
await secondEvents.close();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ const _platformModules = [
|
||||
'lib/common/window.dart',
|
||||
'lib/common/launch.dart',
|
||||
'lib/common/system_dns.dart',
|
||||
'lib/common/permission.dart',
|
||||
];
|
||||
|
||||
final _platformImport = RegExp(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/app_localizations.dart';
|
||||
import 'package:fl_clash/common/app_ports.dart';
|
||||
@@ -142,11 +143,19 @@ void main() {
|
||||
container.dispose();
|
||||
});
|
||||
|
||||
Future<WindowListener> pumpWindowManager(WidgetTester tester) async {
|
||||
Future<WindowListener> pumpWindowManager(
|
||||
WidgetTester tester, {
|
||||
Stream<ProcessSignal>? terminateSignals,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const MaterialApp(home: WindowManager(child: SizedBox.shrink())),
|
||||
child: MaterialApp(
|
||||
home: WindowManager(
|
||||
terminateSignals: terminateSignals,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
@@ -186,6 +195,22 @@ void main() {
|
||||
expect(_RecordingSystemAction.calls, ['exit']);
|
||||
});
|
||||
|
||||
testWidgets('a terminate signal exits like a terminate request', (
|
||||
tester,
|
||||
) async {
|
||||
final signals = StreamController<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 {
|
||||
final listener = await pumpWindowManager(tester);
|
||||
window.bounds = const Rect.fromLTWH(120, 64, 1000, 800);
|
||||
|
||||
@@ -30,6 +30,24 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('every Linux package names the GTK application id as its window '
|
||||
'class', () {
|
||||
final applicationId = RegExp(
|
||||
r'set\(APPLICATION_ID "([^"]+)"\)',
|
||||
).firstMatch(File('linux/CMakeLists.txt').readAsStringSync())!.group(1);
|
||||
|
||||
for (final format in ['deb', 'rpm', 'appimage']) {
|
||||
final config =
|
||||
loadYaml(
|
||||
File(
|
||||
'linux/packaging/$format/make_config.yaml',
|
||||
).readAsStringSync(),
|
||||
)
|
||||
as YamlMap;
|
||||
expect(config['startup_wm_class'], applicationId, reason: format);
|
||||
}
|
||||
});
|
||||
|
||||
test('rpm keeps the Core bytes the Helper was built against', () {
|
||||
final config =
|
||||
loadYaml(
|
||||
|
||||
@@ -45,6 +45,47 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure ClearStaleProxy;
|
||||
var
|
||||
AppPath: String;
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
AppPath := ExpandConstant('{app}\\{{EXECUTABLE_NAME}}');
|
||||
if FileExists(AppPath) then
|
||||
begin
|
||||
Exec(AppPath, '--clear-stale-proxy', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
end;
|
||||
|
||||
function PointsIntoApp(Value: String): Boolean;
|
||||
begin
|
||||
Result := Pos(Lowercase(AddBackslash(ExpandConstant('{app}'))), Lowercase(Value)) > 0;
|
||||
end;
|
||||
|
||||
// The value name is appName and the schemes are protocolSchemes, both in
|
||||
// lib/common.
|
||||
procedure RemoveUserRegistrations;
|
||||
var
|
||||
Schemes: TArrayOfString;
|
||||
Value: String;
|
||||
i: Integer;
|
||||
begin
|
||||
if RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Run', '{{APP_NAME}}', Value) and PointsIntoApp(Value) then
|
||||
begin
|
||||
RegDeleteValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Run', '{{APP_NAME}}');
|
||||
RegDeleteValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run', '{{APP_NAME}}');
|
||||
end;
|
||||
|
||||
Schemes := ['clash', 'clashmeta', 'flclash'];
|
||||
for i := 0 to GetArrayLength(Schemes)-1 do
|
||||
begin
|
||||
if RegQueryStringValue(HKCU, 'Software\Classes\' + Schemes[i] + '\shell\open\command', '', Value) and PointsIntoApp(Value) then
|
||||
begin
|
||||
RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\' + Schemes[i]);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
begin
|
||||
UnregisterHelperService;
|
||||
@@ -52,13 +93,30 @@ begin
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
if CurStep = ssPostInstall then
|
||||
begin
|
||||
ClearStaleProxy;
|
||||
end;
|
||||
end;
|
||||
|
||||
function InitializeUninstall(): Boolean;
|
||||
begin
|
||||
UnregisterHelperService;
|
||||
KillProcesses;
|
||||
ClearStaleProxy;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
begin
|
||||
if CurUninstallStep = usPostUninstall then
|
||||
begin
|
||||
RemoveUserRegistrations;
|
||||
end;
|
||||
end;
|
||||
|
||||
[Languages]
|
||||
{% for locale in LOCALES %}
|
||||
{% if locale.lang == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %}
|
||||
|
||||
+14
-3
@@ -3,13 +3,27 @@
|
||||
#include <windows.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 <algorithm>
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
// The installer and the uninstaller run this after killing the app, whose exit
|
||||
// path is what normally turns the system proxy off.
|
||||
constexpr char kClearStaleProxyArgument[] = "--clear-stale-proxy";
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
std::vector<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()) {
|
||||
SendAppLink(running);
|
||||
WindowPluginActivateWindow(running);
|
||||
@@ -28,9 +42,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
|
||||
flutter::DartProject project(L"data");
|
||||
|
||||
std::vector<std::string> command_line_arguments =
|
||||
GetCommandLineArguments();
|
||||
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
|
||||
FlutterWindow window(project);
|
||||
|
||||
Reference in New Issue
Block a user