Skip to content

Commit abf163e

Browse files
erdemgokselerdemgoksel
authored andcommitted
Refactor Windows service to user session hidden window
Replaces the Windows Service implementation with a user session service that runs as a hidden window, suitable for use with Scheduled Tasks. Updates dependencies in Cargo.toml to include additional Windows API features. Adjusts install.ps1 to use the new service name and description reflecting the hidden window behavior.
1 parent c4bb80e commit abf163e

File tree

3 files changed

+65
-192
lines changed

3 files changed

+65
-192
lines changed

Cargo.toml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,13 @@ name = "beeper-automations"
33
version = "0.1.0"
44
edition = "2024"
55

6-
[[bin]]
7-
name = "auto-beeper-service"
8-
path = "src/bin/service.rs"
9-
106
[[bin]]
117
name = "auto-beeper-configurator"
128
path = "src/bin/configurator.rs"
139

1410
[[bin]]
1511
name = "auto-beeper-windows-service"
1612
path = "src/bin/windows_service.rs"
17-
required-features = ["windows-service"]
1813

1914
[dependencies]
2015
beeper-desktop-api = "0.1.1"
@@ -36,7 +31,7 @@ user-idle2 = { git = "https://github.com/ErdemGKSL/user-idle2-rs.git", features
3631

3732
[target.'cfg(windows)'.dependencies]
3833
windows-service = { version = "0.7", optional = true }
39-
windows = { version = "0.58", features = ["Win32"] }
34+
windows = { version = "0.58", features = ["Win32", "Win32_UI_WindowsAndMessaging", "Win32_System_Console"] }
4035

4136
[features]
4237
windows-service = ["dep:windows-service"]

scripts/install.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ $ErrorActionPreference = "Stop"
55

66
# Configuration
77
$GITHUB_REPO = "ErdemGKSL/beeper-automations"
8-
$SERVICE_NAME = "auto-beeper-service"
8+
$SERVICE_NAME = "auto-beeper-windows-service"
99
$CONFIGURATOR_NAME = "auto-beeper-configurator"
1010
$INSTALL_DIR = "$env:ProgramFiles\BeeperAutomations"
1111
$SCHEDULED_TASK_NAME = "BeeperAutomations"
12-
$SERVICE_DESCRIPTION = "Background service for Beeper automations (runs in user session)"
12+
$SERVICE_DESCRIPTION = "Background service for Beeper automations (runs in user session with hidden window)"
1313

1414
# Color functions
1515
function Write-InfoMessage {

src/bin/windows_service.rs

Lines changed: 62 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,205 +1,83 @@
11
#![cfg(windows)]
22

3-
// Windows Service (Session 0) Implementation
4-
//
5-
// NOTE: Windows services run in Session 0 (non-interactive), which may prevent
6-
// proper user idle detection. For accurate idle detection, install as a User Service
7-
// via install-user-service.ps1, which runs in the interactive user session.
8-
//
9-
// The user idle detection code in is_user_active() includes a fallback that detects
10-
// Session 0 and assumes the user is active when running as a Windows service.
11-
12-
use std::ffi::OsString;
13-
use std::time::Duration;
14-
use windows_service::{
15-
define_windows_service,
16-
service::{
17-
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
18-
ServiceType,
19-
},
20-
service_control_handler::{self, ServiceControlHandlerResult},
21-
service_dispatcher,
22-
};
23-
24-
const SERVICE_NAME: &str = "BeeperAutomations";
25-
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
26-
27-
// Use the shared logging function
28-
use beeper_automations::logging::log_to_file;
29-
30-
define_windows_service!(ffi_service_main, service_main);
31-
32-
fn write_crash_log(msg: &str) {
33-
let log_path = std::env::var("PROGRAMDATA")
34-
.unwrap_or_else(|_| "C:\\ProgramData".to_string())
35-
+ "\\BeeperAutomations\\service_crash.log";
36-
37-
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
38-
39-
if let Ok(mut f) = std::fs::OpenOptions::new()
40-
.create(true)
41-
.write(true)
42-
.append(true)
43-
.open(&log_path)
44-
{
45-
use std::io::Write;
46-
let _ = writeln!(f, "[{}] {}", timestamp, msg);
47-
}
48-
}
49-
50-
fn service_main(_arguments: Vec<OsString>) {
51-
write_crash_log("service_main() called");
52-
53-
// Initialize tracing for Windows service mode BEFORE any other logging
54-
beeper_automations::logging::init_logging(true);
55-
log_to_file("Windows service wrapper started");
56-
57-
log_to_file("WARNING: Running as Windows Service (Session 0)");
58-
log_to_file("WARNING: User idle detection may not work properly in service mode");
59-
log_to_file("WARNING: For proper idle detection, install as User Service via install-user-service.ps1");
60-
61-
write_crash_log("Logging initialized, about to call run_service()");
62-
63-
if let Err(e) = run_service() {
64-
let error_msg = format!("Service error: {}", e);
65-
log_to_file(&error_msg);
66-
log_to_file(&format!("Error details: {:?}", e));
67-
write_crash_log(&error_msg);
3+
// Windows User Service (Hidden Window)
4+
//
5+
// This binary runs the Beeper Automations service in the user's session
6+
// without showing a console window. It's designed to be used with Scheduled Tasks.
7+
8+
// Hide the console window at startup
9+
#[cfg(windows)]
10+
fn hide_console_window() {
11+
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_HIDE};
12+
13+
unsafe {
14+
let h_console = windows::Win32::System::Console::GetConsoleWindow();
15+
if !h_console.is_invalid() {
16+
let _ = ShowWindow(h_console, SW_HIDE);
17+
}
6818
}
69-
70-
log_to_file("Windows service wrapper exiting");
71-
write_crash_log("Windows service wrapper exiting");
7219
}
7320

74-
fn run_service() -> windows_service::Result<()> {
75-
log_to_file("run_service() called");
76-
21+
async fn main_impl() -> anyhow::Result<()> {
22+
use beeper_automations::logging::log_to_file;
23+
24+
log_to_file("Beeper Automations User Service started (hidden window)");
25+
7726
// Set working directory to ProgramData
78-
let work_dir = std::env::var("PROGRAMDATA").unwrap_or_else(|_| "C:\\ProgramData".to_string())
27+
let work_dir = std::env::var("PROGRAMDATA")
28+
.unwrap_or_else(|_| "C:\\ProgramData".to_string())
7929
+ "\\BeeperAutomations";
8030

81-
log_to_file(&format!("Creating work directory: {}", work_dir));
31+
log_to_file(&format!("Working directory: {}", work_dir));
32+
8233
if let Err(e) = std::fs::create_dir_all(&work_dir) {
83-
let error_msg = format!("Failed to create work directory: {}", e);
84-
log_to_file(&error_msg);
85-
log_to_file(&format!("Error details: {:?}", e));
86-
return Err(windows_service::Error::Winapi(std::io::Error::new(
87-
std::io::ErrorKind::Other,
88-
error_msg,
89-
)));
34+
log_to_file(&format!("Failed to create work directory: {}", e));
9035
}
91-
92-
log_to_file(&format!("Setting working directory to: {}", work_dir));
36+
9337
if let Err(e) = std::env::set_current_dir(&work_dir) {
94-
let error_msg = format!("Failed to set working directory: {}", e);
95-
log_to_file(&error_msg);
96-
log_to_file(&format!("Error details: {:?}", e));
97-
return Err(windows_service::Error::Winapi(std::io::Error::new(
98-
std::io::ErrorKind::Other,
99-
error_msg,
100-
)));
101-
} else {
102-
log_to_file(&format!("Working directory set to: {}", work_dir));
38+
log_to_file(&format!("Failed to set working directory: {}", e));
39+
return Err(e.into());
10340
}
10441

105-
// Create a channel to handle service stop events
42+
// Initialize file-based logging (no console output)
43+
beeper_automations::logging::init_logging(true);
44+
log_to_file("File logging initialized");
45+
46+
// Create shutdown channel for clean exit
10647
let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
10748

108-
// Define the service control handler
109-
let event_handler = move |control_event| -> ServiceControlHandlerResult {
110-
match control_event {
111-
ServiceControl::Stop | ServiceControl::Shutdown => {
112-
// Signal the service to stop
113-
let _ = shutdown_tx.blocking_send(());
114-
ServiceControlHandlerResult::NoError
49+
// Set up Ctrl+C handler for graceful shutdown
50+
#[cfg(windows)]
51+
{
52+
use tokio::signal::windows::ctrl_c;
53+
let mut ctrl_c = ctrl_c()?;
54+
55+
tokio::spawn(async move {
56+
if ctrl_c.recv().await.is_some() {
57+
let _ = shutdown_tx.send(()).await;
11558
}
116-
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
117-
_ => ServiceControlHandlerResult::NotImplemented,
118-
}
119-
};
120-
121-
// Register the service control handler
122-
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;
123-
124-
log_to_file("Service control handler registered");
125-
126-
// Tell Windows that the service is starting
127-
status_handle.set_service_status(ServiceStatus {
128-
service_type: SERVICE_TYPE,
129-
current_state: ServiceState::StartPending,
130-
controls_accepted: ServiceControlAccept::empty(),
131-
exit_code: ServiceExitCode::Win32(0),
132-
checkpoint: 0,
133-
wait_hint: Duration::from_secs(0),
134-
process_id: None,
135-
})?;
136-
137-
// Create a Tokio runtime for the async service
138-
let runtime = tokio::runtime::Runtime::new().map_err(|e| {
139-
windows_service::Error::Winapi(std::io::Error::new(
140-
std::io::ErrorKind::Other,
141-
format!("Failed to create Tokio runtime: {}", e),
142-
))
143-
})?;
144-
145-
// Tell Windows that the service is running
146-
status_handle.set_service_status(ServiceStatus {
147-
service_type: SERVICE_TYPE,
148-
current_state: ServiceState::Running,
149-
controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
150-
exit_code: ServiceExitCode::Win32(0),
151-
checkpoint: 0,
152-
wait_hint: Duration::from_secs(0),
153-
process_id: None,
154-
})?;
155-
156-
log_to_file("Service status set to Running");
157-
log_to_file("About to call beeper_automations::run_service_with_shutdown()");
59+
});
60+
}
15861

159-
// Run the service and wait for shutdown signal
160-
let result = runtime.block_on(async {
161-
tokio::select! {
162-
result = beeper_automations::run_service_with_shutdown(shutdown_rx) => {
163-
if let Err(e) = result {
164-
log_to_file(&format!("Service error: {}", e));
165-
log_to_file(&format!("Error details: {:?}", e));
166-
}
167-
log_to_file("run_service_with_shutdown() RETURNED");
168-
}
62+
// Run the service
63+
log_to_file("Starting service loop");
64+
let result = beeper_automations::run_service_with_shutdown(shutdown_rx).await;
65+
66+
match &result {
67+
Ok(_) => log_to_file("Service stopped gracefully"),
68+
Err(e) => {
69+
log_to_file(&format!("Service error: {}", e));
70+
log_to_file(&format!("Error details: {:?}", e));
16971
}
170-
});
171-
172-
log_to_file(&format!("Service block_on completed with result: {:?}", result));
173-
174-
log_to_file("Service loop exited, initiating shutdown");
175-
176-
// Tell Windows that the service is stopping
177-
status_handle.set_service_status(ServiceStatus {
178-
service_type: SERVICE_TYPE,
179-
current_state: ServiceState::StopPending,
180-
controls_accepted: ServiceControlAccept::empty(),
181-
exit_code: ServiceExitCode::Win32(0),
182-
checkpoint: 0,
183-
wait_hint: Duration::from_secs(0),
184-
process_id: None,
185-
})?;
186-
187-
// Tell Windows that the service has stopped
188-
status_handle.set_service_status(ServiceStatus {
189-
service_type: SERVICE_TYPE,
190-
current_state: ServiceState::Stopped,
191-
controls_accepted: ServiceControlAccept::empty(),
192-
exit_code: ServiceExitCode::Win32(0),
193-
checkpoint: 0,
194-
wait_hint: Duration::from_secs(0),
195-
process_id: None,
196-
})?;
197-
198-
Ok(())
72+
}
73+
74+
result
19975
}
20076

201-
fn main() -> windows_service::Result<()> {
202-
// Register the service with Windows Service Control Manager
203-
service_dispatcher::start(SERVICE_NAME, ffi_service_main)?;
204-
Ok(())
77+
fn main() -> anyhow::Result<()> {
78+
// Hide console window to avoid showing cmd popup
79+
hide_console_window();
80+
81+
let runtime = tokio::runtime::Runtime::new()?;
82+
runtime.block_on(main_impl())
20583
}

0 commit comments

Comments
 (0)