From e16a73e4ae0d2a70799ac754a01304b2f4628571 Mon Sep 17 00:00:00 2001 From: wenyue Date: Mon, 3 Nov 2025 12:11:35 +0800 Subject: [PATCH 1/5] feat: enhance isFirstInstance method with retry logic and caching --- lib/flutter_single_instance.dart | 74 ++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/lib/flutter_single_instance.dart b/lib/flutter_single_instance.dart index 17b01d7..781f4de 100644 --- a/lib/flutter_single_instance.dart +++ b/lib/flutter_single_instance.dart @@ -74,7 +74,7 @@ abstract class FlutterSingleInstance { Server? _server; Instance? _instance; - bool? _isFirstInstance; + Future? _isFirstInstance; RandomAccessFile? _locker; /// Logger for this class. @@ -116,22 +116,63 @@ abstract class FlutterSingleInstance { /// Returns [null] if the process does not exist. Future getProcessName(int pid); - /// Returns true if this is the first instance of the app. - /// Automatically writes a pid file to the temp directory if this is the first instance. + /// Checks if this is the first running instance of the app. /// - /// **NOTE:** If [debugMode] is true, this will always return true. (Enabled by default in debug builds) - Future isFirstInstance() async { - _isFirstInstance ??= await () async { + /// Returns `true` if this is the first instance, `false` if another instance is already running. + /// If this is the first instance, a PID file will be created and an RPC server will be started. + /// If another instance is running, its information will be stored in [_instance] for use by + /// [focus]. + /// + /// [maxRetries] specifies the maximum number of retry attempts (defaults to `1`). + /// [retryInterval] specifies the interval between retries (defaults to `1000` milliseconds). + /// + /// **Note:** If [debugMode] is `true`, this method always returns `true`. The method result is + /// cached, and the cache is cleared if activation fails after max retries. + Future isFirstInstance({ + int maxRetries = 1, + Duration retryInterval = const Duration(milliseconds: 1000), + }) async { + if (_isFirstInstance != null) { + return _isFirstInstance!; + } + + _isFirstInstance = () async { if (debugMode) { logger.finest("Debug mode enabled, reporting as first instance"); return true; } var processName = FlutterSingleInstance.processName ?? - await getProcessName(pid); // get name of current process + await getProcessName(pid); // Get name of current process. processName!; - return activateInstance(processName); + // Retry logic for activating instance. + bool result = false; + int attempt = 0; + + while (attempt < maxRetries) { + // Wait before retrying (skip for first attempt). + if (attempt > 0) { + logger.finest( + "Retrying activateInstance (attempt $attempt/$maxRetries)"); + await Future.delayed(retryInterval); + } + + result = await activateInstance(processName); + + if (result) { + break; // Exit loop if activation succeeds. + } + + attempt++; + } + + if (!result) { + // Reset the isFirstInstance future to allow for retries. + _isFirstInstance = null; + } + + return result; }(); return _isFirstInstance!; @@ -140,8 +181,10 @@ abstract class FlutterSingleInstance { /// Activates the first instance of the app and writes a pid file to the temp directory. @protected Future activateInstance(String processName) async { - assert(_locker == null && _instance == null, - "activateInstance should only be called once"); + if (_locker != null) { + logger.finest("Already activated instance, returning true"); + return true; + } final pidFile = await getPidFile(processName); if (pidFile == null) { @@ -149,7 +192,7 @@ abstract class FlutterSingleInstance { return true; } - // Try to lock the file, if it fails, another instance is running + // Try to lock the file, if it fails, another instance is running. try { final locker = await File(pidFile.path + ".lock").open(mode: FileMode.write); @@ -157,7 +200,7 @@ abstract class FlutterSingleInstance { } catch (_) {} if (_locker == null) { - // Another instance is running, try to read the pid file + // Another instance is running, try to read the pid file. try { final data = await pidFile.readAsString(); final json = jsonDecode(data); @@ -172,15 +215,18 @@ abstract class FlutterSingleInstance { return false; } else { - // This is the first instance, create a new instance and write it to the pid file + // This is the first instance, create a new instance and write it to the pid file. final instance = Instance( pid: pid, port: await startRpcServer(), ); - // Write the instance to the pid file + // Write the instance to the pid file. await pidFile.writeAsString(jsonEncode(instance.toJson())); + // Reset the instance. + _instance = null; + logger.finest("Activated $instance at ${pidFile.path}"); return true; } From b24dbf0ab69ada0fc5500c369d8cc6c3506e8c99 Mon Sep 17 00:00:00 2001 From: wenyue Date: Mon, 3 Nov 2025 13:00:21 +0800 Subject: [PATCH 2/5] Update unsupported.dart --- lib/src/unsupported.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/unsupported.dart b/lib/src/unsupported.dart index 794a9dd..7a5e26c 100644 --- a/lib/src/unsupported.dart +++ b/lib/src/unsupported.dart @@ -11,14 +11,16 @@ class Unsupported extends FlutterSingleInstance { Future getProcessName(int pid) async => null; @override - Future isFirstInstance() async => true; + Future isFirstInstance( + {int maxRetries = 10, + Duration retryInterval = const Duration(milliseconds: 1000)}) async => + true; @override Future getPidFile(String processName) async => null; @override - Future focus([Object? metadata, bool bringToFront = true]) async => - null; + Future focus([Object? metadata, bool bringToFront = true]) async => null; @override Future activateInstance(String processName) async => true; From a6509f931428776a352a1f7dcbb45c46fdd2b161 Mon Sep 17 00:00:00 2001 From: McQuenji <60017181+mcquenji@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:05:17 +0100 Subject: [PATCH 3/5] Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/src/unsupported.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/unsupported.dart b/lib/src/unsupported.dart index 7a5e26c..306e24f 100644 --- a/lib/src/unsupported.dart +++ b/lib/src/unsupported.dart @@ -12,7 +12,7 @@ class Unsupported extends FlutterSingleInstance { @override Future isFirstInstance( - {int maxRetries = 10, + {int maxRetries = 1, Duration retryInterval = const Duration(milliseconds: 1000)}) async => true; From 2d367fcbb4f8628a0622eee4b1a61763a537ec4d Mon Sep 17 00:00:00 2001 From: wenyue Date: Mon, 3 Nov 2025 23:08:17 +0800 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: McQuenji <60017181+mcquenji@users.noreply.github.com> --- lib/flutter_single_instance.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/flutter_single_instance.dart b/lib/flutter_single_instance.dart index 781f4de..85a39bf 100644 --- a/lib/flutter_single_instance.dart +++ b/lib/flutter_single_instance.dart @@ -123,7 +123,8 @@ abstract class FlutterSingleInstance { /// If another instance is running, its information will be stored in [_instance] for use by /// [focus]. /// - /// [maxRetries] specifies the maximum number of retry attempts (defaults to `1`). + /// [maxRetries] specifies the maximum number of attempts until this instance ultimately reports as a subsequent one. + /// If set to `1` (default) this instance immediately reports as subsequent if another instance is already running. /// [retryInterval] specifies the interval between retries (defaults to `1000` milliseconds). /// /// **Note:** If [debugMode] is `true`, this method always returns `true`. The method result is @@ -136,6 +137,9 @@ abstract class FlutterSingleInstance { return _isFirstInstance!; } +assert(maxRetries >= 1, 'maxRetries must be greater than or equal to 1'); +assert(retryInterval != Duration.zero, 'retryInterval must be non-zero'); + _isFirstInstance = () async { if (debugMode) { logger.finest("Debug mode enabled, reporting as first instance"); From a4922f5ff7ec4b7c6a1c2e2e7ef451cdab055df2 Mon Sep 17 00:00:00 2001 From: wenyue Date: Mon, 3 Nov 2025 23:10:35 +0800 Subject: [PATCH 5/5] Fix assert statements formatting in flutter_single_instance.dart --- lib/flutter_single_instance.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/flutter_single_instance.dart b/lib/flutter_single_instance.dart index 85a39bf..23afab7 100644 --- a/lib/flutter_single_instance.dart +++ b/lib/flutter_single_instance.dart @@ -137,8 +137,8 @@ abstract class FlutterSingleInstance { return _isFirstInstance!; } -assert(maxRetries >= 1, 'maxRetries must be greater than or equal to 1'); -assert(retryInterval != Duration.zero, 'retryInterval must be non-zero'); + assert(maxRetries >= 1, 'maxRetries must be greater than or equal to 1'); + assert(retryInterval != Duration.zero, 'retryInterval must be non-zero'); _isFirstInstance = () async { if (debugMode) {