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"
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
Section "Install"
|
Section "Install"
|
||||||
|
SetShellVarContext all
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
File "../dist/Win_Client.exe"
|
File "../dist/Win_Client.exe"
|
||||||
File "../compilation/nssm.exe"
|
File "../compilation/nssm.exe"
|
||||||
File /r "../dist/runtime"
|
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
|
# Install and configure service via NSSM
|
||||||
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Client "$INSTDIR\Win_Client.exe"'
|
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 AppDirectory "$INSTDIR"'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStdout "$COMMONAPPDATA\LOGAR\client.log"'
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStdout "$APPDATA\LOGAR\client.log"'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStderr "$COMMONAPPDATA\LOGAR\client_err.log"'
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppStderr "$APPDATA\LOGAR\client_err.log"'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
|
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Client'
|
||||||
|
|
||||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section "Uninstall"
|
Section "Uninstall"
|
||||||
|
SetShellVarContext all
|
||||||
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client'
|
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm'
|
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm'
|
||||||
RMDir /r "$INSTDIR\runtime"
|
RMDir /r "$INSTDIR\runtime"
|
||||||
@@ -45,5 +48,5 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\nssm.exe"
|
Delete "$INSTDIR\nssm.exe"
|
||||||
Delete "$INSTDIR\uninstall.exe"
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
RMDir /r "$INSTDIR"
|
RMDir /r "$INSTDIR"
|
||||||
RMDir /r "$COMMONAPPDATA\LOGAR"
|
RMDir /r "$APPDATA\LOGAR"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|||||||
@@ -16,24 +16,27 @@ RequestExecutionLevel admin
|
|||||||
!insertmacro MUI_LANGUAGE "English"
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
Section "Install"
|
Section "Install"
|
||||||
|
SetShellVarContext all
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
File "../dist/Server.exe"
|
File "../dist/Server.exe"
|
||||||
File "../compilation/nssm.exe"
|
File "../compilation/nssm.exe"
|
||||||
File /r "../dist/runtime"
|
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
|
# Install and configure service via NSSM
|
||||||
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Server "$INSTDIR\Server.exe"'
|
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 AppDirectory "$INSTDIR"'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStdout "$COMMONAPPDATA\LOGAR-Server\server.log"'
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppStdout "$APPDATA\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 AppStderr "$APPDATA\LOGAR-Server\server_err.log"'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
|
ExecWait '"$INSTDIR\nssm.exe" start LOGAR_Server'
|
||||||
|
|
||||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section "Uninstall"
|
Section "Uninstall"
|
||||||
|
SetShellVarContext all
|
||||||
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server'
|
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm'
|
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm'
|
||||||
RMDir /r "$INSTDIR\runtime"
|
RMDir /r "$INSTDIR\runtime"
|
||||||
@@ -46,5 +49,5 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\nssm.exe"
|
Delete "$INSTDIR\nssm.exe"
|
||||||
Delete "$INSTDIR\uninstall.exe"
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
RMDir /r "$INSTDIR"
|
RMDir /r "$INSTDIR"
|
||||||
RMDir /r "$COMMONAPPDATA\LOGAR-Server"
|
RMDir /r "$APPDATA\LOGAR-Server"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|||||||
+71
-19
@@ -384,33 +384,76 @@ def generate_server_keypair(server_name: str):
|
|||||||
return private_key_armored, public_key_armored, fingerprint
|
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:
|
def get_default_data_dir(app_name: str = "LOGAR-Server") -> str:
|
||||||
"""Returns a suitable writable application data directory."""
|
"""Returns a suitable writable application data directory."""
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
# 1. ProgramData (C:\ProgramData\LOGAR-Server)
|
||||||
prog_data = os.environ.get("ProgramData", r"C:\ProgramData")
|
prog_data = os.environ.get("ProgramData", r"C:\ProgramData")
|
||||||
target = os.path.join(prog_data, app_name)
|
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()
|
cwd = os.getcwd()
|
||||||
if os.path.exists(target) or "Program Files" in cwd or not os.access(cwd, os.W_OK):
|
if is_path_writable(cwd):
|
||||||
try:
|
return cwd
|
||||||
os.makedirs(target, exist_ok=True)
|
return os.path.expanduser("~")
|
||||||
return target
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return os.getcwd()
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_writable_path(path: str, app_name: str = "LOGAR-Server", default_filename: str = "logar_state.db") -> str:
|
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:
|
if not path:
|
||||||
path = default_filename
|
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
|
return path
|
||||||
cwd = os.getcwd()
|
|
||||||
if sys.platform == "win32" and ("Program Files" in cwd or not os.access(cwd, os.W_OK)):
|
# If relative and CWD is writable and not in Program Files, use CWD
|
||||||
data_dir = get_default_data_dir(app_name)
|
if not os.path.isabs(path) and "Program Files" not in os.getcwd() and is_path_writable(os.getcwd()):
|
||||||
return os.path.join(data_dir, path)
|
|
||||||
return os.path.abspath(path)
|
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]:
|
def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
|
||||||
"""Loads existing server_config.json or creates a new one on first run."""
|
"""Loads existing server_config.json or creates a new one on first run."""
|
||||||
@@ -450,8 +493,8 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
|
|||||||
SESSION_SECRET = config["session_secret"]
|
SESSION_SECRET = config["session_secret"]
|
||||||
return config
|
return config
|
||||||
|
|
||||||
# Fallback to ProgramData if CWD is read-only / Program Files
|
# 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 os.access(os.getcwd(), os.W_OK)):
|
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))
|
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...")
|
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))
|
target_dir = os.path.dirname(os.path.abspath(target_path))
|
||||||
if target_dir:
|
if target_dir:
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
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
|
# 1. Active Issues
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
@@ -732,8 +780,12 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in
|
|||||||
_execute_schema(db_path)
|
_execute_schema(db_path)
|
||||||
return db_path
|
return db_path
|
||||||
except (sqlite3.OperationalError, PermissionError, OSError) as e:
|
except (sqlite3.OperationalError, PermissionError, OSError) as e:
|
||||||
if "readonly" in str(e).lower() or "permission" in str(e).lower() or isinstance(e, PermissionError):
|
if "readonly" in str(e).lower() or "permission" in str(e).lower() or "locked" in str(e).lower() or isinstance(e, PermissionError):
|
||||||
fallback_path = os.path.join(get_default_data_dir("LOGAR-Server"), "logar_state.db")
|
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}'...")
|
print(f"[!] Warning: Database path '{db_path}' is not writable ({e}). Falling back to '{fallback_path}'...")
|
||||||
_execute_schema(fallback_path)
|
_execute_schema(fallback_path)
|
||||||
return fallback_path
|
return fallback_path
|
||||||
|
|||||||
+55
-15
@@ -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)
|
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:
|
def get_default_data_dir(app_name: str = "LOGAR") -> str:
|
||||||
"""Returns a suitable writable application data directory."""
|
"""Returns a suitable writable application data directory."""
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
# 1. ProgramData (C:\ProgramData\LOGAR)
|
||||||
prog_data = os.environ.get("ProgramData", r"C:\ProgramData")
|
prog_data = os.environ.get("ProgramData", r"C:\ProgramData")
|
||||||
target = os.path.join(prog_data, app_name)
|
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()
|
cwd = os.getcwd()
|
||||||
if os.path.exists(target) or "Program Files" in cwd or not os.access(cwd, os.W_OK):
|
if is_path_writable(cwd):
|
||||||
try:
|
return cwd
|
||||||
os.makedirs(target, exist_ok=True)
|
return os.path.expanduser("~")
|
||||||
return target
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return os.getcwd()
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_writable_path(path: str, app_name: str = "LOGAR", default_filename: str = "client_state.json") -> str:
|
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:
|
if not path:
|
||||||
path = default_filename
|
path = default_filename
|
||||||
if os.path.isabs(path):
|
|
||||||
|
if os.path.isabs(path) and is_path_writable(path):
|
||||||
return path
|
return path
|
||||||
cwd = os.getcwd()
|
|
||||||
if sys.platform == "win32" and ("Program Files" in cwd or not os.access(cwd, os.W_OK)):
|
if not os.path.isabs(path) and "Program Files" not in os.getcwd() and is_path_writable(os.getcwd()):
|
||||||
data_dir = get_default_data_dir(app_name)
|
|
||||||
return os.path.join(data_dir, path)
|
|
||||||
return os.path.abspath(path)
|
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:
|
def get_state_path(config_path: str, custom_state_path: Optional[str] = None) -> str:
|
||||||
if custom_state_path:
|
if custom_state_path:
|
||||||
return custom_state_path
|
return custom_state_path
|
||||||
config_dir = os.path.dirname(os.path.abspath(config_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(get_default_data_dir("LOGAR"), STATE_FILE_NAME)
|
||||||
return os.path.join(config_dir, STATE_FILE_NAME)
|
return os.path.join(config_dir, STATE_FILE_NAME)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user