1 Commits

Author SHA1 Message Date
b2bff9e5c5 Implement IPC to show up existing instance when re-running 2025-04-23 15:51:55 +02:00
3 changed files with 232 additions and 16 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:journaler/db.dart'; import 'package:journaler/db.dart';
@@ -8,6 +9,8 @@ import 'package:window_manager/window_manager.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'dart:math'; 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: Sound does not play when ran from a different workdir? Weird
// TODO: Fix saving the same scratch over and over again // TODO: Fix saving the same scratch over and over again
@@ -16,9 +19,55 @@ import 'dart:math';
const Duration _defaultPopupInterval = Duration(minutes: 20); const Duration _defaultPopupInterval = Duration(minutes: 20);
const String _defaultNotificationSound = 'MeetTheSniper.mp3'; 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 { void main() async {
await DB.init();
WidgetsFlutterBinding.ensureInitialized(); 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(); await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions( WindowOptions windowOptions = const WindowOptions(
@@ -34,6 +83,114 @@ void main() async {
}); });
runApp(const JournalerApp()); 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 { class JournalerApp extends StatelessWidget {
@@ -114,6 +271,7 @@ class JournalerApp extends StatelessWidget {
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
home: const MainPage(), home: const MainPage(),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
navigatorKey: navigatorKey,
); );
} }
} }
@@ -126,6 +284,9 @@ class MainPage extends StatefulWidget {
} }
class MainPageState extends State<MainPage> with WindowListener { class MainPageState extends State<MainPage> with WindowListener {
// Static reference to the current instance
static MainPageState? instance;
final SystemTray _systemTray = SystemTray(); final SystemTray _systemTray = SystemTray();
final Menu _menu = Menu(); final Menu _menu = Menu();
final AudioPlayer _audioPlayer = AudioPlayer(); final AudioPlayer _audioPlayer = AudioPlayer();
@@ -157,16 +318,31 @@ class MainPageState extends State<MainPage> with WindowListener {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Store reference to this instance
instance = this;
windowManager.addListener(this); windowManager.addListener(this);
_initSystemTray(); _initSystemTray();
_loadData(); _loadData();
_loadVolume(); _loadVolume();
windowManager.setPreventClose(true); windowManager.setPreventClose(true);
_setWindowConfig(); _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 @override
void dispose() { void dispose() {
// Clear reference to this instance
if (instance == this) {
instance = null;
}
windowManager.removeListener(this); windowManager.removeListener(this);
_popupTimer?.cancel(); _popupTimer?.cancel();
_debounceTimer?.cancel(); _debounceTimer?.cancel();
@@ -210,7 +386,7 @@ class MainPageState extends State<MainPage> with WindowListener {
_systemTray.registerSystemTrayEventHandler((eventName) { _systemTray.registerSystemTrayEventHandler((eventName) {
debugPrint("System Tray Event: $eventName"); debugPrint("System Tray Event: $eventName");
if (eventName == kSystemTrayEventClick) { if (eventName == kSystemTrayEventClick) {
_showWindow(); showWindow();
} else if (eventName == kSystemTrayEventRightClick) { } else if (eventName == kSystemTrayEventRightClick) {
_systemTray.popUpContextMenu(); _systemTray.popUpContextMenu();
} }
@@ -220,26 +396,56 @@ class MainPageState extends State<MainPage> with WindowListener {
void _startPopupTimer() { void _startPopupTimer() {
_popupTimer?.cancel(); _popupTimer?.cancel();
_popupTimer = Timer.periodic(_currentPopupInterval, (timer) { _popupTimer = Timer.periodic(_currentPopupInterval, (timer) {
_showWindow(); showWindow();
}); });
debugPrint( debugPrint(
"Popup timer started with interval: ${_currentPopupInterval.inMinutes} minutes", "Popup timer started with interval: ${_currentPopupInterval.inMinutes} minutes",
); );
} }
Future<void> _showWindow() async { Future<void> showWindow() async {
_loadData(); _loadData();
try {
bool wasVisible = await windowManager.isVisible(); bool wasVisible = await windowManager.isVisible();
debugPrint("Current window visibility: $wasVisible");
if (!wasVisible) { if (!wasVisible) {
// First make sure the window has the right size and position
await windowManager.setSize(const Size(1600, 900)); await windowManager.setSize(const Size(1600, 900));
await windowManager.center(); 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.show();
await windowManager.focus(); await windowManager.focus();
_currentEntryFocusNode.requestFocus(); }
await _playSound(); } catch (e) {
} else { debugPrint("Error showing window: $e");
await windowManager.focus();
_currentEntryFocusNode.requestFocus();
} }
} }

View File

@@ -289,7 +289,7 @@ packages:
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@@ -376,6 +376,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.2" 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: screen_retriever:
dependency: transitive dependency: transitive
description: description:

View File

@@ -38,6 +38,8 @@ dependencies:
window_manager: ^0.4.3 window_manager: ^0.4.3
audioplayers: ^6.4.0 audioplayers: ^6.4.0
sqflite_common_ffi: ^2.3.5 sqflite_common_ffi: ^2.3.5
path: ^1.8.0
ps_list: ^0.0.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: