Implement IPC to show up existing instance when re-running
This commit is contained in:
236
lib/main.dart
236
lib/main.dart
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:journaler/db.dart';
|
||||
@@ -8,6 +9,8 @@ import 'package:window_manager/window_manager.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'dart:math';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:ps_list/ps_list.dart';
|
||||
|
||||
// TODO: Sound does not play when ran from a different workdir? Weird
|
||||
// TODO: Fix saving the same scratch over and over again
|
||||
@@ -16,9 +19,55 @@ import 'dart:math';
|
||||
const Duration _defaultPopupInterval = Duration(minutes: 20);
|
||||
const String _defaultNotificationSound = 'MeetTheSniper.mp3';
|
||||
|
||||
// Flag to indicate if there's a pending show request from a secondary instance
|
||||
bool _pendingShowRequest = false;
|
||||
|
||||
// Global navigator key to access the navigator state from anywhere
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
/// Paths for IPC file in system temp directory
|
||||
class AppFiles {
|
||||
static String get ipcFilePath =>
|
||||
path.join(Directory.systemTemp.path, 'journaler_ipc.txt');
|
||||
}
|
||||
|
||||
void main() async {
|
||||
await DB.init();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final ipcFile = File(AppFiles.ipcFilePath);
|
||||
|
||||
if (await alreadyRunning()) {
|
||||
await signalPrimaryInstanceAndExit(ipcFile);
|
||||
} else {
|
||||
await runPrimaryInstance(ipcFile);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> alreadyRunning() async {
|
||||
final processes = await PSList.getRunningProcesses();
|
||||
final executable = Platform.resolvedExecutable;
|
||||
final executableName = path.basename(executable);
|
||||
final journalers =
|
||||
processes
|
||||
.where((process) => process == executableName)
|
||||
.toList();
|
||||
debugPrint("Journalers: $journalers");
|
||||
return journalers.length > 1;
|
||||
}
|
||||
|
||||
/// Run the primary instance of the application
|
||||
Future<void> runPrimaryInstance(File ipcFile) async {
|
||||
debugPrint("Starting as primary instance");
|
||||
|
||||
// Create or clear the IPC file
|
||||
if (!await ipcFile.exists()) {
|
||||
await ipcFile.create();
|
||||
}
|
||||
// Start a watcher for the IPC file to detect signals from other instances
|
||||
startIpcWatcher(ipcFile);
|
||||
|
||||
// Initialize the app
|
||||
await DB.init();
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
WindowOptions windowOptions = const WindowOptions(
|
||||
@@ -34,6 +83,114 @@ void main() async {
|
||||
});
|
||||
|
||||
runApp(const JournalerApp());
|
||||
|
||||
// Set up termination handlers to clean up resources
|
||||
setupTerminationHandlers(ipcFile);
|
||||
}
|
||||
|
||||
/// Handle termination signals by cleaning up IPC file
|
||||
void setupTerminationHandlers(File ipcFile) {
|
||||
// Handle normal termination
|
||||
ProcessSignal.sigterm.watch().listen((_) async {
|
||||
await cleanupFiles(ipcFile);
|
||||
exit(0);
|
||||
});
|
||||
|
||||
// Handle Windows and Linux CTRL+C termination
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
ProcessSignal.sigint.watch().listen((_) async {
|
||||
await cleanupFiles(ipcFile);
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up IPC file
|
||||
Future<void> cleanupFiles(File ipcFile) async {
|
||||
try {
|
||||
if (await ipcFile.exists()) {
|
||||
await ipcFile.delete();
|
||||
}
|
||||
debugPrint("Removed IPC file on app termination");
|
||||
} catch (e) {
|
||||
debugPrint("Error cleaning up files: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// Signal the primary instance to show itself, then exit
|
||||
Future<void> signalPrimaryInstanceAndExit(File ipcFile) async {
|
||||
debugPrint(
|
||||
"Another instance is already running. Signaling it to show window.",
|
||||
);
|
||||
|
||||
try {
|
||||
// Create the IPC file if it doesn't exist
|
||||
if (!await ipcFile.exists()) {
|
||||
await ipcFile.create();
|
||||
}
|
||||
|
||||
// Write a 'show' command with a timestamp
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
await ipcFile.writeAsString('show:$timestamp');
|
||||
debugPrint("Signal sent to primary instance via IPC file");
|
||||
|
||||
// Give the primary instance a moment to process the signal
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
} catch (e) {
|
||||
debugPrint("Error signaling primary instance: $e");
|
||||
}
|
||||
|
||||
// Exit this instance
|
||||
debugPrint("Exiting secondary instance");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/// Start watching the IPC file for signals from other instances
|
||||
void startIpcWatcher(File ipcFile) {
|
||||
// Initial check of the file content
|
||||
checkIpcFileContent(ipcFile);
|
||||
|
||||
// Set up a file watcher
|
||||
final watcher = ipcFile.watch();
|
||||
watcher.listen((event) {
|
||||
if (event.type == FileSystemEvent.modify) {
|
||||
checkIpcFileContent(ipcFile);
|
||||
}
|
||||
});
|
||||
|
||||
// Also set up a periodic check as a fallback
|
||||
Timer.periodic(const Duration(milliseconds: 200), (_) {
|
||||
checkIpcFileContent(ipcFile);
|
||||
});
|
||||
|
||||
debugPrint("IPC file watcher started");
|
||||
}
|
||||
|
||||
/// Check the IPC file content for commands
|
||||
Future<void> checkIpcFileContent(File ipcFile) async {
|
||||
try {
|
||||
if (await ipcFile.exists()) {
|
||||
final content = await ipcFile.readAsString();
|
||||
|
||||
if (content.isNotEmpty && content.startsWith('show:')) {
|
||||
// Clear the file immediately to avoid processing the same command multiple times
|
||||
await ipcFile.writeAsString('');
|
||||
|
||||
// Process the show command
|
||||
if (MainPageState.instance != null) {
|
||||
debugPrint("Received show command - making window visible");
|
||||
MainPageState.instance!.showWindow();
|
||||
} else {
|
||||
debugPrint(
|
||||
"MainPageState not initialized yet - setting pending flag",
|
||||
);
|
||||
_pendingShowRequest = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error processing IPC file: $e");
|
||||
}
|
||||
}
|
||||
|
||||
class JournalerApp extends StatelessWidget {
|
||||
@@ -114,6 +271,7 @@ class JournalerApp extends StatelessWidget {
|
||||
themeMode: ThemeMode.system,
|
||||
home: const MainPage(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: navigatorKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -126,6 +284,9 @@ class MainPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class MainPageState extends State<MainPage> with WindowListener {
|
||||
// Static reference to the current instance
|
||||
static MainPageState? instance;
|
||||
|
||||
final SystemTray _systemTray = SystemTray();
|
||||
final Menu _menu = Menu();
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
@@ -157,16 +318,31 @@ class MainPageState extends State<MainPage> with WindowListener {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Store reference to this instance
|
||||
instance = this;
|
||||
windowManager.addListener(this);
|
||||
_initSystemTray();
|
||||
_loadData();
|
||||
_loadVolume();
|
||||
windowManager.setPreventClose(true);
|
||||
_setWindowConfig();
|
||||
|
||||
// Check if there's a pending show request from another instance
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_pendingShowRequest) {
|
||||
debugPrint("Processing pending show request during initialization");
|
||||
_pendingShowRequest = false;
|
||||
showWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clear reference to this instance
|
||||
if (instance == this) {
|
||||
instance = null;
|
||||
}
|
||||
windowManager.removeListener(this);
|
||||
_popupTimer?.cancel();
|
||||
_debounceTimer?.cancel();
|
||||
@@ -210,7 +386,7 @@ class MainPageState extends State<MainPage> with WindowListener {
|
||||
_systemTray.registerSystemTrayEventHandler((eventName) {
|
||||
debugPrint("System Tray Event: $eventName");
|
||||
if (eventName == kSystemTrayEventClick) {
|
||||
_showWindow();
|
||||
showWindow();
|
||||
} else if (eventName == kSystemTrayEventRightClick) {
|
||||
_systemTray.popUpContextMenu();
|
||||
}
|
||||
@@ -220,26 +396,56 @@ class MainPageState extends State<MainPage> with WindowListener {
|
||||
void _startPopupTimer() {
|
||||
_popupTimer?.cancel();
|
||||
_popupTimer = Timer.periodic(_currentPopupInterval, (timer) {
|
||||
_showWindow();
|
||||
showWindow();
|
||||
});
|
||||
debugPrint(
|
||||
"Popup timer started with interval: ${_currentPopupInterval.inMinutes} minutes",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showWindow() async {
|
||||
Future<void> showWindow() async {
|
||||
_loadData();
|
||||
bool wasVisible = await windowManager.isVisible();
|
||||
if (!wasVisible) {
|
||||
await windowManager.setSize(const Size(1600, 900));
|
||||
await windowManager.center();
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
_currentEntryFocusNode.requestFocus();
|
||||
await _playSound();
|
||||
} else {
|
||||
await windowManager.focus();
|
||||
_currentEntryFocusNode.requestFocus();
|
||||
try {
|
||||
bool wasVisible = await windowManager.isVisible();
|
||||
debugPrint("Current window visibility: $wasVisible");
|
||||
|
||||
if (!wasVisible) {
|
||||
// First make sure the window has the right size and position
|
||||
await windowManager.setSize(const Size(1600, 900));
|
||||
await windowManager.center();
|
||||
|
||||
// Now show and focus
|
||||
await windowManager.show();
|
||||
await Future.delayed(const Duration(milliseconds: 100)); // Short delay
|
||||
await windowManager.focus();
|
||||
|
||||
// Set input focus
|
||||
_currentEntryFocusNode.requestFocus();
|
||||
|
||||
// Play notification sound
|
||||
await _playSound();
|
||||
|
||||
debugPrint("Window made visible and focused");
|
||||
} else {
|
||||
// Already visible, just focus
|
||||
await windowManager.focus();
|
||||
_currentEntryFocusNode.requestFocus();
|
||||
debugPrint("Window already visible, just focused");
|
||||
}
|
||||
|
||||
// Verify the window is now visible
|
||||
bool isNowVisible = await windowManager.isVisible();
|
||||
debugPrint("Window visibility after show attempt: $isNowVisible");
|
||||
|
||||
if (!isNowVisible) {
|
||||
debugPrint(
|
||||
"WARNING: Window still not visible after show attempt, trying again",
|
||||
);
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error showing window: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
10
pubspec.lock
10
pubspec.lock
@@ -289,7 +289,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
@@ -376,6 +376,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
ps_list:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ps_list
|
||||
sha256: "19d32f6c643313cf4f5101bb144b8978b9ba3dc42c9a01b247e8ed90581bc0ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.5"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@@ -38,6 +38,8 @@ dependencies:
|
||||
window_manager: ^0.4.3
|
||||
audioplayers: ^6.4.0
|
||||
sqflite_common_ffi: ^2.3.5
|
||||
path: ^1.8.0
|
||||
ps_list: ^0.0.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user