Skip to content

Commit f9e7a19

Browse files
committed
feat: implement focussing the main instance (#2)
UNTESTED
1 parent 29b007c commit f9e7a19

17 files changed

+669
-68
lines changed

README.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Flutter Single Instance
22

3-
Provides utilities for handiling single instancing in Flutter.
3+
A simple way to check if your application is already running.
44

55
| Platform | Support |
66
| -------- | ------- |
@@ -24,28 +24,48 @@ flutter pub add flutter_single_instance
2424

2525
### MacOS
2626

27-
Disable sandboxing in `macos/Runner/DebugProfile.entitlements` and `macos/Runner/Release.entitlements` files.
27+
Disable sandboxing and enable networking in `macos/Runner/DebugProfile.entitlements` and `macos/Runner/Release.entitlements` files.
2828

2929
```xml
3030
<key>com.apple.security.app-sandbox</key>
3131
<false/>
32+
33+
<key>com.apple.security.network.server</key>
34+
<true/>
35+
36+
<key>com.apple.security.network.client</key>
37+
<true/>
3238
```
3339

40+
- `com.apple.security.app-sandbox`: Set to false to allow access to the filesystem.
41+
- `com.apple.security.network.server`: Set to true to allow the app to act as a server and listen for incoming focus requests.
42+
- `com.apple.security.network.client`: Set to true to allow the app to act as a client and send focus requests to the server (i.e. the main instance).
43+
3444
## Usage
3545

3646
A simple usage example:
3747

3848
```dart
49+
import 'dart:io';
50+
51+
import 'package:flutter/material.dart';
3952
import 'package:flutter_single_instance/flutter_single_instance.dart';
4053
41-
main() async {
54+
void main() async {
4255
WidgetsFlutterBinding.ensureInitialized();
56+
await windowManager.ensureInitialized();
4357
44-
if(await FlutterSingleInstance().isFirstInstance()){
45-
runApp(MyApp());
46-
}else{
58+
if (await FlutterSingleInstance().isFirstInstance()) {
59+
runApp(const MyApp());
60+
} else {
4761
print("App is already running");
4862
63+
final err = await FlutterSingleInstance().focus();
64+
65+
if (err != null) {
66+
print("Error focusing running instance: $err");
67+
}
68+
4969
exit(0);
5070
}
5171
}
@@ -57,4 +77,4 @@ You can safely use this package in web and other unsupported platforms. It will
5777

5878
## Limitations
5979

60-
Currently this package does not provide a way to bring the existing instance to the front. If you have any ideas on how to achieve this, please open an issue or a pull request.
80+
- ~~Currently this package does not provide a way to bring the existing instance to the front. If you have any ideas on how to achieve this, please open an issue or a pull request.~~ This limitation has been resolved in version 1.2.0 (unstable).

analysis_options.yaml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
include: package:flutter_lints/flutter.yaml
2-
3-
# Additional information about this file can be found at
4-
# https://dart.dev/guides/language/analysis-options
1+
linter:
2+
rules:
3+
package_api_docs: true
4+
public_member_api_docs: true
5+
package_prefixed_library_names: true
6+
library_private_types_in_public_api: true
7+
package_names: true
8+
lines_longer_than_80_chars: false
9+
sort_pub_dependencies: true
10+
join_return_with_assignment: true
11+
prefer_for_elements_to_map_fromIterable: true
12+
null_check_on_nullable_type_parameter: true
13+
# yet to be released: document_ignores: true

lib/flutter_single_instance.dart

Lines changed: 114 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
1-
/// Provides utilities for handiling single instancing in Flutter.
1+
/// A simple way to check if your application is already running.
22
///
3-
/// A simple usage example:
3+
/// ---
44
///
55
/// ```dart
6+
/// import 'dart:io';
7+
///
8+
/// import 'package:flutter/material.dart';
69
/// import 'package:flutter_single_instance/flutter_single_instance.dart';
710
///
8-
/// main() async {
11+
/// void main() async {
912
/// WidgetsFlutterBinding.ensureInitialized();
13+
/// await windowManager.ensureInitialized();
1014
///
11-
/// if(await FlutterSingleInstance().isFirstInstance()){
12-
/// runApp(MyApp());
13-
/// }else{
15+
/// if (await FlutterSingleInstance().isFirstInstance()) {
16+
/// runApp(const MyApp());
17+
/// } else {
1418
/// print("App is already running");
1519
///
20+
/// final err = await FlutterSingleInstance().focus();
21+
///
22+
/// if (err != null) {
23+
/// print("Error focusing running instance: $err");
24+
/// }
25+
///
1626
/// exit(0);
1727
/// }
1828
/// }
1929
/// ```
2030
library flutter_single_instance;
2131

32+
export 'package:window_manager/window_manager.dart' show windowManager;
33+
34+
import 'dart:convert';
2235
import 'dart:io';
2336
import 'package:flutter/foundation.dart';
37+
import 'package:flutter_single_instance/src/focus.dart';
38+
import 'package:flutter_single_instance/src/generated/focus.pbgrpc.dart';
39+
import 'package:flutter_single_instance/src/instance.dart';
40+
import 'package:grpc/grpc.dart';
2441
import 'package:path_provider/path_provider.dart';
2542
import 'src/linux.dart';
2643
import 'src/macos.dart';
@@ -30,24 +47,27 @@ import 'src/unsupported.dart';
3047
/// Provides utilities for checking if this is the first instance of the app.
3148
/// Make sure to call `WidgetsFlutterBinding.ensureInitialized()` before using this class.
3249
abstract class FlutterSingleInstance {
33-
static FlutterSingleInstance? _instance;
50+
static FlutterSingleInstance? _singelton;
3451

52+
/// Internal constructor for implementations.
3553
@protected
36-
const FlutterSingleInstance.internal();
54+
FlutterSingleInstance.internal();
55+
56+
Server? _server;
57+
Instance? _instance;
3758

59+
/// Provides utilities for checking if this is the first instance of the app.
60+
/// Make sure to call `WidgetsFlutterBinding.ensureInitialized()` before using this class.
3861
factory FlutterSingleInstance() {
39-
_instance ??= kIsWeb || Platform.isAndroid || Platform.isIOS
40-
? const FlutterSingleInstanceUnsopported()
41-
: Platform.isMacOS
42-
? const FlutterSingleInstanceMacOS()
43-
: Platform.isLinux
44-
? const FlutterSingleInstanceLinux()
45-
: Platform.isWindows
46-
? const FlutterSingleInstanceWindows()
47-
: throw UnsupportedError(
48-
'Platform ${Platform.operatingSystem} is not supported.');
49-
50-
return _instance!;
62+
_singelton ??= Platform.isMacOS
63+
? FlutterSingleInstanceMacOS()
64+
: Platform.isLinux
65+
? FlutterSingleInstanceLinux()
66+
: Platform.isWindows
67+
? FlutterSingleInstanceWindows()
68+
: FlutterSingleInstanceUnsopported();
69+
70+
return _singelton!;
5171
}
5272

5373
/// If enabled [FlutterSingleInstance.isFirstInstance] will always return true.
@@ -71,36 +91,42 @@ abstract class FlutterSingleInstance {
7191
var pidFile = await getPidFile(processName);
7292
pidFile!;
7393

74-
if (pidFile.existsSync()) {
75-
var pid = int.parse(pidFile.readAsStringSync());
94+
if (!pidFile.existsSync()) {
95+
// No pid file, so this is the first instance.
96+
await activateInstance(processName);
97+
return true;
98+
}
7699

77-
var pidName = await getProcessName(pid);
100+
final data = await pidFile.readAsString();
78101

79-
if (processName != pidName) {
80-
// Process does not exist, so we can activate this instance.
81-
await _activateInstance(processName);
102+
_instance = Instance.fromJson(jsonDecode(data));
82103

83-
return true;
84-
} else {
85-
// Process exists, so this is not the first instance.
86-
return false;
87-
}
88-
} else {
89-
// No pid file, so this is the first instance.
90-
await _activateInstance(processName);
104+
final pidName = await getProcessName(_instance!.pid);
91105

92-
return true;
106+
if (processName == pidName) {
107+
// Process exists, so this is not the first instance.
108+
return false;
93109
}
110+
111+
// Process does not exist, so we can activate this instance.
112+
await activateInstance(processName);
113+
114+
return true;
94115
}
95116

96-
/// Activates the first instance of the app.
97-
/// Writes a pid file to the temp directory.
98-
Future<void> _activateInstance(String processName) async {
117+
/// Activates the first instance of the app and writes a pid file to the temp directory.
118+
@protected
119+
Future<void> activateInstance(String processName) async {
99120
var pidFile = await getPidFile(processName);
100121

101122
if (pidFile?.existsSync() == false) await pidFile?.create();
102123

103-
await pidFile?.writeAsString(pid.toString());
124+
final instance = Instance(
125+
pid: pid,
126+
port: await startRpcServer(),
127+
);
128+
129+
await pidFile?.writeAsString(jsonEncode(instance.toJson()));
104130
}
105131

106132
/// Returns the pid file.
@@ -110,4 +136,53 @@ abstract class FlutterSingleInstance {
110136

111137
return File('${tmp.path}/$processName.pid');
112138
}
139+
140+
/// Starts an RPC server that listens for focus requests.
141+
@protected
142+
Future<int> startRpcServer() async {
143+
_server = Server.create(
144+
services: [FocusService()],
145+
codecRegistry: CodecRegistry(
146+
codecs: const [
147+
GzipCodec(),
148+
IdentityCodec(),
149+
],
150+
),
151+
);
152+
153+
await _server!.serve(port: 0);
154+
155+
return _server!.port!;
156+
}
157+
158+
/// Focuses the running instance of the app and
159+
/// returns `null` if the operation was successful or an error message if it failed.
160+
Future<String?> focus() async {
161+
if (_instance == null) return "No instance to focus";
162+
if (_server != null) return "This is the first instance";
163+
164+
try {
165+
final channel = ClientChannel(
166+
'localhost',
167+
port: _instance!.port,
168+
options: ChannelOptions(
169+
credentials: ChannelCredentials.insecure(),
170+
codecRegistry: CodecRegistry(
171+
codecs: const [
172+
GzipCodec(),
173+
IdentityCodec(),
174+
],
175+
),
176+
),
177+
);
178+
179+
final client = FocusServiceClient(channel);
180+
181+
final response = await client.focus(FocusRequest());
182+
183+
return response.success ? null : response.error;
184+
} catch (e) {
185+
return e.toString();
186+
}
187+
}
113188
}

lib/src/focus.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'package:flutter_single_instance/src/generated/focus.pbgrpc.dart';
2+
import 'package:grpc/grpc.dart';
3+
import 'package:window_manager/window_manager.dart';
4+
5+
class FocusService extends FocusServiceBase {
6+
@override
7+
Future<FocusResponse> focus(ServiceCall call, FocusRequest request) async {
8+
try {
9+
await windowManager.focus();
10+
11+
return FocusResponse(success: true);
12+
} catch (e) {
13+
return FocusResponse(success: false, error: e.toString());
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)