fix(database, installer): enable WAL mode, grant ProgramData permissions in NSIS, and fallback to LocalAppData when read-only
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m21s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 8m5s

This commit is contained in:
2026-09-26 20:21:14 +02:00
parent e47fe609a6
commit b40966ac4c
4 changed files with 147 additions and 49 deletions
+7 -4
View File
@@ -16,24 +16,27 @@ RequestExecutionLevel admin
!insertmacro MUI_LANGUAGE "English"
Section "Install"
SetShellVarContext all
SetOutPath "$INSTDIR"
File "../dist/Win_Client.exe"
File "../compilation/nssm.exe"
File /r "../dist/runtime"
CreateDirectory "$COMMONAPPDATA\LOGAR"
CreateDirectory "$APPDATA\LOGAR"
ExecWait 'icacls "$APPDATA\LOGAR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
# Install and configure service via NSSM
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Client "$INSTDIR\Win_Client.exe"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppDirectory "$INSTDIR"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStdout "$COMMONAPPDATA\LOGAR\client.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStderr "$COMMONAPPDATA\LOGAR\client_err.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStdout "$APPDATA\LOGAR\client.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStderr "$APPDATA\LOGAR\client_err.log"'
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
WriteUninstaller "$INSTDIR\uninstall.exe"
SectionEnd
Section "Uninstall"
SetShellVarContext all
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client'
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm'
RMDir /r "$INSTDIR\runtime"
@@ -45,5 +48,5 @@ Section "Uninstall"
Delete "$INSTDIR\nssm.exe"
Delete "$INSTDIR\uninstall.exe"
RMDir /r "$INSTDIR"
RMDir /r "$COMMONAPPDATA\LOGAR"
RMDir /r "$APPDATA\LOGAR"
SectionEnd
+7 -4
View File
@@ -16,24 +16,27 @@ RequestExecutionLevel admin
!insertmacro MUI_LANGUAGE "English"
Section "Install"
SetShellVarContext all
SetOutPath "$INSTDIR"
File "../dist/Server.exe"
File "../compilation/nssm.exe"
File /r "../dist/runtime"
CreateDirectory "$COMMONAPPDATA\LOGAR-Server"
CreateDirectory "$APPDATA\LOGAR-Server"
ExecWait 'icacls "$APPDATA\LOGAR-Server" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
# Install and configure service via NSSM
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Server "$INSTDIR\Server.exe"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppDirectory "$INSTDIR"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStdout "$COMMONAPPDATA\LOGAR-Server\server.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStderr "$COMMONAPPDATA\LOGAR-Server\server_err.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStdout "$APPDATA\LOGAR-Server\server.log"'
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStderr "$APPDATA\LOGAR-Server\server_err.log"'
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
WriteUninstaller "$INSTDIR\uninstall.exe"
SectionEnd
Section "Uninstall"
SetShellVarContext all
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server'
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm'
RMDir /r "$INSTDIR\runtime"
@@ -46,5 +49,5 @@ Section "Uninstall"
Delete "$INSTDIR\nssm.exe"
Delete "$INSTDIR\uninstall.exe"
RMDir /r "$INSTDIR"
RMDir /r "$COMMONAPPDATA\LOGAR-Server"
RMDir /r "$APPDATA\LOGAR-Server"
SectionEnd
+76 -24
View File
@@ -384,32 +384,75 @@ def generate_server_keypair(server_name: str):
return private_key_armored, public_key_armored, fingerprint
def is_path_writable(filepath: str) -> bool:
"""Checks if a file or directory path is actually writable by the current process."""
try:
if os.path.exists(filepath):
if os.path.isdir(filepath):
test_f = os.path.join(filepath, f".perm_{os.getpid()}")
with open(test_f, "w") as f:
f.write("1")
os.remove(test_f)
return True
else:
with open(filepath, "a") as f:
pass
return True
else:
parent = os.path.dirname(os.path.abspath(filepath)) or "."
os.makedirs(parent, exist_ok=True)
test_f = os.path.join(parent, f".perm_{os.getpid()}")
with open(test_f, "w") as f:
f.write("1")
os.remove(test_f)
return True
except Exception:
return False
def get_default_data_dir(app_name: str = "LOGAR-Server") -> str:
"""Returns a suitable writable application data directory."""
if sys.platform == "win32":
# 1. ProgramData (C:\ProgramData\LOGAR-Server)
prog_data = os.environ.get("ProgramData", r"C:\ProgramData")
target = os.path.join(prog_data, app_name)
cwd = os.getcwd()
if os.path.exists(target) or "Program Files" in cwd or not os.access(cwd, os.W_OK):
try:
os.makedirs(target, exist_ok=True)
return target
except Exception:
pass
return os.getcwd()
target_prog = os.path.join(prog_data, app_name)
if is_path_writable(target_prog):
return target_prog
# 2. LocalAppData (C:\Users\<user>\AppData\Local\LOGAR-Server)
local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
target_local = os.path.join(local_app, app_name)
if is_path_writable(target_local):
return target_local
# 3. Temp directory (%TEMP%\LOGAR-Server)
temp_dir = os.path.join(os.environ.get("TEMP", r"C:\Windows\Temp"), app_name)
if is_path_writable(temp_dir):
return temp_dir
cwd = os.getcwd()
if is_path_writable(cwd):
return cwd
return os.path.expanduser("~")
def resolve_writable_path(path: str, app_name: str = "LOGAR-Server", default_filename: str = "logar_state.db") -> str:
"""Resolves a file or directory path, falling back to ProgramData if running in a read-only directory (e.g. Program Files)."""
"""Resolves a file or directory path, ensuring it is writable or falling back to a writable data directory."""
if not path:
path = default_filename
if os.path.isabs(path):
# If already absolute and writable, keep it
if os.path.isabs(path) and is_path_writable(path):
return path
cwd = os.getcwd()
if sys.platform == "win32" and ("Program Files" in cwd or not os.access(cwd, os.W_OK)):
data_dir = get_default_data_dir(app_name)
return os.path.join(data_dir, path)
return os.path.abspath(path)
# If relative and CWD is writable and not in Program Files, use CWD
if not os.path.isabs(path) and "Program Files" not in os.getcwd() and is_path_writable(os.getcwd()):
return os.path.abspath(path)
# Otherwise route into the guaranteed writable data directory
data_dir = get_default_data_dir(app_name)
fname = os.path.basename(path)
return os.path.join(data_dir, fname)
def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
@@ -450,8 +493,8 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
SESSION_SECRET = config["session_secret"]
return config
# Fallback to ProgramData if CWD is read-only / Program Files
if not os.path.isabs(config_path) and ("Program Files" in os.getcwd() or not os.access(os.getcwd(), os.W_OK)):
# Fallback to writable data dir if CWD is read-only / Program Files
if not os.path.isabs(config_path) and ("Program Files" in os.getcwd() or not is_path_writable(os.getcwd())):
config_path = os.path.join(get_default_data_dir("LOGAR-Server"), os.path.basename(config_path))
print(f"[!] Config '{config_path}' not found. Initializing first-run configuration...")
@@ -609,7 +652,12 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in
target_dir = os.path.dirname(os.path.abspath(target_path))
if target_dir:
os.makedirs(target_dir, exist_ok=True)
conn = sqlite3.connect(target_path)
conn = sqlite3.connect(target_path, timeout=10.0)
try:
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA busy_timeout=5000;")
except Exception:
pass
# 1. Active Issues
conn.execute("""
@@ -732,11 +780,15 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in
_execute_schema(db_path)
return db_path
except (sqlite3.OperationalError, PermissionError, OSError) as e:
if "readonly" in str(e).lower() or "permission" in str(e).lower() or isinstance(e, PermissionError):
fallback_path = os.path.join(get_default_data_dir("LOGAR-Server"), "logar_state.db")
print(f"[!] Warning: Database path '{db_path}' is not writable ({e}). Falling back to '{fallback_path}'...")
_execute_schema(fallback_path)
return fallback_path
if "readonly" in str(e).lower() or "permission" in str(e).lower() or "locked" in str(e).lower() or isinstance(e, PermissionError):
local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
fallback_dir = os.path.join(local_app, "LOGAR-Server")
os.makedirs(fallback_dir, exist_ok=True)
fallback_path = os.path.join(fallback_dir, "logar_state.db")
if fallback_path != db_path:
print(f"[!] Warning: Database path '{db_path}' is not writable ({e}). Falling back to '{fallback_path}'...")
_execute_schema(fallback_path)
return fallback_path
raise
+57 -17
View File
@@ -115,39 +115,79 @@ def get_tls_socket(hub_host: str, hub_port: int, cert_dir: str):
return ctx.wrap_socket(raw_sock, server_hostname=hub_host)
def is_path_writable(filepath: str) -> bool:
"""Checks if a file or directory path is actually writable by the current process."""
try:
if os.path.exists(filepath):
if os.path.isdir(filepath):
test_f = os.path.join(filepath, f".perm_{os.getpid()}")
with open(test_f, "w") as f:
f.write("1")
os.remove(test_f)
return True
else:
with open(filepath, "a") as f:
pass
return True
else:
parent = os.path.dirname(os.path.abspath(filepath)) or "."
os.makedirs(parent, exist_ok=True)
test_f = os.path.join(parent, f".perm_{os.getpid()}")
with open(test_f, "w") as f:
f.write("1")
os.remove(test_f)
return True
except Exception:
return False
def get_default_data_dir(app_name: str = "LOGAR") -> str:
"""Returns a suitable writable application data directory."""
if sys.platform == "win32":
# 1. ProgramData (C:\ProgramData\LOGAR)
prog_data = os.environ.get("ProgramData", r"C:\ProgramData")
target = os.path.join(prog_data, app_name)
cwd = os.getcwd()
if os.path.exists(target) or "Program Files" in cwd or not os.access(cwd, os.W_OK):
try:
os.makedirs(target, exist_ok=True)
return target
except Exception:
pass
return os.getcwd()
target_prog = os.path.join(prog_data, app_name)
if is_path_writable(target_prog):
return target_prog
# 2. LocalAppData (C:\Users\<user>\AppData\Local\LOGAR)
local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
target_local = os.path.join(local_app, app_name)
if is_path_writable(target_local):
return target_local
# 3. Temp directory (%TEMP%\LOGAR)
temp_dir = os.path.join(os.environ.get("TEMP", r"C:\Windows\Temp"), app_name)
if is_path_writable(temp_dir):
return temp_dir
cwd = os.getcwd()
if is_path_writable(cwd):
return cwd
return os.path.expanduser("~")
def resolve_writable_path(path: str, app_name: str = "LOGAR", default_filename: str = "client_state.json") -> str:
"""Resolves a file or directory path, falling back to ProgramData if running in a read-only directory (e.g. Program Files)."""
"""Resolves a file or directory path, ensuring it is writable or falling back to a writable data directory."""
if not path:
path = default_filename
if os.path.isabs(path):
if os.path.isabs(path) and is_path_writable(path):
return path
cwd = os.getcwd()
if sys.platform == "win32" and ("Program Files" in cwd or not os.access(cwd, os.W_OK)):
data_dir = get_default_data_dir(app_name)
return os.path.join(data_dir, path)
return os.path.abspath(path)
if not os.path.isabs(path) and "Program Files" not in os.getcwd() and is_path_writable(os.getcwd()):
return os.path.abspath(path)
data_dir = get_default_data_dir(app_name)
fname = os.path.basename(path)
return os.path.join(data_dir, fname)
def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str:
if custom_state_path:
return custom_state_path
config_dir = os.path.dirname(os.path.abspath(config_path))
if sys.platform == "win32" and ("Program Files" in config_dir or not os.access(config_dir, os.W_OK)):
if sys.platform == "win32" and ("Program Files" in config_dir or not is_path_writable(config_dir)):
return os.path.join(get_default_data_dir("LOGAR"), STATE_FILE_NAME)
return os.path.join(config_dir, STATE_FILE_NAME)