Check for existing instance on startup using sockets.

This commit is contained in:
Dain Nilsson 2022-01-20 13:47:54 +01:00
parent cedc86b03e
commit 589b9fecc4
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
6 changed files with 121 additions and 6 deletions

View File

@ -0,0 +1,79 @@
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:window_manager/window_manager.dart';
const _lockFileName = 'lockfile';
const _pingMessage = 'YA-PING';
const _pongMessage = 'YA-PONG';
final log = Logger('single_instance');
void _startServer(File lockfile) async {
ServerSocket socket;
if (Platform.isWindows) {
socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
lockfile.writeAsString('${socket.port}');
} else {
socket = await ServerSocket.bind(
InternetAddress(lockfile.path, type: InternetAddressType.unix), 0);
}
log.info('Lock file and socket created.');
socket.listen((client) {
client.listen((data) async {
final message = String.fromCharCodes(data);
if (message == _pingMessage) {
log.info('Got incomming connection');
if (!await WindowManager.instance.isMinimized()) {
await WindowManager.instance.setAlwaysOnTop(true);
await WindowManager.instance.setAlwaysOnTop(false);
} else {
await WindowManager.instance.restore();
}
// This doesn't seem to always work
await WindowManager.instance.focus();
client.write(_pongMessage);
}
client.close();
}, cancelOnError: true);
});
}
Future<void> ensureSingleInstance() async {
final appSupport = await getApplicationSupportDirectory();
final lockfile = File(path.join(appSupport.path, _lockFileName));
log.info('Lock file: $lockfile');
if (await lockfile.exists()) {
try {
Socket client;
if (Platform.isWindows) {
final port = int.parse(await lockfile.readAsString());
client = await Socket.connect(InternetAddress.loopbackIPv4, port);
} else {
client = await Socket.connect(
InternetAddress(lockfile.path, type: InternetAddressType.unix), 0);
}
client.write(_pingMessage);
await client.flush();
client.listen((data) async {
final message = String.fromCharCodes(data);
await client.close();
if (message == _pongMessage) {
log.info('Other application instance already running, exit.');
exit(0);
}
}, cancelOnError: true);
} on Exception {
await lockfile.delete();
_startServer(lockfile);
}
} else {
_startServer(lockfile);
}
}

View File

@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:logging/logging.dart';
import 'package:window_manager/window_manager.dart';
import 'package:yubico_authenticator/desktop/single_instance.dart';
import 'app/app.dart';
import 'app/views/main_page.dart';
@ -17,7 +18,11 @@ import 'error_page.dart';
final log = Logger('main');
void main() async {
_initLogging(Level.INFO);
WidgetsFlutterBinding.ensureInitialized();
log.info('Ensuring single instance...');
await ensureSingleInstance();
await windowManager.ensureInitialized();
// Either use the _YKMAN_EXE environment variable, or look relative to executable.
@ -43,8 +48,8 @@ void main() async {
try {
var rpc = await RpcSession.launch(exe!);
// Enable logging TODO: Make this configurable
_initLogging(Level.INFO, rpc);
log.info('ykman process started', exe);
rpc.setLogLevel(Logger.root.level);
overrides.add(rpcProvider.overrideWithValue(rpc));
page = const MainPage();
} catch (e) {
@ -52,8 +57,8 @@ void main() async {
page = ErrorPage(error: e.toString());
}
// Only MacOS supports hiding the window at start currently.
// For now, this size should match windows/runner/main.cpp and linux/flutter/my_application.cc
// Linux doesn't currently support hiding the window at start currently.
// For now, this size should match linux/flutter/my_application.cc to avoid window flicker at startup.
windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setSize(const Size(400, 720));
windowManager.show();
@ -65,7 +70,7 @@ void main() async {
));
}
void _initLogging(Level level, RpcSession rpc) {
void _initLogging(Level level) {
//TODO: Add support for logging to stderr and file
Logger.root.onRecord.listen((record) {
developer.log(
@ -78,7 +83,6 @@ void _initLogging(Level level, RpcSession rpc) {
});
Logger.root.level = level;
rpc.setLogLevel(level);
}
//TODO: Remove below this

View File

@ -5,10 +5,12 @@
import FlutterMacOS
import Foundation
import path_provider_macos
import shared_preferences_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View File

@ -352,6 +352,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
path_provider_linux:
dependency: transitive
description:
@ -359,6 +380,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
path_provider_platform_interface:
dependency: transitive
description:

View File

@ -37,6 +37,7 @@ dependencies:
async: ^2.8.2
logging: ^1.0.2
path_provider: ^2.0.8
shared_preferences: ^2.0.12
flutter_riverpod: ^1.0.0
json_annotation: ^4.4.0

View File

@ -117,7 +117,8 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
//original line: window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);