feat: implement Windows service wrapper - #21
Merged
Merged
Conversation
Add comprehensive Windows service functionality to allow the daemon to run as a background Windows service that can be managed via Services.msc and automatically start on system boot. Features implemented: - Windows service class using pywin32 - Service installation, start, stop, and removal commands - Integration with main daemon entry point - Proper error handling for non-Windows systems - Service logging and graceful shutdown - Comprehensive unit tests with Windows API mocking The service can be managed using: - `python -m yk_daemon.daemon --install` - Install as Windows service - `python -m yk_daemon.daemon --start` - Start the service - `python -m yk_daemon.daemon --stop` - Stop the service - `python -m yk_daemon.daemon --remove` - Remove the service The service will auto-start on Windows boot and can be managed via Services.msc. All functionality gracefully handles non-Windows environments with appropriate error messages. Resolves #9
- Replace complex Windows API mocking with simpler approach - Focus on testing actual behavior rather than implementation details - Add missing type annotations for mypy compliance - All 13 service tests now pass on both Windows and Linux - Ensures CI/CD pipeline compatibility as requested
- Fix InstallService() call to use proper argument order and service class string - Pass service class as string instead of class object - Remove duplicate displayName parameter that caused the error - Remove unused variables to fix linting - Add comprehensive type annotations and type ignore comments for mypy - Import and use proper Config type for type checking - Update print statements to show correct service information Fixes the 'InstallService() got multiple values for argument displayName' error and ensures CI pipeline passes on both Windows and Linux platforms
- Add ReportServiceStatus(SERVICE_RUNNING) call after daemon thread starts - This tells Windows that the service has successfully started - Fixes error 1053 'The service did not respond to the start or control request in a timely fashion' - Windows services must explicitly report their running status to the Service Control Manager This should resolve the service startup timeout issue on Windows
- Change service name from 'YubiKeyDaemon' to 'YubiKeyDaemonService' - Change display name from 'YubiKey Daemon' to 'YubiKey Daemon Service' - Update test to expect new service names - This avoids error 1072 'The specified service has been marked for deletion' - Provides a cleaner service name that avoids conflicts with previously deleted services Users can now install the service immediately without waiting for Windows cleanup
- Move ReportServiceStatus(SERVICE_RUNNING) to the very beginning of SvcDoRun() - This tells Windows the service is running before doing any configuration loading - Prevents 1053 timeout error by responding to Windows within the startup window - Add better error handling for configuration failures - keep service running - Add more detailed logging for debugging service startup issues The service now reports as running immediately, then does its actual work, which is the recommended pattern for Windows services
- Add debug logging to C:/temp/yk-daemon-debug.log for service troubleshooting - Log every step of service startup process with timestamps - Track configuration loading, logging setup, and daemon thread startup - Capture exceptions with detailed error messages - This will help identify where the service is failing during startup The debug log will show exactly where the service stops during the startup process
- Add detailed module-level debug logging with Python paths and environment info - Add service class definition and installation debugging - Add enhanced error logging for InstallService failures - Add debug logging for service startup process to help diagnose 1053 timeout errors - All debug logs written to C:/temp/yk-daemon-debug.log for easy troubleshooting
- Add comprehensive debug logging for main entry point with sys.argv tracking - Improve service argument detection to distinguish Windows service calls from manual commands - Add explicit binaryPath parameter to InstallService for proper Windows execution - Add robust argument parsing fallback for Windows service manager calls - Enhance error handling and debug logging throughout service startup process
- Remove the binaryPath parameter from InstallService call - Keep the enhanced debug logging but revert to original working InstallService signature - This fixes the 'unexpected keyword argument binaryPath' error
…rtup - Add proper __main__ block that calls HandleCommandLine directly when executed by Windows - Add debug logging to track Windows service execution path - This should fix the 1053 timeout error by ensuring Windows can properly start the service - Keep custom main() function for manual service management commands
…ging - Add module-level HandleCommandLine call for when Windows imports the module directly - Add comprehensive debug logging to start_service() function to track StartService() calls - Detect Windows service imports by checking sys.argv patterns - This addresses the issue where __name__ != '__main__' when Windows starts the service
- Make module-level service startup detection more permissive - Clearly distinguish between manual commands vs Windows service process - Add debug logging for is_manual_command detection logic - This should catch the actual Windows service startup process that was being missed
…import errors - Add exeName parameter to InstallService() to use our Poetry venv Python - Remove problematic service description code that caused mypy errors - This fixes the ModuleNotFoundError: No module named 'servicemanager' from Windows Event Log - The service process will now have access to pywin32 modules in our virtual environment
…tartup error The service was failing to start with 'PythonService was unable to locate the service manager' because servicemanager was being imported at module level. Made servicemanager import optional even at import time while keeping other essential win32 modules required for service functionality.
…r class Refactored service.py from 779 to 282 lines (63% reduction) by: - Introducing ServiceManager class to encapsulate all service operations - Removing all manual debug logging and module-level detection logic - Eliminating repetitive WINDOWS_SERVICE_AVAILABLE checks - Updating daemon.py to use ServiceManager directly Benefits: - Clean OOP design with single responsibility - Single availability check in ServiceManager.__init__ - Removed 497 lines of complex debugging and detection code - All tests pass (173 passed, 1 skipped)
Fixed critical service startup issues: - Report SERVICE_RUNNING immediately before any operations - Setup logging before using logger to prevent uninitialized logger calls - Import and start daemon components directly instead of calling run_daemon() (which calls sys.exit() and crashes threads) - Properly cleanup servers and notifier on shutdown This prevents error 1053: "The service did not respond to the start or control request in a timely fashion."
…r virtual environment support Refactored Windows service to use multiprocessing.Process pattern instead of threading, and register python.exe directly instead of pythonservice.exe. Key improvements: - Added _exe_name_ = sys.executable to point to venv's python.exe - Added _exe_args_ to specify script path - Use multiprocessing.Process to run daemon in separate process - Implement servicemanager.Initialize() / PrepareToHostSingle() pattern - Service startup detection via len(sys.argv)==1 - Added --fg flag for foreground testing mode - Proper multiprocessing.freeze_support() for Windows Benefits: - Proper virtual environment support - service uses venv's Python - Clean process isolation - Easier to debug and test - Standard pywin32 multiprocessing pattern This approach is recommended for Python 3.5+ virtual environments.
….exe registration Previously, even though we set _exe_name_ and _exe_args_ class attributes, InstallService was still using pythonservice.exe because we didn't pass exeName parameter explicitly. Changes: - Added exeName=sys.executable parameter to InstallService() - Added exeArgs parameter with script path and Python flags - Added debug output showing Python path and script path during install Now the service will properly use the virtual environment's python.exe instead of pythonservice.exe.
Fix multiprocessing.freeze_support() conditional call to avoid None attribute error.
Remove duplicated daemon startup logic from service.py and instead call the existing run_daemon() function from daemon.py. This eliminates code duplication and ensures consistent behavior. Also add win32traceutil to mypy ignore list in pyproject.toml to avoid conflicts between direct mypy runs and pre-commit hooks.
When running as a Windows service, the working directory is C:\Windows\System32, causing config and log files to be placed in the wrong location. This change: - Uses C:\ProgramData\yk-daemon\ for config and log files (Windows standard for service data) - Allows override via YK_DAEMON_CONFIG_PATH environment variable - Displays file locations during service installation - Creates necessary directories automatically After installation, users should place config.json in: C:\ProgramData\yk-daemon\config.json Log files will be written to: C:\ProgramData\yk-daemon\yk-daemon.log
Set up basic file logging immediately when service process starts, before attempting to load configuration. This ensures that any errors during config loading or daemon startup are captured in the log file. Critical for Windows services where stdout/stderr are not available. The log file will now be created even if the service fails to start.
Add try/except around log path resolution with fallback to temp directory if default path fails. Also add detailed startup logging showing: - Log file location - Python executable path - Working directory This ensures the service always creates a log file somewhere, even if there are permission issues with the default ProgramData location. Log locations (in priority order): 1. C:\ProgramData\yk-daemon\yk-daemon.log (preferred) 2. C:\Users\<user>\AppData\Local\Temp\yk-daemon.log (fallback)
Document both primary and fallback log file locations, and add PowerShell commands to check which location is being used.
When LoggingConfig.file was changed to use field(default_factory=...), it could no longer be accessed as a class attribute. Call _get_default_log_path() directly instead when building config from dict. This fixes the startup error: AttributeError: type object 'LoggingConfig' has no attribute 'file'
Add specific test for _build_config_from_dict with factory defaults to catch bugs where class attributes are accessed instead of calling the default_factory function. Also improve test_load_config_no_file_uses_defaults to check all config sections, not just rest_api and socket. Replace type: ignore comments with noqa comments since pre-commit's --ignore-missing-imports flag makes them unused.
Add automatic setup of service directory during installation: - Copy notification.wav to C:\ProgramData\yk-daemon\ if not present - Update default sound_file path to use service directory on Windows - Display all service file locations during installation - Search multiple locations to find notification.wav source file This ensures notification sounds work out-of-the-box when running as a Windows service without manual file copying. Also fix notification sound_file config to use default_factory like we did with logging.file to avoid class attribute access issues.
The config validation was only checking the current working directory for the sound file, which caused a false warning when running as a Windows service (CWD = C:\Windows\System32). The Notifier class already has sophisticated path resolution that searches multiple locations (CWD, project root, etc.) and gives accurate warnings. Config validation should only validate types, not file existence. This fixes the confusing behavior where: - User gets warning: "Sound file does not exist" - But sound still plays (because Notifier finds it in project root) Now the validation is consistent with actual runtime behavior.
Add methods to Config class to: - to_dict(): Convert config to dictionary for JSON serialization - save_to_file(): Save config to JSON file with proper formatting This allows the service installer to create a default config.json without hardcoding the structure, ensuring it stays in sync when the Config class changes in the future. Benefits: - No duplicated config structure code - Automatically adapts when Config class is modified - Single source of truth for config structure - Easy for users to see and modify all default settings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements comprehensive Windows service functionality for the YubiKey Daemon as requested in issue #9.
Features Added
--install- Install as Windows service--start- Start the Windows service--stop- Stop the Windows service--remove- Remove/uninstall the Windows serviceUsage
The service can be managed using the main daemon entry point:
Implementation Details
Testing
Test Plan
Acceptance Criteria
All acceptance criteria from issue #9 have been met:
Resolves #9