prep for linux compatibility

This commit is contained in:
edschuy95 2026-01-20 15:57:22 -05:00
parent 66d5032085
commit 6cceb32037
3 changed files with 137 additions and 31 deletions

View file

@ -6,14 +6,15 @@ import sys
import random
import re
import queue
#import readchar
import ctypes
import tempfile
import psutil
import subprocess
import getpass
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
import pygame
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
#kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
# Event to signal shutdown
shutdown_event = threading.Event()
@ -21,19 +22,29 @@ shutdown_event = threading.Event()
ERROR_ALREADY_EXISTS = 183
# ========================= CONFIG =========================
DEBUG = False # Set True to enable debug overrides
DEBUG = True # Set True to enable debug overrides
# Default paths (used if not in debug mode)
DEFAULT_GAME_EXE = r".\qgame.dll"
DEFAULT_PLAYLIST = r".\arena\music\playlist.txt"
DEFAULT_RA3_MAPS = r".\arena\music\ra3_maps.txt"
DEFAULT_ARENA0_BL = r".\arena\music\arena0_bl.txt"
DEFAULT_WIN_GAME_EXE = r"./qgame.dll"
DEFAULT_LIN_GAME_EXE = r"./qgame.so"
DEFAULT_PLAYLIST = r"./arena/music/playlist.txt"
DEFAULT_RA3_MAPS = r"./arena/music/ra3_maps.txt"
DEFAULT_ARENA0_BL = r"./arena/music/arena0_bl.txt"
# Debug override paths
DEBUG_GAME_EXE = r"D:\GOG Games\Quake III\qgame.dll"
DEBUG_PLAYLIST = r"D:\GOG Games\Quake III\arena\music\playlist.txt"
DEBUG_RA3_MAPS = r"D:\GOG Games\Quake III\arena\music\ra3_maps.txt"
DEBUG_ARENA0_BL = r"D:\GOG Games\Quake III\arena\music\arena0_bl.txt"
DEBUG_WIN_GAME_EXE = r"D:/GOG Games/Quake III/qgame.dll"
DEBUG_LIN_GAME_EXE = r"D:/GOG Games/Quake III/qgame.so"
DEBUG_PLAYLIST = r"D:/GOG Games/Quake III/arena/music/playlist.txt"
DEBUG_RA3_MAPS = r"D:/GOG Games/Quake III/arena/music/ra3_maps.txt"
DEBUG_ARENA0_BL = r"D:/GOG Games/Quake III/arena/music/arena0_bl.txt"
if (os.name == "nt"):
DEFAULT_GAME_EXE = DEFAULT_WIN_GAME_EXE
DEBUG_GAME_EXE = DEBUG_WIN_GAME_EXE
else:
DEFAULT_GAME_EXE = DEFAULT_LIN_GAME_EXE
DEBUG_GAME_EXE = DEBUG_LIN_GAME_EXE
# Initial volume
VOLUME_STEP = 0.1 # step for W/S volume control
@ -78,6 +89,54 @@ ANSI_ESCAPE_RE = re.compile(
# ==========================================================
# ===================== UTILITY FUNCTIONS =================
def acquire_single_instance(lockfile_base: str):
"""
Ensures only one instance of the program is running per user.
Uses a lock file in the temp directory and psutil to check for stale locks.
Returns (file descriptor, lockfile path) if acquired, None if another instance is running.
"""
username = getpass.getuser()
lockfile_name = f"{lockfile_base}_{username}.lock"
lockfile_path = os.path.join(tempfile.gettempdir(), lockfile_name)
# If lock file exists, check if PID inside is running
if os.path.exists(lockfile_path):
try:
with open(lockfile_path, "r") as f:
pid = int(f.read())
if psutil.pid_exists(pid):
# Another instance is running
return None, lockfile_path
else:
# Stale lock file; remove it
os.unlink(lockfile_path)
except Exception:
# Could not read PID; remove stale lock
try:
os.unlink(lockfile_path)
except:
pass
# Create lock file exclusively
try:
fd = os.open(lockfile_path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
os.write(fd, str(os.getpid()).encode())
return fd, lockfile_path
except FileExistsError:
# Race condition: another process created it just now
return None, lockfile_path
def release_single_instance(fd, lockfile_path):
"""Closes the lock file and removes it."""
try:
os.close(fd)
except:
pass
try:
os.unlink(lockfile_path)
except:
pass
def Check_Arena_Blacklist(arenaline: str) -> bool:
global arena0_bl_path
arena_bl_local = load_arena0_blacklist(arena0_bl_path)
@ -86,21 +145,21 @@ def Check_Arena_Blacklist(arenaline: str) -> bool:
return True
return False
def acquire_single_instance(name: str):
handle = kernel32.CreateMutexW(
None, # lpMutexAttributes
False, # bInitialOwner
name # lpName
)
# def acquire_single_instance(name: str):
# handle = kernel32.CreateMutexW(
# None, # lpMutexAttributes
# False, # bInitialOwner
# name # lpName
# )
if not handle:
raise ctypes.WinError(ctypes.get_last_error())
# if not handle:
# raise ctypes.WinError(ctypes.get_last_error())
if kernel32.GetLastError() == ERROR_ALREADY_EXISTS:
kernel32.CloseHandle(handle)
return None
# if kernel32.GetLastError() == ERROR_ALREADY_EXISTS:
# kernel32.CloseHandle(handle)
# return None
return handle
# return handle
def strip_ansi(text: str) -> str:
return ANSI_ESCAPE_RE.sub("", text)
@ -465,11 +524,11 @@ def send_command(proc, cmd):
# ======================== MAIN ===========================
def main():
mutex = acquire_single_instance("Global\\q3aLauncherAndMp3Playback")
# mutex = acquire_single_instance("Global\\q3aLauncherAndMp3Playback")
if mutex is None:
print("Another instance is already running.")
sys.exit(1)
# if mutex is None:
# print("Another instance is already running.")
# sys.exit(1)
global playlist, ra3_maps, pty_proc, playlist_path, ra3_maps_path, arena0_blacklist, arena0_bl_path
@ -503,7 +562,7 @@ def main():
# # Launch quake process via subprocess
pty_proc = subprocess.Popen(
[game_exe, "--showterminalconsole"] + sys.argv[1:],
[game_exe, "+set", "ttycon", "1"] + sys.argv[1:],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
@ -542,5 +601,13 @@ def main():
pygame.mixer.quit()
if __name__ == "__main__":
main()
lock_fd, lock_path = acquire_single_instance("q3a_launcher.lock")
if lock_fd is None:
print("Another instance is already running.")
sys.exit(1)
try:
main()
finally:
release_single_instance(lock_fd, lock_path)
# ==========================================================