From a00557bb9da7578e009f7fbca8735076a1e1811a Mon Sep 17 00:00:00 2001 From: hstyi Date: Sun, 16 Mar 2025 17:02:40 +0800 Subject: [PATCH] feat: process lock (#380) --- src/main/java/app/termora/Kernel32.java | 38 ---- src/main/java/app/termora/MyKernel32.java | 38 ++++ .../app/termora/ApplicationInitializr.kt | 91 ++++++++ .../kotlin/app/termora/ApplicationRunner.kt | 54 +---- .../app/termora/ApplicationSingleton.kt | 215 ++++++++++++++++++ src/main/kotlin/app/termora/Main.kt | 54 +---- .../app/termora/NativeStringComparator.kt | 2 +- 7 files changed, 348 insertions(+), 144 deletions(-) delete mode 100644 src/main/java/app/termora/Kernel32.java create mode 100644 src/main/java/app/termora/MyKernel32.java create mode 100644 src/main/kotlin/app/termora/ApplicationInitializr.kt create mode 100644 src/main/kotlin/app/termora/ApplicationSingleton.kt diff --git a/src/main/java/app/termora/Kernel32.java b/src/main/java/app/termora/Kernel32.java deleted file mode 100644 index ab7e68e..0000000 --- a/src/main/java/app/termora/Kernel32.java +++ /dev/null @@ -1,38 +0,0 @@ -package app.termora; - -import com.sun.jna.Native; -import com.sun.jna.Pointer; -import com.sun.jna.WString; -import com.sun.jna.win32.StdCallLibrary; - -interface Kernel32 extends StdCallLibrary { - - Kernel32 INSTANCE = Native.load("Kernel32", Kernel32.class); - WString INVARIANT_LOCALE = new WString(""); - - int CompareStringEx(WString lpLocaleName, - int dwCmpFlags, - WString lpString1, - int cchCount1, - WString lpString2, - int cchCount2, - Pointer lpVersionInformation, - Pointer lpReserved, - int lParam); - - default int CompareStringEx(int dwCmpFlags, - String str1, - String str2) { - return Kernel32.INSTANCE - .CompareStringEx( - INVARIANT_LOCALE, - dwCmpFlags, - new WString(str1), - str1.length(), - new WString(str2), - str2.length(), - Pointer.NULL, - Pointer.NULL, - 0); - } - } \ No newline at end of file diff --git a/src/main/java/app/termora/MyKernel32.java b/src/main/java/app/termora/MyKernel32.java new file mode 100644 index 0000000..5677657 --- /dev/null +++ b/src/main/java/app/termora/MyKernel32.java @@ -0,0 +1,38 @@ +package app.termora; + +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.WString; +import com.sun.jna.win32.StdCallLibrary; + +interface MyKernel32 extends StdCallLibrary { + + MyKernel32 INSTANCE = Native.load("Kernel32", MyKernel32.class); + WString INVARIANT_LOCALE = new WString(""); + + int CompareStringEx(WString lpLocaleName, + int dwCmpFlags, + WString lpString1, + int cchCount1, + WString lpString2, + int cchCount2, + Pointer lpVersionInformation, + Pointer lpReserved, + int lParam); + + default int CompareStringEx(int dwCmpFlags, + String str1, + String str2) { + return MyKernel32.INSTANCE + .CompareStringEx( + INVARIANT_LOCALE, + dwCmpFlags, + new WString(str1), + str1.length(), + new WString(str2), + str2.length(), + Pointer.NULL, + Pointer.NULL, + 0); + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/ApplicationInitializr.kt b/src/main/kotlin/app/termora/ApplicationInitializr.kt new file mode 100644 index 0000000..ec66994 --- /dev/null +++ b/src/main/kotlin/app/termora/ApplicationInitializr.kt @@ -0,0 +1,91 @@ +package app.termora + +import com.formdev.flatlaf.util.SystemInfo +import com.pty4j.util.PtyUtil +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.SystemUtils +import org.tinylog.configuration.Configuration +import java.io.File +import kotlin.system.exitProcess + +class ApplicationInitializr { + + fun run() { + + // 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹 + if (SystemUtils.IS_OS_MAC_OSX) { + setupNativeLibraries() + } + + if (SystemUtils.IS_OS_MAC_OSX) { + System.setProperty("apple.awt.application.name", Application.getName()) + } + + // 设置 tinylog + setupTinylog() + + // 检查是否单例 + checkSingleton() + + // 启动 + ApplicationRunner().run() + + } + + + private fun setupNativeLibraries() { + if (!SystemUtils.IS_OS_MAC_OSX) { + return + } + + val appPath = Application.getAppPath() + if (StringUtils.isBlank(appPath)) { + return + } + + val contents = File(appPath).parentFile?.parentFile ?: return + val dylib = FileUtils.getFile(contents, "app", "dylib") + if (!dylib.exists()) { + return + } + + val jna = FileUtils.getFile(dylib, "jna") + if (jna.exists()) { + System.setProperty("jna.boot.library.path", jna.absolutePath) + } + + val pty4j = FileUtils.getFile(dylib, "pty4j") + if (pty4j.exists()) { + System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath) + } + + val jSerialComm = FileUtils.getFile(dylib, "jSerialComm") + if (jSerialComm.exists()) { + System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath) + } + + val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter") + if (restart4j.exists()) { + System.setProperty("restarter.path", restart4j.absolutePath) + } + } + + /** + * Windows 情况覆盖 + */ + private fun setupTinylog() { + if (SystemInfo.isWindows) { + val dir = File(Application.getBaseDataDir(), "logs") + FileUtils.forceMkdir(dir) + Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log") + Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log") + } + } + + private fun checkSingleton() { + if (ApplicationSingleton.getInstance().isSingleton()) return + System.err.println("Program is already running") + exitProcess(1) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index 1c472cf..47d5255 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -21,34 +21,16 @@ import org.apache.commons.lang3.LocaleUtils import org.apache.commons.lang3.SystemUtils import org.json.JSONObject import org.slf4j.LoggerFactory -import org.tinylog.configuration.Configuration -import java.io.File -import java.nio.channels.FileChannel -import java.nio.channels.FileLock -import java.nio.file.Paths -import java.nio.file.StandardOpenOption import java.util.* import javax.swing.* import kotlin.system.exitProcess import kotlin.system.measureTimeMillis class ApplicationRunner { - private lateinit var singletonChannel: FileChannel - private lateinit var singletonLock: FileLock - private val log by lazy { - if (!::singletonLock.isInitialized) { - throw UnsupportedOperationException("Singleton lock is not initialized") - } - LoggerFactory.getLogger(ApplicationRunner::class.java) - } + private val log by lazy { LoggerFactory.getLogger(ApplicationRunner::class.java) } fun run() { measureTimeMillis { - // 覆盖 tinylog 配置 - val setupTinylog = measureTimeMillis { setupTinylog() } - - // 是否单例 - val checkSingleton = measureTimeMillis { checkSingleton() } // 打印系统信息 val printSystemInfo = measureTimeMillis { printSystemInfo() } @@ -82,8 +64,6 @@ class ApplicationRunner { val startMainFrame = measureTimeMillis { startMainFrame() } if (log.isDebugEnabled) { - log.debug("setupTinylog: {}ms", setupTinylog) - log.debug("checkSingleton: {}ms", checkSingleton) log.debug("printSystemInfo: {}ms", printSystemInfo) log.debug("openDatabase: {}ms", openDatabase) log.debug("loadSettings: {}ms", loadSettings) @@ -168,7 +148,7 @@ class ApplicationRunner { // if (Application.isUnknownVersion()) - FlatInspector.install("ctrl X") + FlatInspector.install("ctrl X") UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true) UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false) @@ -234,36 +214,6 @@ class ApplicationRunner { } - /** - * Windows 情况覆盖 - */ - private fun setupTinylog() { - if (SystemInfo.isWindows) { - val dir = File(Application.getBaseDataDir(), "logs") - FileUtils.forceMkdir(dir) - Configuration.set("writer_file.latest", "${dir.absolutePath}/${Application.getName().lowercase()}.log") - Configuration.set("writer_file.file", "${dir.absolutePath}/{date:yyyy}-{date:MM}-{date:dd}.log") - } - } - - - private fun checkSingleton() { - singletonChannel = FileChannel.open( - Paths.get(Application.getBaseDataDir().absolutePath, "lock"), - StandardOpenOption.CREATE, - StandardOpenOption.WRITE, - ) - - val lock = singletonChannel.tryLock() - if (lock == null) { - System.err.println("Program is already running") - exitProcess(1) - } - - singletonLock = lock - } - - private fun openDatabase() { try { Database.getDatabase() diff --git a/src/main/kotlin/app/termora/ApplicationSingleton.kt b/src/main/kotlin/app/termora/ApplicationSingleton.kt new file mode 100644 index 0000000..7920467 --- /dev/null +++ b/src/main/kotlin/app/termora/ApplicationSingleton.kt @@ -0,0 +1,215 @@ +package app.termora + +import com.formdev.flatlaf.util.SystemInfo +import com.sun.jna.platform.win32.Kernel32 +import com.sun.jna.platform.win32.User32 +import com.sun.jna.platform.win32.WinDef.* +import com.sun.jna.platform.win32.WinError +import com.sun.jna.platform.win32.WinUser.* +import com.sun.jna.platform.win32.Wtsapi32 +import org.slf4j.LoggerFactory +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.JFrame +import javax.swing.SwingUtilities + +class ApplicationSingleton private constructor() : Disposable { + + @Volatile + private var isSingleton = null as Boolean? + + + companion object { + fun getInstance(): ApplicationSingleton { + return ApplicationScope.forApplicationScope() + .getOrCreate(ApplicationSingleton::class) { ApplicationSingleton() } + } + } + + fun isSingleton(): Boolean { + var singleton = this.isSingleton + if (singleton != null) return singleton + + try { + synchronized(this) { + singleton = this.isSingleton + if (singleton != null) return singleton as Boolean + + if (SystemInfo.isWindows) { + val handle = Kernel32.INSTANCE.CreateMutex(null, false, Application.getName()) + singleton = handle != null && Kernel32.INSTANCE.GetLastError() != WinError.ERROR_ALREADY_EXISTS + if (singleton == true) { + // 启动监听器,方便激活窗口 + Thread.ofVirtual().start(Win32HelperWindow.getInstance()) + } else { + // 尝试激活窗口 + Win32HelperWindow.tick() + } + } else { + singleton = FileLocker.getInstance().tryLock() + } + + this.isSingleton = singleton == true + } + + } catch (e: Exception) { + e.printStackTrace(System.err) + return false + } + + + return this.isSingleton == true + + } + + private class FileLocker private constructor() { + companion object { + fun getInstance(): FileLocker { + return ApplicationScope.forApplicationScope() + .getOrCreate(FileLocker::class) { FileLocker() } + } + } + + + private lateinit var singletonChannel: FileChannel + private lateinit var singletonLock: FileLock + + + fun tryLock(): Boolean { + singletonChannel = FileChannel.open( + Paths.get(Application.getBaseDataDir().absolutePath, "lock"), + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + ) + + val lock = singletonChannel.tryLock() ?: return false + + this.singletonLock = lock + + return true + } + } + + + private class Win32HelperWindow private constructor() : Runnable { + + companion object { + private val log = LoggerFactory.getLogger(Win32HelperWindow::class.java) + private val WindowClass = "${Application.getName()}HelperWindowClass" + private val WindowName = + "${Application.getName()} hidden helper window, used only to catch the windows events" + private const val TICK: Int = WM_USER + 1 + + fun getInstance(): Win32HelperWindow { + return ApplicationScope.forApplicationScope() + .getOrCreate(Win32HelperWindow::class) { Win32HelperWindow() } + } + + + fun tick() { + val hWnd = User32.INSTANCE.FindWindow(WindowClass, WindowName) ?: return + User32.INSTANCE.SendMessage(hWnd, TICK, WPARAM(), LPARAM()) + } + } + + private val isRunning = AtomicBoolean(false) + + override fun run() { + if (SystemInfo.isWindows) { + if (isRunning.compareAndSet(false, true)) { + Win32Window() + } + } + } + + + private class Win32Window : WindowProc { + /** + * Instantiates a new win32 window test. + */ + init { + // define new window class + val hInst = Kernel32.INSTANCE.GetModuleHandle(null) + + val wClass = WNDCLASSEX() + wClass.hInstance = hInst + wClass.lpfnWndProc = this + wClass.lpszClassName = WindowClass + + // register window class + User32.INSTANCE.RegisterClassEx(wClass) + + // create new window + val hWnd = User32.INSTANCE.CreateWindowEx( + User32.WS_EX_TOPMOST, + WindowClass, + WindowName, + 0, 0, 0, 0, 0, + null, // WM_DEVICECHANGE contradicts parent=WinUser.HWND_MESSAGE + null, hInst, null + ) + + + val msg = MSG() + while (User32.INSTANCE.GetMessage(msg, hWnd, 0, 0) > 0) { + User32.INSTANCE.TranslateMessage(msg) + User32.INSTANCE.DispatchMessage(msg) + } + + Wtsapi32.INSTANCE.WTSUnRegisterSessionNotification(hWnd) + User32.INSTANCE.UnregisterClass(WindowClass, hInst) + User32.INSTANCE.DestroyWindow(hWnd) + + } + + override fun callback(hwnd: HWND, uMsg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT { + when (uMsg) { + WM_CREATE -> { + if (log.isDebugEnabled) { + log.debug("win32 helper window created") + } + return LRESULT() + } + + TICK -> { + if (log.isDebugEnabled) { + log.debug("win32 helper window tick") + } + onTick() + return LRESULT() + } + + WM_DESTROY -> { + if (log.isDebugEnabled) { + log.debug("win32 helper window destroyed") + } + User32.INSTANCE.PostQuitMessage(0) + return LRESULT() + } + + else -> return User32.INSTANCE.DefWindowProc(hwnd, uMsg, wParam, lParam) + } + } + + private fun onTick() { + SwingUtilities.invokeLater(object : Runnable { + override fun run() { + val windows = TermoraFrameManager.getInstance().getWindows() + if (windows.isEmpty()) return + for (window in windows) { + if (window.extendedState and JFrame.ICONIFIED == JFrame.ICONIFIED) { + window.extendedState = window.extendedState and JFrame.ICONIFIED.inv() + } + } + windows.last().toFront() + } + }) + + } + + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/Main.kt b/src/main/kotlin/app/termora/Main.kt index c9a4902..ac7859b 100644 --- a/src/main/kotlin/app/termora/Main.kt +++ b/src/main/kotlin/app/termora/Main.kt @@ -1,58 +1,6 @@ package app.termora -import com.pty4j.util.PtyUtil -import org.apache.commons.io.FileUtils -import org.apache.commons.lang3.StringUtils -import org.apache.commons.lang3.SystemUtils -import java.io.File - fun main() { - // 由于 macOS 签名和公证问题,依赖二进制依赖会单独在一个文件夹 - if (SystemUtils.IS_OS_MAC_OSX) { - setupNativeLibraries() - } - - if (SystemUtils.IS_OS_MAC_OSX) { - System.setProperty("apple.awt.application.name", Application.getName()) - } - - ApplicationRunner().run() + ApplicationInitializr().run() } - -private fun setupNativeLibraries() { - if (!SystemUtils.IS_OS_MAC_OSX) { - return - } - - val appPath = Application.getAppPath() - if (StringUtils.isBlank(appPath)) { - return - } - - val contents = File(appPath).parentFile?.parentFile ?: return - val dylib = FileUtils.getFile(contents, "app", "dylib") - if (!dylib.exists()) { - return - } - - val jna = FileUtils.getFile(dylib, "jna") - if (jna.exists()) { - System.setProperty("jna.boot.library.path", jna.absolutePath) - } - - val pty4j = FileUtils.getFile(dylib, "pty4j") - if (pty4j.exists()) { - System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath) - } - - val jSerialComm = FileUtils.getFile(dylib, "jSerialComm") - if (jSerialComm.exists()) { - System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath) - } - - val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter") - if (restart4j.exists()) { - System.setProperty("restarter.path", restart4j.absolutePath) - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/NativeStringComparator.kt b/src/main/kotlin/app/termora/NativeStringComparator.kt index e2e201b..b70e028 100644 --- a/src/main/kotlin/app/termora/NativeStringComparator.kt +++ b/src/main/kotlin/app/termora/NativeStringComparator.kt @@ -22,7 +22,7 @@ class NativeStringComparator private constructor() : Comparator { override fun compare(o1: String, o2: String): Int { if (SystemInfo.isWindows) { // CompareStringEx returns 1, 2, 3 respectively instead of -1, 0, 1 - return Kernel32.INSTANCE.CompareStringEx(SORT_DIGITSASNUMBERS, o1, o2) - 2 + return MyKernel32.INSTANCE.CompareStringEx(SORT_DIGITSASNUMBERS, o1, o2) - 2 } else if (SystemInfo.isMacOS) { val pool = NSAutoreleasePool() try {