-
Notifications
You must be signed in to change notification settings - Fork 17
Add isolated Hive store launch flag #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import 'dart:convert'; | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:cryptography_plus/cryptography_plus.dart'; | ||
| import 'package:path/path.dart' as path; | ||
|
|
||
| final class HiveStoreLaunchException implements Exception { | ||
| const HiveStoreLaunchException(this.message, [this.cause]); | ||
|
|
||
| final String message; | ||
| final Object? cause; | ||
|
|
||
| @override | ||
| String toString() => message; | ||
| } | ||
|
|
||
| final class HiveStoreLaunch { | ||
| const HiveStoreLaunch._({ | ||
| required this.fileOpenArgs, | ||
| required String? alternateHiveDirectory, | ||
| }) : _alternateHiveDirectory = alternateHiveDirectory; | ||
|
|
||
| static const optionName = '--hive-store-dir'; | ||
| static const defaultWindowsSingleInstanceId = 'icarus_single_instance'; | ||
|
|
||
| final List<String> fileOpenArgs; | ||
| final String? _alternateHiveDirectory; | ||
|
|
||
| static HiveStoreLaunch parse(List<String> rawArgs) { | ||
| final fileOpenArgs = <String>[]; | ||
| String? alternateHiveDirectory; | ||
| var sawOption = false; | ||
|
|
||
| for (var index = 0; index < rawArgs.length; index += 1) { | ||
| final argument = rawArgs[index]; | ||
| if (argument == optionName) { | ||
| if (sawOption) { | ||
| throw const HiveStoreLaunchException( | ||
| '$optionName may only be supplied once.', | ||
| ); | ||
| } | ||
| if (index + 1 >= rawArgs.length || | ||
| rawArgs[index + 1] == optionName || | ||
| rawArgs[index + 1].startsWith('$optionName=')) { | ||
| throw const HiveStoreLaunchException( | ||
| '$optionName requires an absolute directory path.', | ||
| ); | ||
| } | ||
| sawOption = true; | ||
| alternateHiveDirectory = rawArgs[++index]; | ||
| _validateOptionValue(alternateHiveDirectory); | ||
| continue; | ||
| } | ||
|
|
||
| if (argument.startsWith('$optionName=')) { | ||
| if (sawOption) { | ||
| throw const HiveStoreLaunchException( | ||
| '$optionName may only be supplied once.', | ||
| ); | ||
| } | ||
| sawOption = true; | ||
| alternateHiveDirectory = argument.substring(optionName.length + 1); | ||
| _validateOptionValue(alternateHiveDirectory); | ||
| continue; | ||
| } | ||
|
|
||
| fileOpenArgs.add(argument); | ||
| } | ||
|
|
||
| return HiveStoreLaunch._( | ||
| fileOpenArgs: List.unmodifiable(fileOpenArgs), | ||
| alternateHiveDirectory: alternateHiveDirectory, | ||
| ); | ||
| } | ||
|
|
||
| void validateForWeb() { | ||
| if (_alternateHiveDirectory == null) return; | ||
| throw const HiveStoreLaunchException( | ||
| '$optionName is only available in desktop builds.', | ||
| ); | ||
| } | ||
|
|
||
| Future<PreparedHiveStore?> prepareAlternateStore({ | ||
| required Future<Directory> Function() getDefaultHiveDirectory, | ||
| Future<void> Function(Directory directory)? probeDirectory, | ||
| }) async { | ||
| final requestedPath = _alternateHiveDirectory; | ||
| if (requestedPath == null) return null; | ||
| if (!path.isAbsolute(requestedPath)) { | ||
| throw HiveStoreLaunchException( | ||
| '$optionName requires an absolute path: $requestedPath', | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| final requestedDirectory = Directory(path.normalize(requestedPath)); | ||
| await requestedDirectory.create(recursive: true); | ||
| final canonicalPath = path.normalize( | ||
| await requestedDirectory.resolveSymbolicLinks(), | ||
| ); | ||
| final canonicalDirectory = Directory(canonicalPath); | ||
| await (probeDirectory ?? _probeDirectory)(canonicalDirectory); | ||
|
|
||
| final defaultDirectory = await getDefaultHiveDirectory(); | ||
| final defaultPath = await _resolvedDirectoryPath(defaultDirectory); | ||
| final isDefaultDirectory = | ||
| _pathIdentity(canonicalPath) == _pathIdentity(defaultPath); | ||
|
|
||
| return PreparedHiveStore( | ||
| hiveDirectoryPath: canonicalPath, | ||
| windowsSingleInstanceId: isDefaultDirectory | ||
| ? defaultWindowsSingleInstanceId | ||
| : await _windowsSingleInstanceId(canonicalPath), | ||
| ); | ||
| } on HiveStoreLaunchException { | ||
| rethrow; | ||
| } catch (error) { | ||
| throw HiveStoreLaunchException( | ||
| 'Could not prepare Hive store directory: $requestedPath', | ||
| error, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| static void _validateOptionValue(String value) { | ||
| if (value.isEmpty) { | ||
| throw const HiveStoreLaunchException( | ||
| '$optionName requires a non-empty directory path.', | ||
| ); | ||
| } | ||
| if (value.contains('\u0000')) { | ||
| throw const HiveStoreLaunchException( | ||
| '$optionName cannot contain a NUL character.', | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| static Future<void> _probeDirectory(Directory directory) async { | ||
| final probeDirectory = await directory.createTemp('.icarus-hive-probe-'); | ||
| try { | ||
| final probeFile = File(path.join(probeDirectory.path, 'write-probe')); | ||
| await probeFile.writeAsBytes(const [0], flush: true); | ||
| } finally { | ||
| if (await probeDirectory.exists()) { | ||
| await probeDirectory.delete(recursive: true); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| static Future<String> _resolvedDirectoryPath(Directory directory) async { | ||
| final absoluteDirectory = | ||
| Directory(path.normalize(directory.absolute.path)); | ||
| if (!await absoluteDirectory.exists()) return absoluteDirectory.path; | ||
| return path.normalize(await absoluteDirectory.resolveSymbolicLinks()); | ||
| } | ||
|
|
||
| static String _pathIdentity(String directoryPath) { | ||
| final normalized = path.normalize(directoryPath); | ||
| return Platform.isWindows ? normalized.toLowerCase() : normalized; | ||
| } | ||
|
|
||
| static Future<String> _windowsSingleInstanceId( | ||
| String canonicalPath, | ||
| ) async { | ||
| final digest = | ||
| await Sha256().hash(utf8.encode(_pathIdentity(canonicalPath))); | ||
| final hex = digest.bytes | ||
| .map((byte) => byte.toRadixString(16).padLeft(2, '0')) | ||
| .join(); | ||
| return '${defaultWindowsSingleInstanceId}_$hex'; | ||
| } | ||
| } | ||
|
|
||
| final class PreparedHiveStore { | ||
| const PreparedHiveStore({ | ||
| required this.hiveDirectoryPath, | ||
| required this.windowsSingleInstanceId, | ||
| }); | ||
|
|
||
| final String hiveDirectoryPath; | ||
| final String windowsSingleInstanceId; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export 'windows_process_termination_stub.dart' | ||
| if (dart.library.io) 'windows_process_termination_io.dart'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import 'dart:async'; | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:win32/win32.dart'; | ||
|
|
||
| Future<void> terminateDuplicateWindowsProcess() { | ||
| if (!Platform.isWindows) { | ||
| throw UnsupportedError('Windows process termination requires Windows.'); | ||
| } | ||
|
|
||
| // flutter_inappwebview_windows can crash in DLL_PROCESS_DETACH while | ||
| // releasing static WinRT state. This runs after argument forwarding and | ||
| // before Hive opens, so skipping DLL cleanup cannot interrupt library writes. | ||
| // See https://github.com/pichillilorenzo/flutter_inappwebview/issues/2733. | ||
| final result = TerminateProcess(GetCurrentProcess(), 0); | ||
| if (result == 0) { | ||
| throw WindowsException(HRESULT_FROM_WIN32(GetLastError())); | ||
|
Comment on lines
+15
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 600 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
cat -n lib/startup/windows_process_termination_io.dart
printf '%s\n' '--- dependency declarations and lock entry ---'
rg -n -A4 -B3 '^[[:space:]]*win32:|^ win32:' pubspec.yaml pubspec.lock
printf '%s\n' '--- bound win32 API usage ---'
rg -n -A8 -B5 'TerminateProcess|GetLastError|HRESULT_FROM_WIN32' .Repository: SunkenInTime/icarus Length of output: 3188 🏁 Script executed: #!/bin/bash
set -eu
cat -n lib/startup/windows_process_termination_io.dart
rg -n -A4 -B3 '^[[:space:]]*win32:|^ win32:' pubspec.yaml pubspec.lock
rg -n -A10 -B5 'TerminateProcess|GetLastError|HRESULT_FROM_WIN32' .Repository: SunkenInTime/icarus Length of output: 3091 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target ---'
sed -n '1,80p' lib/startup/windows_process_termination_io.dart
printf '%s\n' '--- locked win32 version ---'
rg -n -A8 -B3 'win32:' pubspec.yaml pubspec.lock
printf '%s\n' '--- win32 references ---'
rg -n -A12 -B6 'TerminateProcess|GetLastError|HRESULT_FROM_WIN32' --glob '*.dart' --glob 'pubspec*' .Repository: SunkenInTime/icarus Length of output: 3334 🌐 Web query:
💡 Result: In Citations:
🌐 Web query:
💡 Result: When using Dart FFI to interact with Windows APIs like TerminateProcess from kernel32.dll, relying on GetLastError in Dart is historically unreliable because the Dart runtime or other native operations can overwrite the thread-local error state before it can be queried [1]. In versions of the package:win32 library around v5.15.0 and earlier, this limitation was a known issue [1][2]. To mitigate this in older versions, developers were advised to call GetLastError immediately following the target API call, though this did not provide a total guarantee against interference [1][3]. Modern iterations of package:win32 (starting from v6.0.0 and further refined in v6.3.0) have addressed this issue by adopting a pattern where APIs that rely on SetLastError return a Win32Result object [1][4][5]. This object captures both the function's logical return value and the error code atomically at the native level, ensuring that the error state is preserved and accurately reported without risk of being clobbered by intervening operations [1][4]. If you are working with an older version like v5.15.0, you are encouraged to upgrade to the latest version to utilize these robust error-handling mechanisms [4][5]. Citations:
🌐 Web query:
💡 Result: The TerminateProcess function is a Win32 API used to unconditionally cause a process and all of its threads to exit [1]. Regarding your query: 1. GetLastError: If TerminateProcess fails, it returns a value of zero [1][2]. In this case, you should call GetLastError to retrieve extended error information [1][3]. A common error code associated with failures is ERROR_ACCESS_DENIED, which occurs if you attempt to terminate a process for which you lack the necessary PROCESS_TERMINATE access right [1]. 2. Win32Result: Win32Result is not a native Windows API type but is a specific construct used by certain third-party libraries, such as the Dart win32 package [4]. In that context, Win32Result is used to solve the unreliability of using GetLastError across foreign function interfaces (FFI) [4]. It works by wrapping native API calls to atomically capture both the logical return value and the error code immediately after the call, preventing the thread-local last-error state from being overwritten before it can be read [4]. 3. TerminateProcess Behavior: When calling TerminateProcess, if the target process is the calling process, the function does not return because the thread is immediately terminated [1][2]. Otherwise, the function is asynchronous, initiating termination and returning immediately [1][3]. You can use WaitForSingleObject if you need to verify that the process has actually finished terminating [1]. Citations:
Capture the native error from The lockfile uses 🤖 Prompt for AI AgentsSource: MCP tools |
||
| } | ||
|
|
||
| // Keep startup suspended until Windows finishes terminating the process. | ||
| return Completer<void>().future; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Future<void> terminateDuplicateWindowsProcess() async { | ||
| throw UnsupportedError('Windows process termination requires Windows.'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🤖 get_repo_knowledge executed:
get_repo_knowledge SunkenInTime/icarus /tmp/coderabbit-repo-knowledge/sunkenintime-icarus-bd498870Length of output: 661
🏁 Script executed:
Repository: SunkenInTime/icarus
Length of output: 13963
🏁 Script executed:
Repository: SunkenInTime/icarus
Length of output: 14438
🌐 Web query:
windows_single_instance 1.1.0 ensureSingleInstance exitFunction implementation💡 Result:
In version 1.1.0 of the windows_single_instance Flutter package, the ensureSingleInstance method was updated to support a custom exit function [1]. This functionality allows developers to define how the application should terminate or behave when a second instance is detected, rather than relying on the default exit behavior [2][1]. While the package documentation focuses on the primary usage—restricting an app to a single instance and handling the second window via a callback—the version 1.1.0 changelog explicitly notes the addition of support for passing a custom exit function to the ensureSingleInstance call [2][1]. If you are implementing this in your main function, the signature generally accommodates the arguments (such as the app identifier and the onSecondWindow callback). For precise implementation details, you should check the specific parameter name in your IDE's autocomplete or the source code for version 1.1.0, as custom exit functions are typically passed as an optional parameter to allow the application to perform cleanup or graceful termination before exiting [2][1]. If you are using the package, ensure your main function is set up as follows: void main(List args) async { WidgetsFlutterBinding.ensureInitialized; await WindowsSingleInstance.ensureSingleInstance( args, "your_unique_app_identifier", onSecondWindow: (args) { // Logic for when the second instance is opened }, // Custom exit function implementation here if supported by your specific call signature); runApp(const MyApp); } [2][3][4]
Citations:
🏁 Script executed:
Repository: SunkenInTime/icarus
Length of output: 5993
Move duplicate-instance detection before persisted-log initialization.
_initializePersistedDebugLog()queues a log write without awaiting it. A duplicate process can therefore callterminateDuplicateWindowsProcess, which usesTerminateProcess, while_appendPersistedLogEntryis still writing. This can lose the startup log entry.🤖 Prompt for AI Agents
Source: MCP tools