fix(database, installer): enable WAL mode, grant ProgramData permissions in NSIS, and fallback to LocalAppData when read-only
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user