feat: process lock (#380)

This commit is contained in:
hstyi
2025-03-16 17:02:40 +08:00
committed by GitHub
parent e478535ae5
commit a00557bb9d
7 changed files with 348 additions and 144 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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()
}
})
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -22,7 +22,7 @@ class NativeStringComparator private constructor() : Comparator<String> {
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 {