Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add auto-run functionality to the Windows app #74

Merged
merged 28 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7fb2729
feat: Add auto-run functionality to the Windows app
bl4ckswordsman Nov 10, 2024
74bf839
Add auto-run functionality to application settings
bl4ckswordsman Nov 10, 2024
f870be3
Add auto-run functionality to application settings
bl4ckswordsman Nov 10, 2024
6c010c4
Add platform check for Windows before setting auto-run
bl4ckswordsman Nov 10, 2024
9fe9601
Add error handling in `remove_auto_run` to catch `FileNotFoundError`
bl4ckswordsman Nov 10, 2024
06469d9
Add platform check for Windows in auto-run settings
bl4ckswordsman Nov 10, 2024
05dd72c
Add platform checks for Windows in auto-run functions and adjust exec…
bl4ckswordsman Nov 10, 2024
017d498
Enhance auto-run functionality with registry verification and error h…
bl4ckswordsman Nov 10, 2024
78dd517
Sync auto-run settings on application initialization for Windows plat…
bl4ckswordsman Nov 10, 2024
50b5f9a
Refactor autorun verification and synchronization logic in app settings
bl4ckswordsman Nov 10, 2024
6538d18
Improve autorun setup with enhanced error handling and registry verif…
bl4ckswordsman Nov 10, 2024
013f679
refactor: Refactor autorun handling to improve registry updates and s…
bl4ckswordsman Nov 10, 2024
20a9f51
feat: Add Windows-only decorator to restrict autorun functions to Win…
bl4ckswordsman Nov 10, 2024
a15d363
chore: temporary removal of file for debugging
bl4ckswordsman Nov 10, 2024
1d5b20e
chore: enable debugging
bl4ckswordsman Nov 10, 2024
d95868f
feat: Update Windows build configuration to use windows_main.pyw and …
bl4ckswordsman Nov 10, 2024
ade08cf
feat: Enhance Windows build workflow with debug mode input and add wi…
bl4ckswordsman Nov 10, 2024
1ca1ef7
fix: Update Windows build workflow to use disco_beacon_win_test.spec …
bl4ckswordsman Nov 10, 2024
e1b61d7
feat: Refactor settings management to dynamically determine settings …
bl4ckswordsman Nov 11, 2024
c933711
docs: Update README to clarify settings storage locations and add aut…
bl4ckswordsman Nov 11, 2024
243478f
feat: Add logging for autorun registry operations and enhance autorun…
bl4ckswordsman Nov 11, 2024
f4f7577
fix: Update Windows build workflow to dynamically set DEBUG_MODE base…
bl4ckswordsman Nov 11, 2024
9302f4b
fix: Update test_build_windows.yaml
bl4ckswordsman Nov 11, 2024
cf97f20
Update test_build_windows.yaml
bl4ckswordsman Nov 11, 2024
eaf0c67
docs: Update README with settings storage locations for Windows and L…
bl4ckswordsman Nov 11, 2024
12277af
fix: Correct debug_mode condition syntax in Windows build workflow
bl4ckswordsman Nov 11, 2024
cc96fd9
fix: Improve error logging and exception handling in app_settings.py
bl4ckswordsman Nov 11, 2024
655e7d7
fix: Remove deprecated settings.json file from monitor-app
bl4ckswordsman Nov 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/test_build_windows.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
name: Test Windows Build

on:
workflow_dispatch: # Manual trigger
workflow_dispatch:
inputs:
debug_mode:
description: 'Enable Debug Mode'
required: false
type: boolean
default: true
pull_request: # Optionally trigger on PRs

jobs:
Expand Down Expand Up @@ -32,10 +38,13 @@ jobs:
(Get-Content src/version.py) -replace 'VERSION = ".*"', "VERSION = `"$testVersion`"" | Set-Content src/version.py
- name: Build executable
run: |
pyinstaller --clean disco_beacon.spec
# Replace the DEBUG_MODE line in the spec file
$debug_value = if ('${{ inputs.debug_mode }}' -eq 'false') { "False" } else { "True" }
(Get-Content disco_beacon_win_test.spec) -replace 'DEBUG_MODE = (?:True|False)', "DEBUG_MODE = $debug_value" | Set-Content disco_beacon_win_test.spec
pyinstaller --clean disco_beacon_win_test.spec
- name: Upload test artifact
uses: actions/upload-artifact@v4
with:
name: DiscoBeacon_Windows_Test_${{ env.VERSION }}
path: ${{ github.workspace }}/monitor-app/dist/DiscoBeacon.exe
retention-days: 5
retention-days: 5
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ Download the latest Windows or Linux build from the Releases page:
4. **Check Interval**: Set how often to check server status (in seconds)
5. **Monitor Mode**: Choose 'Both' for game and server, or 'Server Only'

Enter these settings in the app's configuration dialog to get started. You can also configure the app by editing the `settings.json` file directly.
Enter these settings in the app's configuration dialog to get started. Settings are stored in:

- Windows: `%APPDATA%\bl4ckswordsman\Disco Beacon\settings.json`

- Linux: `~/.config/disco-beacon/settings.json`

On Windows, you can also enable "Run on system startup" in the settings dialog to automatically start the app when you log in.

## Web App (Svelte)

Expand Down
2 changes: 1 addition & 1 deletion monitor-app/disco_beacon.spec
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,4 @@ if platform.system() == "Windows":
codesign_identity=None,
entitlements_file=None,
icon=[ico_icon_path],
)
)
59 changes: 59 additions & 0 deletions monitor-app/disco_beacon_win_test.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# -*- mode: python ; coding: utf-8 -*-

import os

# Toggle debug mode here
DEBUG_MODE = True # Set to True to enable console and use main.py

block_cipher = None

# Define the paths
icons_dir = 'icons'
ico_icon_path = os.path.join('icons', 'tower-control.ico')

# Select main file based on debug mode
main_file = 'main.py' if DEBUG_MODE else 'windows_main.pyw'

a = Analysis(
[main_file], # Use the selected main file
pathex=[],
binaries=[],
datas=[
('resources', 'resources'),
(icons_dir, 'icons'), # Include the entire icons directory
],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='DiscoBeacon',
version_file=None, # Version info from version.py will be shown in the app
debug=DEBUG_MODE, # Set debug based on DEBUG_MODE
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=DEBUG_MODE, # Set console based on DEBUG_MODE
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=[ico_icon_path],
)
9 changes: 8 additions & 1 deletion monitor-app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from src.core.state import GameState, GameServerState
from src.gui.utils.gui_init import init_gui
from src.gui.utils.app_settings import AppSettings
from src.core.app_settings import settings_loader
from src.core.app_settings import settings_loader, settings_saver, handle_autorun_change
from src.gui.utils.platform_utils import is_windows

gui_available = False

Expand Down Expand Up @@ -74,6 +75,12 @@ def initialize_application():
AppSettings.set_app_metadata()
logger.info("Application setup completed")

if is_windows():
# Handle autorun based on saved setting
auto_run_enabled = settings_loader.get_setting('auto_run', False)
if not handle_autorun_change(auto_run_enabled):
settings_saver.set_setting('auto_run', False)

def main() -> None:
logger.info("Application starting")

Expand Down
8 changes: 0 additions & 8 deletions monitor-app/settings.json

This file was deleted.

153 changes: 142 additions & 11 deletions monitor-app/src/core/app_settings.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,172 @@
import json
import os
import sys
from src.gui.utils.platform_utils import is_windows
from src.core.logger import logger
from src.gui.utils.app_settings import AppSettings

if is_windows():
import winreg

class SettingsLoader:
def __init__(self, settings_file='settings.json'):
self.settings_file = settings_file
def __init__(self):
self.default_settings = {
'webhook_url': '',
'api_key': '',
'steam_id': '',
'check_interval': 10,
'game_app_id': 892970, # Valheim's App ID as default
'monitor_mode': 'both'
'monitor_mode': 'both',
'auto_run': False
}
self.settings_file = self._get_settings_path()
self.settings = self.load_settings()

def _get_settings_path(self):
"""Get the appropriate settings file path based on the platform"""
try:
if is_windows():
base_path = os.path.join(os.getenv('APPDATA', ''), AppSettings.ORG_NAME, AppSettings.APP_NAME)
else:
config_home = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
base_path = os.path.join(config_home, 'disco-beacon')

if not os.path.exists(base_path):
old_umask = os.umask(0o077)
try:
os.makedirs(base_path, mode=0o700, exist_ok=True)
finally:
os.umask(old_umask)

settings_path = os.path.join(base_path, 'settings.json')
return settings_path
except Exception as e:
logger.error("Failed to get settings path: {}".format(str(e)))
fallback_path = os.path.expanduser("~/.disco-beacon-settings.json")
logger.info("Using fallback settings path: {}".format(fallback_path))
return fallback_path

def load_settings(self):
if os.path.exists(self.settings_file):
with open(self.settings_file, 'r') as f:
return json.load(f)
return self.default_settings
try:
if os.path.exists(self.settings_file):
with open(self.settings_file, 'r') as f:
loaded_settings = json.load(f)
merged_settings = self.default_settings.copy()
merged_settings.update(loaded_settings)
return merged_settings
except Exception as e:
logger.error(f"Failed to load settings: {e}")
return self.default_settings.copy()

def get_setting(self, key, default=None):
return self.settings.get(key, self.default_settings.get(key, default))


class SettingsSaver:
def __init__(self, settings_loader):
self.settings_loader = settings_loader

def save_settings(self):
with open(self.settings_loader.settings_file, 'w') as f:
json.dump(self.settings_loader.settings, f, indent=4)
temp_file = None
old_umask = None
try:
old_umask = os.umask(0o077)
settings_dir = os.path.dirname(self.settings_loader.settings_file)

if not os.path.exists(settings_dir):
os.makedirs(settings_dir, mode=0o700)

temp_file = f"{self.settings_loader.settings_file}.tmp"
with open(temp_file, 'w') as f:
json.dump(self.settings_loader.settings, f, indent=4)
f.flush()
os.fsync(f.fileno())

os.chmod(temp_file, 0o600)
os.replace(temp_file, self.settings_loader.settings_file)
os.chmod(self.settings_loader.settings_file, 0o600)
return True

except Exception as e:
logger.error(f"Failed to save settings: {e}")
return False

finally:
if old_umask is not None:
os.umask(old_umask)
if temp_file and os.path.exists(temp_file):
try:
os.remove(temp_file)
except OSError as e:
logger.debug(f"Failed to remove temporary file: {e}")

def set_setting(self, key, value):
self.settings_loader.settings[key] = value
self.save_settings()
return self.save_settings()

def set_auto_run(app_name, app_path):
"""Set up autorun for Windows"""
if not is_windows():
return False

try:
exe_path = sys.executable if getattr(sys, 'frozen', False) else os.path.abspath(app_path)
quoted_path = f'"{exe_path}"'

logger.debug(f"Setting autorun registry entry: {quoted_path}")
key = r"Software\Microsoft\Windows\CurrentVersion\Run"
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, key) as reg_key:
winreg.SetValueEx(reg_key, app_name, 0, winreg.REG_SZ, quoted_path)
logger.debug("Registry entry created successfully")
return True
except Exception as e:
logger.error(f"Failed to set autorun registry: {e}")
return False

def remove_auto_run(app_name):
"""Remove autorun entry for Windows"""
if not is_windows():
return True

try:
key = r"Software\Microsoft\Windows\CurrentVersion\Run"
logger.debug(f"Removing autorun registry entry for {app_name}")
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key, 0, winreg.KEY_SET_VALUE) as reg_key:
winreg.DeleteValue(reg_key, app_name)
logger.debug("Registry entry removed successfully")
return True
except FileNotFoundError:
logger.debug(f"No autorun registry entry found for {app_name}")
return True
except Exception as e:
logger.error(f"Failed to remove autorun registry: {e}")
return False

def handle_autorun_change(enabled: bool):
"""Handle changes to autorun setting"""
if not is_windows():
logger.debug("Autorun change requested but system is not Windows")
return True

app_name = AppSettings.APP_NAME
app_path = os.path.abspath(sys.argv[0])

if enabled:
logger.info(f"Enabling autorun for {app_name}")
success = set_auto_run(app_name, app_path)
if success:
logger.info("Autorun successfully enabled")
else:
logger.error("Failed to enable autorun")
return success
else:
logger.info(f"Disabling autorun for {app_name}")
success = remove_auto_run(app_name)
if success:
logger.info("Autorun successfully disabled")
else:
logger.error("Failed to disable autorun")
return success

# Initialize singleton instances
settings_loader = SettingsLoader()
settings_saver = SettingsSaver(settings_loader)
43 changes: 40 additions & 3 deletions monitor-app/src/gui/settings_dialog.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit,
QSpinBox, QPushButton, QComboBox, QLabel)
QSpinBox, QPushButton, QComboBox, QLabel, QCheckBox)
from PySide6.QtCore import Signal, Qt
from src.version import __version__
from src.core.app_settings import settings_loader, settings_saver
from src.core.app_settings import settings_loader, settings_saver, handle_autorun_change
from src.core import constants
from src.gui.utils.platform_utils import is_windows_11
from src.gui.utils.platform_utils import is_windows_11, is_windows
from src.gui.utils.mica_utils import apply_mica_to_window

class SettingsDialog(QDialog):
Expand Down Expand Up @@ -50,6 +50,14 @@ def __init__(self, parent=None):
self.monitor_mode.setCurrentText(settings_loader.get_setting('monitor_mode', 'Both').title())
form_layout.addRow("Monitor Mode:", self.monitor_mode)

self.auto_run_checkbox = QCheckBox("Run on system startup")
if is_windows():
self.auto_run_checkbox.setChecked(settings_loader.get_setting('auto_run', False))
else:
self.auto_run_checkbox.setEnabled(False)
self.auto_run_checkbox.setToolTip("Auto-run is only supported on Windows")
form_layout.addRow(self.auto_run_checkbox)

self.layout.addLayout(form_layout)

save_button = QPushButton("Save")
Expand All @@ -68,10 +76,39 @@ def __init__(self, parent=None):
self.layout.addWidget(self.build_version_label)

def save_settings(self):
# Save all non-autorun settings first
settings_saver.set_setting('webhook_url', self.webhook_url.text())
settings_saver.set_setting('api_key', self.api_key.text())
settings_saver.set_setting('steam_id', self.steam_id.text())
settings_saver.set_setting('check_interval', self.check_interval.value())
settings_saver.set_setting('game_app_id', [k for k, v in constants.SUPPORTED_GAMES.items() if v == self.game_selector.currentText()][0])
settings_saver.set_setting('monitor_mode', self.monitor_mode.currentText().lower())

# Handle autorun setting separately to apply changes immediately
autorun_enabled = self.auto_run_checkbox.isChecked()

# Handle autorun changes
autorun_enabled = self.auto_run_checkbox.isChecked()
registry_updated = handle_autorun_change(autorun_enabled)
settings_saved = settings_saver.set_setting('auto_run', registry_updated and autorun_enabled)

if is_windows() and not registry_updated:
self.auto_run_checkbox.setChecked(False)
from PySide6.QtWidgets import QMessageBox
QMessageBox.warning(
self,
"Settings Error",
"Failed to save autorun settings. Make sure you have the necessary permissions."
)
return

if not settings_saved:
from PySide6.QtWidgets import QMessageBox
QMessageBox.warning(
self,
"Settings Error",
"Failed to save settings. Make sure you have write permissions in your config directory."
)
return

self.accept()
Loading