Skip to content

Commit

Permalink
Merge pull request #89 from patriksvensson/feature/GH-83
Browse files Browse the repository at this point in the history
Add Windows service support
  • Loading branch information
patriksvensson committed Apr 20, 2020
2 parents 21020fd + d296cc0 commit 92a9a93
Show file tree
Hide file tree
Showing 19 changed files with 601 additions and 138 deletions.
232 changes: 202 additions & 30 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@ actix-cors = "0.2.0"
actix-web-static-files = "2.0.0"
structopt = "0.3.9"
log = "0.4"
env_logger = "0.7.1"
chrono = "0.4.10"
regex = "1.3.3"
schemars = "0.7.0-alpha-1"
derive_builder = "0.9.0"
base64 = "0.11.0"
ctrlc = { version = "3.1.4", features = ["termination"] }
futures = "0.3.4"
simplelog = "0.7.5"

[target.'cfg(windows)'.dependencies]
windows-service = { git = "https://github.com/mullvad/windows-service-rs", rev="202d88bf438fdb870f4fc26e2031d36ab083fc42" }

[dev-dependencies]
test-case = "0.3.3"
Expand Down
2 changes: 1 addition & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ fn main() {
if env::var("CARGO_FEATURE_EMBEDDED_WEB").is_ok() {
let dir = Path::new("./web/dist");
if !dir.exists() {
panic!("The UI have not been built.");
panic!("The UI have not been built");
}
resource_dir("./web/dist").build().unwrap();
}
Expand Down
128 changes: 80 additions & 48 deletions src/api.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::collections::HashMap;
use std::sync::mpsc;
use std::sync::Arc;
use std::thread;

use actix_cors::Cors;
use actix_files as fs;
use actix_web::web;
use actix_web::{App, HttpServer};
use actix_rt::System;
use actix_web::dev::Server;
use actix_web::{web, App, HttpServer};
use actix_web_static_files;
use log::{debug, info};

Expand All @@ -29,63 +32,92 @@ fn generate() -> HashMap<&'static str, Resource> {
HashMap::new()
}

pub async fn start_and_block(
///////////////////////////////////////////////////////////
// Handle

pub struct HttpServerHandle {
server: actix_web::dev::Server,
}

impl HttpServerHandle {
pub fn new(server: Server) -> Self {
Self { server }
}
pub async fn stop(&self) {
info!("Stopping HTTP server...");
self.server.stop(true).await;
}
}

///////////////////////////////////////////////////////////
// Start HTTP server

pub fn start(
context: Arc<EngineState>,
server_address: Option<String>,
) -> DuckResult<()> {
// Get the address to bind to.
let bind = match server_address {
Some(ref address) => address,
None => {
if cfg!(feature = "docker") {
// Bind to host container
info!("Duck is compiled for docker, so binding to host container.");
DOCKER_SERVER_ADDRESS
} else if cfg!(feature = "embedded-web") {
// Bind to port 8080
info!("Duck is compiled with embedded UI, so binding to port 8080.");
EMBEDDED_SERVER_ADDRESS
} else {
// Bind to localhost
DEFAULT_SERVER_ADDRESS
}
}
};
) -> DuckResult<HttpServerHandle> {
let bind = get_binding(&server_address);

// Are we running embedded web?
if cfg!(feature = "embedded-web") {
debug!("Serving embedded UI.");
debug!("Serving embedded UI");
}

info!("Duck server address: {}", bind);
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let system = System::new("duck-http-server");
let server = HttpServer::new(move || {
let app = App::new()
.wrap(Cors::new().finish())
.data(context.clone())
.service(web::resource("/api/server").to(endpoints::server_info))
.service(web::resource("/api/builds").to(endpoints::get_builds))
.service(web::resource("/api/builds/view/{id}").to(endpoints::get_builds_for_view));

HttpServer::new(move || {
let app = App::new()
.wrap(Cors::new().finish())
.data(context.clone())
.service(web::resource("/api/server").to(endpoints::server_info))
.service(web::resource("/api/builds").to(endpoints::get_builds))
.service(web::resource("/api/builds/view/{id}").to(endpoints::get_builds_for_view));
// Serve static files from the web directory?
if cfg!(feature = "docker") {
return app.service(fs::Files::new("/", "./web").index_file("index.html"));
}

// Serve static files from the web directory?
if cfg!(feature = "docker") {
return app.service(fs::Files::new("/", "./web").index_file("index.html"));
}
// Serve embedded web?
if cfg!(feature = "embedded-web") {
let generated = generate();
return app.service(actix_web_static_files::ResourceFiles::new("/", generated));
}

// Serve embedded web?
if cfg!(feature = "embedded-web") {
let generated = generate();
return app.service(actix_web_static_files::ResourceFiles::new("/", generated));
}
return app;
})
.bind(bind.clone())
.unwrap()
.disable_signals()
.run();

return app;
})
.bind(bind)
.unwrap()
.run()
.await?;
info!("HTTP server started: {}", bind);

info!("Web server stopped.");
tx.send(server).unwrap();
system.run()
});

Ok(())
Ok(HttpServerHandle::new(rx.recv()?))
}

fn get_binding(server_address: &Option<String>) -> String {
// Get the address to bind to.
match server_address {
Some(ref address) => address.to_owned(),
None => {
if cfg!(feature = "docker") {
// Bind to host container
info!("Duck is compiled for docker, so binding to host container");
DOCKER_SERVER_ADDRESS.to_owned()
} else if cfg!(feature = "embedded-web") {
// Bind to port 8080
info!("Duck is compiled with embedded UI, so binding to port 8080");
EMBEDDED_SERVER_ADDRESS.to_owned()
} else {
// Bind to localhost
DEFAULT_SERVER_ADDRESS.to_owned()
}
}
}
}
3 changes: 3 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ pub mod schema;
pub mod start;
pub mod validate;

#[cfg(windows)]
pub mod service;

pub const DEFAULT_CONFIG: &str = "config.json";
pub const ENV_CONFIG: &str = "DUCK_CONFIG";
pub const ENV_BINDING: &str = "DUCK_BIND";
2 changes: 1 addition & 1 deletion src/commands/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct Arguments {
///////////////////////////////////////////////////////////
// Command

pub async fn execute(args: Arguments) -> DuckResult<()> {
pub fn execute(args: Arguments) -> DuckResult<()> {
let mut file = File::create(args.output)?;
file.write_all(duck::get_schema().as_bytes())?;
Ok(())
Expand Down
158 changes: 158 additions & 0 deletions src/commands/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use std::ffi::OsString;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use futures::executor::block_on;
use log::error;
use windows_service::service::{
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode,
ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType,
};
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
use windows_service::{define_windows_service, service_dispatcher};

use duck::DuckResult;

///////////////////////////////////////////////////////////
// Constants

const SERVICE_EXECUTABLE: &str = "duck.exe";
const SERVICE_NAME: &str = "Duck Service";
const SERVICE_DISPLAY_NAME: &str = "Duck Service";
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;

///////////////////////////////////////////////////////////
// Installation

pub fn install() -> DuckResult<()> {
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;

let service_info = ServiceInfo {
name: OsString::from(SERVICE_NAME),
display_name: OsString::from(SERVICE_DISPLAY_NAME),
service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::OnDemand,
error_control: ServiceErrorControl::Normal,
executable_path: std::env::current_exe()?.with_file_name(SERVICE_EXECUTABLE),
launch_arguments: vec![OsString::from("-f"), OsString::from("service")],
dependencies: vec![],
account_name: None, // run as System
account_password: None,
};

service_manager.create_service(&service_info, ServiceAccess::empty())?;

Ok(())
}

///////////////////////////////////////////////////////////
// Uninstallation

pub fn uninstall() -> DuckResult<()> {
let manager_access = ServiceManagerAccess::CONNECT;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;

let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE;
let service = service_manager.open_service(SERVICE_NAME, service_access)?;

let service_status = service.query_status()?;
if service_status.current_state != ServiceState::Stopped {
service.stop()?;
// Wait for service to stop
thread::sleep(Duration::from_secs(1));
}

service.delete()?;

Ok(())
}

///////////////////////////////////////////////////////////
// Running

pub fn start() -> DuckResult<()> {
service_dispatcher::start(SERVICE_NAME, ffi_service_main)?;
Ok(())
}

// Generate the windows service boilerplate.
// The boilerplate contains the low-level service entry function (ffi_service_main) that parses
// incoming service arguments into Vec<OsString> and passes them to user defined service
// entry (duck_service_main).
define_windows_service!(ffi_service_main, duck_service_main);

pub fn duck_service_main(_arguments: Vec<OsString>) {
if let Err(e) = run_service() {
error!(
"An error occured while running Duck as a Windows service: {}",
e
)
}
}

pub fn run_service() -> DuckResult<()> {
// Create a channel to be able to poll a stop event from the service worker loop.
let (shutdown_tx, shutdown_rx) = mpsc::channel();

// Define system service event handler that will be receiving service events.
let event_handler = move |control_event| -> ServiceControlHandlerResult {
match control_event {
// Notifies a service to report its current status information to the service
// control manager. Always return NoError even if not implemented.
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,

// Handle stop
ServiceControl::Stop => {
shutdown_tx.send(()).unwrap();
ServiceControlHandlerResult::NoError
}

_ => ServiceControlHandlerResult::NotImplemented,
}
};

// Register system service event handler.
// The returned status handle should be used to report service status changes to the system.
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;

// Tell the system that service is running
status_handle.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Running,
controls_accepted: ServiceControlAccept::STOP,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::from_secs(10),
})?;

// Start Duck server.
let configuration = std::env::current_exe()?.with_file_name("config.json");
let handle = duck::run(configuration, None)?;

// Wait for exit
loop {
// Poll shutdown event.
match shutdown_rx.recv_timeout(Duration::from_secs(1)) {
Ok(_) | Err(mpsc::RecvTimeoutError::Disconnected) => {
block_on(handle.stop())?;
break;
}
Err(mpsc::RecvTimeoutError::Timeout) => (),
};
}

// Tell the system that service has stopped.
status_handle.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Stopped,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::from_secs(10),
})?;

Ok(())
}
24 changes: 23 additions & 1 deletion src/commands/start.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::path::PathBuf;
use std::time::Duration;

use log::info;

use duck::DuckResult;
use structopt::StructOpt;
Expand Down Expand Up @@ -37,7 +40,26 @@ impl Default for Arguments {
// Command

pub async fn execute(args: Arguments) -> DuckResult<()> {
duck::run(args.config, args.server_address).await
let handle = duck::run(args.config, args.server_address)?;

wait_for_ctrl_c()?;

info!("Stopping...");
handle.stop().await?;
info!("Duck has been stopped");

Ok(())
}

fn wait_for_ctrl_c() -> DuckResult<()> {
let (signaler, listener) = waithandle::new();
ctrlc::set_handler(move || {
signaler.signal().expect("Error signaling listener");
})
.expect("Error setting Ctrl-C handler");
info!("Press Ctrl-C to exit");
while !listener.wait(Duration::from_millis(50))? {}
Ok(())
}

///////////////////////////////////////////////////////////
Expand Down
Loading

0 comments on commit 92a9a93

Please sign in to comment.