mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: process lock (#380)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
38
src/main/java/app/termora/MyKernel32.java
Normal file
38
src/main/java/app/termora/MyKernel32.java
Normal 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);
|
||||
}
|
||||
}
|
||||
91
src/main/kotlin/app/termora/ApplicationInitializr.kt
Normal file
91
src/main/kotlin/app/termora/ApplicationInitializr.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
215
src/main/kotlin/app/termora/ApplicationSingleton.kt
Normal file
215
src/main/kotlin/app/termora/ApplicationSingleton.kt
Normal 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()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user