From 4ad8d922f7583afb2a6be8eb0ed4e4bd5d777ea7 Mon Sep 17 00:00:00 2001 From: Antoine Gersant Date: Wed, 30 Dec 2020 21:41:57 -0800 Subject: [PATCH] Platform-specific improvements (#127) * Use native-windows-gui crate to manage tray icon Adds log file support on Windows * Log file location now works like other paths * Removed context builder * Context --> App * Removed mount URLs from App * Switch to a nicer crate for forking daemon * Handle errors from notify_ready * Add application icon to all Windows Polaris executables, not just those created by the release script * Add build.rs to release tarball * Create PID file parent directory if necessary --- .gitignore | 1 + Cargo.lock | 70 ++-- Cargo.toml | 12 +- build.rs | 9 + res/unix/release_script.sh | 2 +- res/windows/application/application.manifest | 15 - res/windows/application/application.rc | 7 - .../application/icon_polaris_outline_16.png | Bin 0 -> 1878 bytes .../application/icon_polaris_outline_64.ico | Bin 32038 -> 0 bytes res/windows/release_script.ps1 | 8 +- src/app/mod.rs | 74 +++++ src/app/thumbnail/manager.rs | 4 - src/db/mod.rs | 13 +- src/main.rs | 154 ++++----- src/options.rs | 2 + src/paths.rs | 106 ++++++ src/service/actix/mod.rs | 40 +-- src/service/actix/test.rs | 30 +- src/service/mod.rs | 199 ------------ src/ui/windows.rs | 302 ++---------------- 20 files changed, 382 insertions(+), 666 deletions(-) create mode 100644 build.rs delete mode 100644 res/windows/application/application.manifest delete mode 100644 res/windows/application/application.rc create mode 100644 res/windows/application/icon_polaris_outline_16.png delete mode 100644 res/windows/application/icon_polaris_outline_64.ico create mode 100644 src/paths.rs diff --git a/.gitignore b/.gitignore index 23bee30..3bc29b8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ TestConfig.toml # Runtime artifacts *.sqlite +polaris.log /thumbnails # Release process artifacts (usually runs on CI) diff --git a/Cargo.lock b/Cargo.lock index 6abcc45..5fd8d82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,12 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "boxfnonce" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426" + [[package]] name = "branca" version = "0.10.0" @@ -691,6 +697,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "daemonize" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70c24513e34f53b640819f0ac9f705b673fcf4006d7aab8778bee72ebfc89815" +dependencies = [ + "boxfnonce", + "libc", +] + [[package]] name = "deflate" version = "0.8.6" @@ -1561,6 +1577,29 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b78760a249b7611363d02cfbd56974e1957faf2caa4fce36d4207b7edc803b1" +[[package]] +name = "native-windows-derive" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e12bdd46113e604a98d04f19f79249e1679be21a65eaa1dbadec16ba00c94f7" +dependencies = [ + "proc-macro2", + "quote 1.0.7", + "syn 1.0.54", +] + +[[package]] +name = "native-windows-gui" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe8d44e6cea6bba40a302d1ab3ee50c6d9f9714ab94a776b0db0a5521c49c9ce" +dependencies = [ + "bitflags", + "lazy_static", + "winapi 0.3.9", + "winapi-build", +] + [[package]] name = "net2" version = "0.2.36" @@ -1819,6 +1858,7 @@ dependencies = [ "branca", "cookie", "crossbeam-channel", + "daemonize", "diesel", "diesel_migrations", "fs_extra", @@ -1834,6 +1874,8 @@ dependencies = [ "metaflac", "mp3-duration", "mp4ameta", + "native-windows-derive", + "native-windows-gui", "num_cpus", "opus_headers", "pbkdf2", @@ -1850,11 +1892,9 @@ dependencies = [ "thiserror", "time 0.2.23", "toml", - "unix-daemonize", "ureq", "url", - "uuid", - "winapi 0.3.9", + "winres", ] [[package]] @@ -2666,15 +2706,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -[[package]] -name = "unix-daemonize" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531faed80732acaa13d1016c66d6a9180b5045c4fcef8daa20bb2baf46b13907" -dependencies = [ - "libc", -] - [[package]] name = "untrusted" version = "0.7.1" @@ -2712,12 +2743,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "uuid" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" - [[package]] name = "v_escape" version = "0.14.1" @@ -2927,6 +2952,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "winres" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4fb510bbfe5b8992ff15f77a2e6fe6cf062878f0eda00c0f44963a807ca5dc" +dependencies = [ + "toml", +] + [[package]] name = "wrapped-vec" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index e708912..1d09d9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,12 @@ name = "polaris" version = "0.0.0" authors = ["Antoine Gersant "] edition = "2018" +build = "build.rs" [features] default = ["bundle-sqlite"] bundle-sqlite = ["libsqlite3-sys"] -ui = ["uuid", "winapi"] +ui = ["native-windows-gui", "native-windows-derive"] [dependencies] actix-files = { version = "0.4" } @@ -59,12 +60,15 @@ default_features = false features = ["bmp", "gif", "jpeg", "png"] [target.'cfg(windows)'.dependencies] -uuid = { version="0.8", optional = true } -winapi = { version = "0.3.3", features = ["winuser", "libloaderapi", "shellapi", "errhandlingapi"], optional = true } +native-windows-gui = {version = "1.0.7", default-features = false, features = ["cursor", "image-decoder", "message-window", "menu", "tray-notification"], optional = true } +native-windows-derive = {version = "1.0.2", optional = true } [target.'cfg(unix)'.dependencies] +daemonize = "0.4.1" sd-notify = "0.1.0" -unix-daemonize = "0.1.2" + +[target.'cfg(windows)'.build-dependencies] +winres = "0.1" [dev-dependencies] headers = "0.3" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..dd021f4 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +#[cfg(windows)] +fn main() { + let mut res = winres::WindowsResource::new(); + res.set_icon("./res/windows/application/icon_polaris_512.ico"); + res.compile().unwrap(); +} + +#[cfg(unix)] +fn main() {} diff --git a/res/unix/release_script.sh b/res/unix/release_script.sh index 8edbbf9..90cd1d0 100755 --- a/res/unix/release_script.sh +++ b/res/unix/release_script.sh @@ -3,7 +3,7 @@ echo "Creating output directory" mkdir -p release/tmp/polaris echo "Copying package files" -cp -r web docs/swagger src migrations test-data Cargo.toml Cargo.lock rust-toolchain res/unix/Makefile release/tmp/polaris +cp -r web docs/swagger src migrations test-data build.rs Cargo.toml Cargo.lock rust-toolchain res/unix/Makefile release/tmp/polaris echo "Creating tarball" tar -zc -C release/tmp -f release/polaris.tar.gz polaris diff --git a/res/windows/application/application.manifest b/res/windows/application/application.manifest deleted file mode 100644 index 283a35d..0000000 --- a/res/windows/application/application.manifest +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - true - - - \ No newline at end of file diff --git a/res/windows/application/application.rc b/res/windows/application/application.rc deleted file mode 100644 index 88ee412..0000000 --- a/res/windows/application/application.rc +++ /dev/null @@ -1,7 +0,0 @@ -#define IDI_POLARIS 0x101 -#define IDI_POLARIS_TRAY 0x102 - -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "application.manifest" - -IDI_POLARIS ICON "icon_polaris_512.ico" -IDI_POLARIS_TRAY ICON "icon_polaris_outline_64.ico" diff --git a/res/windows/application/icon_polaris_outline_16.png b/res/windows/application/icon_polaris_outline_16.png new file mode 100644 index 0000000000000000000000000000000000000000..2dbf94e6048fb857e1fe177cb5a8894b1cd08b52 GIT binary patch literal 1878 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 diff --git a/res/windows/release_script.ps1 b/res/windows/release_script.ps1 index b041079..b4c7491 100644 --- a/res/windows/release_script.ps1 +++ b/res/windows/release_script.ps1 @@ -2,10 +2,6 @@ if (!(Test-Path env:POLARIS_VERSION)) { throw "POLARIS_VERSION environment variable is not defined" } -"Compiling resource file" -$rc_exe = Join-Path "C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64" RC.exe -& $rc_exe /fo res\windows\application\application.res res\windows\application\application.rc - "" "Compiling executable" # TODO: Uncomment the following once Polaris can do variable expansion of %LOCALAPPDATA% @@ -17,8 +13,8 @@ $rc_exe = Join-Path "C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64 # $env:POLARIS_LOG_DIR = "$INSTALL_DIR" # $env:POLARIS_CACHE_DIR = "$INSTALL_DIR" # $env:POLARIS_PID_DIR = "$INSTALL_DIR" -cargo rustc --release --features "ui" -- -C link-args="/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup res\windows\application\application.res" -cargo rustc --release -- -o ".\target\release\polaris-cli.exe" -C link-args="res\windows\application\application.res" +cargo rustc --release --features "ui" -- -o ".\target\release\polaris.exe" +cargo rustc --release -- -o ".\target\release\polaris-cli.exe" "" "Creating output directory" diff --git a/src/app/mod.rs b/src/app/mod.rs index bd3f7bc..3a1da3e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,3 +1,9 @@ +use std::fs; +use std::path::PathBuf; + +use crate::db::DB; +use crate::paths::Paths; + pub mod config; pub mod ddns; pub mod index; @@ -10,3 +16,71 @@ pub mod vfs; #[cfg(test)] pub mod test; + +#[derive(Clone)] +pub struct App { + pub port: u16, + pub auth_secret: settings::AuthSecret, + pub web_dir_path: PathBuf, + pub swagger_dir_path: PathBuf, + pub db: DB, + pub index: index::Index, + pub config_manager: config::Manager, + pub ddns_manager: ddns::Manager, + pub lastfm_manager: lastfm::Manager, + pub playlist_manager: playlist::Manager, + pub settings_manager: settings::Manager, + pub thumbnail_manager: thumbnail::Manager, + pub user_manager: user::Manager, + pub vfs_manager: vfs::Manager, +} + +impl App { + pub fn new(port: u16, paths: Paths) -> anyhow::Result { + let db = DB::new(&paths.db_file_path)?; + fs::create_dir_all(&paths.web_dir_path)?; + fs::create_dir_all(&paths.swagger_dir_path)?; + + let thumbnails_dir_path = paths.cache_dir_path.join("thumbnails"); + + let vfs_manager = vfs::Manager::new(db.clone()); + let settings_manager = settings::Manager::new(db.clone()); + let auth_secret = settings_manager.get_auth_secret()?; + let ddns_manager = ddns::Manager::new(db.clone()); + let user_manager = user::Manager::new(db.clone(), auth_secret); + let index = index::Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone()); + let config_manager = config::Manager::new( + settings_manager.clone(), + user_manager.clone(), + vfs_manager.clone(), + ddns_manager.clone(), + ); + let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone()); + let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path); + let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone()); + + if let Some(config_path) = paths.config_file_path { + let config = config::Config::from_path(&config_path)?; + config_manager.apply(&config)?; + } + + let auth_secret = settings_manager.get_auth_secret()?; + + Ok(Self { + port, + auth_secret, + web_dir_path: paths.web_dir_path, + swagger_dir_path: paths.swagger_dir_path, + index, + config_manager, + ddns_manager, + lastfm_manager, + playlist_manager, + settings_manager, + thumbnail_manager, + user_manager, + vfs_manager, + db, + }) + } +} diff --git a/src/app/thumbnail/manager.rs b/src/app/thumbnail/manager.rs index a6cdf4d..82dbdaa 100644 --- a/src/app/thumbnail/manager.rs +++ b/src/app/thumbnail/manager.rs @@ -19,10 +19,6 @@ impl Manager { } } - pub fn get_directory(&self) -> &Path { - &self.thumbnails_dir_path - } - pub fn get_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Result { match self.retrieve_thumbnail(image_path, thumbnailoptions) { Some(path) => Ok(path), diff --git a/src/db/mod.rs b/src/db/mod.rs index 568840e..c99e598 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,7 +3,7 @@ use diesel::r2d2::{self, ConnectionManager, PooledConnection}; use diesel::sqlite::SqliteConnection; use diesel::RunQueryDsl; use diesel_migrations; -use std::path::{Path, PathBuf}; +use std::path::Path; mod schema; @@ -16,7 +16,6 @@ embed_migrations!("migrations"); #[derive(Clone)] pub struct DB { pool: r2d2::Pool>, - location: PathBuf, } #[derive(Debug)] @@ -42,22 +41,16 @@ impl diesel::r2d2::CustomizeConnection impl DB { pub fn new(path: &Path) -> Result { + std::fs::create_dir_all(&path.parent().unwrap())?; let manager = ConnectionManager::::new(path.to_string_lossy()); let pool = diesel::r2d2::Pool::builder() .connection_customizer(Box::new(ConnectionCustomizer {})) .build(manager)?; - let db = DB { - pool: pool, - location: path.to_owned(), - }; + let db = DB { pool: pool }; db.migrate_up()?; Ok(db) } - pub fn location(&self) -> &Path { - &self.location - } - pub fn connect(&self) -> Result>> { self.pool.get().map_err(Error::new) } diff --git a/src/main.rs b/src/main.rs index 721a07c..5fb2add 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![cfg_attr(all(windows, feature = "ui"), windows_subsystem = "windows")] #![recursion_limit = "256"] #[macro_use] @@ -6,12 +7,15 @@ extern crate diesel; extern crate diesel_migrations; use anyhow::*; -use log::{error, info}; -use simplelog::{LevelFilter, SimpleLogger, TermLogger, TerminalMode}; +use log::info; +use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger}; +use std::fs; +use std::path::PathBuf; mod app; mod db; mod options; +mod paths; mod service; #[cfg(test)] mod test; @@ -19,75 +23,46 @@ mod ui; mod utils; #[cfg(unix)] -fn daemonize( - foreground: bool, - pid_file_path: &Option, - log_file_path: &Option, -) -> Result<()> { - use std::fs; - use std::io::Write; - use std::path::PathBuf; - use unix_daemonize::{daemonize_redirect, ChdirMode}; - +fn daemonize(foreground: bool, pid_file_path: &PathBuf) -> Result<()> { if foreground { return Ok(()); } - - let log_path = log_file_path.clone().unwrap_or_else(|| { - let mut path = PathBuf::from(option_env!("POLARIS_LOG_DIR").unwrap_or(".")); - path.push("polaris.log"); - path - }); - fs::create_dir_all(&log_path.parent().unwrap())?; - - let pid = match daemonize_redirect(Some(&log_path), Some(&log_path), ChdirMode::NoChdir) { - Ok(p) => p, - Err(e) => bail!("Daemonize error: {:#?}", e), - }; - - let pid_path = pid_file_path.clone().unwrap_or_else(|| { - let mut path = PathBuf::from(option_env!("POLARIS_PID_DIR").unwrap_or(".")); - path.push("polaris.pid"); - path - }); - fs::create_dir_all(&pid_path.parent().unwrap())?; - - let mut file = fs::File::create(pid_path)?; - file.write_all(pid.to_string().as_bytes())?; + if let Some(parent) = pid_file_path.parent() { + fs::create_dir_all(parent)?; + } + let daemonize = daemonize::Daemonize::new() + .pid_file(pid_file_path) + .working_directory("."); + daemonize.start()?; Ok(()) } #[cfg(unix)] -fn notify_ready() { +fn notify_ready() -> Result<()> { if let Ok(true) = sd_notify::booted() { - if let Err(e) = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]) { - error!("Unable to send ready notification: {}", e); - } + sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; } + Ok(()) } -#[cfg(not(unix))] -fn notify_ready() {} - -fn init_logging(cli_options: &options::CLIOptions) -> Result<()> { - let log_level = cli_options.log_level.unwrap_or(LevelFilter::Info); +fn init_logging(log_level: LevelFilter, log_file_path: &PathBuf) -> Result<()> { let log_config = simplelog::ConfigBuilder::new() .set_location_level(LevelFilter::Error) .build(); - #[cfg(unix)] - let prefer_term_logger = cli_options.foreground; - - #[cfg(not(unix))] - let prefer_term_logger = true; - - if prefer_term_logger { - match TermLogger::init(log_level, log_config.clone(), TerminalMode::Stdout) { - Ok(_) => return Ok(()), - Err(e) => error!("Error starting terminal logger: {}", e), - } + if let Some(parent) = log_file_path.parent() { + fs::create_dir_all(parent)?; } - SimpleLogger::init(log_level, log_config)?; + + CombinedLogger::init(vec![ + TermLogger::new(log_level, log_config.clone(), TerminalMode::Mixed), + WriteLogger::new( + log_level, + log_config.clone(), + fs::File::create(log_file_path)?, + ), + ])?; + Ok(()) } @@ -104,60 +79,41 @@ fn main() -> Result<()> { return Ok(()); } + let paths = paths::Paths::new(&cli_options); + + // Logging + let log_level = cli_options.log_level.unwrap_or(LevelFilter::Info); + init_logging(log_level, &paths.log_file_path)?; + + // Fork #[cfg(unix)] - daemonize( - cli_options.foreground, - &cli_options.pid_file_path, - &cli_options.log_file_path, - )?; + daemonize(cli_options.foreground, &paths.pid_file_path)?; - init_logging(&cli_options)?; + info!("Cache files location is {:#?}", paths.cache_dir_path); + info!("Config files location is {:#?}", paths.config_file_path); + info!("Database file location is {:#?}", paths.db_file_path); + info!("Log file location is {:#?}", paths.log_file_path); + #[cfg(unix)] + if !cli_options.foreground { + info!("Pid file location is {:#?}", paths.pid_file_path); + } + info!("Swagger files location is {:#?}", paths.swagger_dir_path); + info!("Web client files location is {:#?}", paths.web_dir_path); - // Create service context - let mut context_builder = service::ContextBuilder::new(); - if let Some(port) = cli_options.port { - context_builder = context_builder.port(port); - } - if let Some(path) = cli_options.config_file_path { - info!("Config file location is {:#?}", path); - context_builder = context_builder.config_file_path(path); - } - if let Some(path) = cli_options.database_file_path { - context_builder = context_builder.database_file_path(path); - } - if let Some(path) = cli_options.web_dir_path { - context_builder = context_builder.web_dir_path(path); - } - if let Some(path) = cli_options.swagger_dir_path { - context_builder = context_builder.swagger_dir_path(path); - } - if let Some(path) = cli_options.cache_dir_path { - context_builder = context_builder.cache_dir_path(path); - } - - let context = context_builder.build()?; - info!("Database file location is {:#?}", context.db.location()); - info!("Web client files location is {:#?}", context.web_dir_path); - info!("Swagger files location is {:#?}", context.swagger_dir_path); - info!( - "Thumbnails files location is {:#?}", - context.thumbnail_manager.get_directory() - ); - - // Begin collection scans - context.index.begin_periodic_updates(); - - // Start DDNS updates - context.ddns_manager.begin_periodic_updates(); + // Create and run app + let app = app::App::new(cli_options.port.unwrap_or(5050), paths)?; + app.index.begin_periodic_updates(); + app.ddns_manager.begin_periodic_updates(); // Start server info!("Starting up server"); std::thread::spawn(move || { - let _ = service::run(context); + let _ = service::run(app); }); // Send readiness notification - notify_ready(); + #[cfg(unix)] + notify_ready()?; // Run UI ui::run(); diff --git a/src/options.rs b/src/options.rs index e3d5e11..339338f 100644 --- a/src/options.rs +++ b/src/options.rs @@ -7,6 +7,7 @@ pub struct CLIOptions { #[cfg(unix)] pub foreground: bool, pub log_file_path: Option, + #[cfg(unix)] pub pid_file_path: Option, pub config_file_path: Option, pub database_file_path: Option, @@ -36,6 +37,7 @@ impl Manager { #[cfg(unix)] foreground: matches.opt_present("f"), log_file_path: matches.opt_str("log").map(PathBuf::from), + #[cfg(unix)] pid_file_path: matches.opt_str("pid").map(PathBuf::from), config_file_path: matches.opt_str("c").map(PathBuf::from), database_file_path: matches.opt_str("d").map(PathBuf::from), diff --git a/src/paths.rs b/src/paths.rs new file mode 100644 index 0000000..d30ecb1 --- /dev/null +++ b/src/paths.rs @@ -0,0 +1,106 @@ +use std::path::PathBuf; + +use crate::options::CLIOptions; + +pub struct Paths { + pub cache_dir_path: PathBuf, + pub config_file_path: Option, + pub db_file_path: PathBuf, + pub log_file_path: PathBuf, + #[cfg(unix)] + pub pid_file_path: PathBuf, + pub swagger_dir_path: PathBuf, + pub web_dir_path: PathBuf, +} + +// TODO Make this the only implementation when we can expand %LOCALAPPDATA% correctly on Windows +// And fix the installer accordingly (`release_script.ps1`) +#[cfg(not(windows))] +impl Default for Paths { + fn default() -> Self { + Self { + cache_dir_path: ["."].iter().collect(), + config_file_path: None, + db_file_path: [".", "db.sqlite"].iter().collect(), + log_file_path: [".", "polaris.log"].iter().collect(), + pid_file_path: [".", "polaris.pid"].iter().collect(), + swagger_dir_path: [".", "docs", "swagger"].iter().collect(), + web_dir_path: [".", "web"].iter().collect(), + } + } +} + +#[cfg(windows)] +impl Default for Paths { + fn default() -> Self { + let local_app_data = std::env::var("LOCALAPPDATA").map(PathBuf::from).unwrap(); + let install_directory: PathBuf = + local_app_data.join(["Permafrost", "Polaris"].iter().collect::()); + Self { + cache_dir_path: install_directory.clone(), + config_file_path: None, + db_file_path: install_directory.join("db.sqlite"), + log_file_path: install_directory.join("polaris.log"), + swagger_dir_path: install_directory.join("swagger"), + web_dir_path: install_directory.join("web"), + } + } +} + +impl Paths { + fn from_build() -> Self { + let defaults = Self::default(); + Self { + db_file_path: option_env!("POLARIS_DB_DIR") + .map(PathBuf::from) + .map(|p| p.join("db.sqlite")) + .unwrap_or(defaults.db_file_path), + config_file_path: None, + cache_dir_path: option_env!("POLARIS_CACHE_DIR") + .map(PathBuf::from) + .unwrap_or(defaults.cache_dir_path), + log_file_path: option_env!("POLARIS_LOG_DIR") + .map(PathBuf::from) + .map(|p| p.join("polaris.log")) + .unwrap_or(defaults.log_file_path), + #[cfg(unix)] + pid_file_path: option_env!("POLARIS_PID_DIR") + .map(PathBuf::from) + .map(|p| p.join("polaris.pid")) + .unwrap_or(defaults.pid_file_path), + swagger_dir_path: option_env!("POLARIS_SWAGGER_DIR") + .map(PathBuf::from) + .unwrap_or(defaults.swagger_dir_path), + web_dir_path: option_env!("POLARIS_WEB_DIR") + .map(PathBuf::from) + .unwrap_or(defaults.web_dir_path), + } + } + + pub fn new(cli_options: &CLIOptions) -> Self { + let mut paths = Self::from_build(); + if let Some(path) = &cli_options.cache_dir_path { + paths.cache_dir_path = path.clone(); + } + if let Some(path) = &cli_options.config_file_path { + paths.config_file_path = Some(path.clone()); + } + if let Some(path) = &cli_options.database_file_path { + paths.db_file_path = path.clone(); + } + if let Some(path) = &cli_options.log_file_path { + paths.log_file_path = path.clone(); + } + #[cfg(unix)] + if let Some(path) = &cli_options.pid_file_path { + paths.pid_file_path = path.clone(); + } + if let Some(path) = &cli_options.swagger_dir_path { + paths.swagger_dir_path = path.clone(); + } + if let Some(path) = &cli_options.web_dir_path { + paths.web_dir_path = path.clone(); + } + return paths; + } +} diff --git a/src/service/actix/mod.rs b/src/service/actix/mod.rs index 7847893..fef7c4a 100644 --- a/src/service/actix/mod.rs +++ b/src/service/actix/mod.rs @@ -2,58 +2,58 @@ use actix_web::{ middleware::{normalize::TrailingSlash, Compress, Logger, NormalizePath}, rt::System, web::{self, ServiceConfig}, - App, HttpServer, + App as ActixApp, HttpServer, }; use anyhow::*; use log::error; -use crate::service; +use crate::app::App; mod api; #[cfg(test)] pub mod test; -pub fn make_config(context: service::Context) -> impl FnOnce(&mut ServiceConfig) + Clone { +pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone { move |cfg: &mut ServiceConfig| { - let encryption_key = cookie::Key::derive_from(&context.auth_secret.key[..]); - cfg.app_data(web::Data::new(context.index)) - .app_data(web::Data::new(context.config_manager)) - .app_data(web::Data::new(context.ddns_manager)) - .app_data(web::Data::new(context.lastfm_manager)) - .app_data(web::Data::new(context.playlist_manager)) - .app_data(web::Data::new(context.settings_manager)) - .app_data(web::Data::new(context.thumbnail_manager)) - .app_data(web::Data::new(context.user_manager)) - .app_data(web::Data::new(context.vfs_manager)) + let encryption_key = cookie::Key::derive_from(&app.auth_secret.key[..]); + cfg.app_data(web::Data::new(app.index)) + .app_data(web::Data::new(app.config_manager)) + .app_data(web::Data::new(app.ddns_manager)) + .app_data(web::Data::new(app.lastfm_manager)) + .app_data(web::Data::new(app.playlist_manager)) + .app_data(web::Data::new(app.settings_manager)) + .app_data(web::Data::new(app.thumbnail_manager)) + .app_data(web::Data::new(app.user_manager)) + .app_data(web::Data::new(app.vfs_manager)) .app_data(web::Data::new(encryption_key)) .service( - web::scope(&context.api_url) + web::scope("/api") .configure(api::make_config()) .wrap_fn(api::http_auth_middleware) .wrap(NormalizePath::new(TrailingSlash::Trim)), ) .service( - actix_files::Files::new(&context.swagger_url, context.swagger_dir_path) + actix_files::Files::new("/swagger", app.swagger_dir_path) .redirect_to_slash_directory() .index_file("index.html"), ) .service( - actix_files::Files::new(&context.web_url, context.web_dir_path) + actix_files::Files::new("/", app.web_dir_path) .redirect_to_slash_directory() .index_file("index.html"), ); } } -pub fn run(context: service::Context) -> Result<()> { +pub fn run(app: App) -> Result<()> { System::run(move || { - let address = format!("0.0.0.0:{}", context.port); + let address = format!("0.0.0.0:{}", app.port); HttpServer::new(move || { - App::new() + ActixApp::new() .wrap(Logger::default()) .wrap(Compress::default()) - .configure(make_config(context.clone())) + .configure(make_config(app.clone())) }) .disable_signals() .bind(address) diff --git a/src/service/actix/test.rs b/src/service/actix/test.rs index 17e0c1e..e832dfd 100644 --- a/src/service/actix/test.rs +++ b/src/service/actix/test.rs @@ -4,14 +4,15 @@ use actix_web::{ test, test::*, web::Bytes, - App, + App as ActixApp, }; use http::{response::Builder, Method, Request, Response}; use serde::de::DeserializeOwned; use serde::Serialize; use std::ops::Deref; -use std::path::{Path, PathBuf}; +use crate::app::App; +use crate::paths::Paths; use crate::service::actix::*; use crate::service::dto; use crate::service::test::TestService; @@ -77,21 +78,24 @@ impl ActixTestService { impl TestService for ActixTestService { fn new(test_name: &str) -> Self { let output_dir = prepare_test_directory(test_name); - let db_path: PathBuf = output_dir.join("db.sqlite"); - let context = service::ContextBuilder::new() - .port(5050) - .database_file_path(db_path) - .web_dir_path(Path::new("test-data/web").into()) - .swagger_dir_path(["docs", "swagger"].iter().collect()) - .cache_dir_path(["test-output", test_name].iter().collect()) - .build() - .unwrap(); + let paths = Paths { + cache_dir_path: ["test-output", test_name].iter().collect(), + config_file_path: None, + db_file_path: output_dir.join("db.sqlite"), + #[cfg(unix)] + pid_file_path: output_dir.join("polaris.pid"), + log_file_path: output_dir.join("polaris.log"), + swagger_dir_path: ["docs", "swagger"].iter().collect(), + web_dir_path: ["test-data", "web"].iter().collect(), + }; + + let app = App::new(5050, paths).unwrap(); let system_runner = System::new("test"); let server = test::start(move || { - let config = make_config(context.clone()); - App::new() + let config = make_config(app.clone()); + ActixApp::new() .wrap(Logger::default()) .wrap(Compress::default()) .configure(config) diff --git a/src/service/mod.rs b/src/service/mod.rs index fe1caa4..a351576 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,9 +1,3 @@ -use std::fs; -use std::path::PathBuf; - -use crate::app::{config, ddns, index::Index, lastfm, playlist, settings, thumbnail, user, vfs}; -use crate::db::DB; - mod dto; mod error; @@ -12,196 +6,3 @@ mod test; mod actix; pub use actix::*; - -#[derive(Clone)] -pub struct Context { - pub port: u16, - pub auth_secret: settings::AuthSecret, - pub web_dir_path: PathBuf, - pub swagger_dir_path: PathBuf, - pub web_url: String, - pub swagger_url: String, - pub api_url: String, - pub db: DB, - pub index: Index, - pub config_manager: config::Manager, - pub ddns_manager: ddns::Manager, - pub lastfm_manager: lastfm::Manager, - pub playlist_manager: playlist::Manager, - pub settings_manager: settings::Manager, - pub thumbnail_manager: thumbnail::Manager, - pub user_manager: user::Manager, - pub vfs_manager: vfs::Manager, -} - -struct Paths { - db_dir_path: PathBuf, - web_dir_path: PathBuf, - swagger_dir_path: PathBuf, - cache_dir_path: PathBuf, -} - -// TODO Make this the only implementation when we can expand %LOCALAPPDATA% correctly on Windows -// And fix the installer accordingly (`release_script.ps1`) -#[cfg(not(windows))] -impl Default for Paths { - fn default() -> Self { - Self { - db_dir_path: ["."].iter().collect(), - web_dir_path: [".", "web"].iter().collect(), - swagger_dir_path: [".", "docs", "swagger"].iter().collect(), - cache_dir_path: ["."].iter().collect(), - } - } -} - -#[cfg(windows)] -impl Default for Paths { - fn default() -> Self { - let local_app_data = std::env::var("LOCALAPPDATA").map(PathBuf::from).unwrap(); - let install_directory: PathBuf = - local_app_data.join(["Permafrost", "Polaris"].iter().collect::()); - Self { - db_dir_path: install_directory.clone(), - web_dir_path: install_directory.join("web"), - swagger_dir_path: install_directory.join("swagger"), - cache_dir_path: install_directory.clone(), - } - } -} - -impl Paths { - fn new() -> Self { - let defaults = Self::default(); - Self { - db_dir_path: option_env!("POLARIS_DB_DIR") - .map(PathBuf::from) - .unwrap_or(defaults.db_dir_path), - web_dir_path: option_env!("POLARIS_WEB_DIR") - .map(PathBuf::from) - .unwrap_or(defaults.web_dir_path), - swagger_dir_path: option_env!("POLARIS_SWAGGER_DIR") - .map(PathBuf::from) - .unwrap_or(defaults.swagger_dir_path), - cache_dir_path: option_env!("POLARIS_CACHE_DIR") - .map(PathBuf::from) - .unwrap_or(defaults.cache_dir_path), - } - } -} - -pub struct ContextBuilder { - port: Option, - config_file_path: Option, - database_file_path: Option, - web_dir_path: Option, - swagger_dir_path: Option, - cache_dir_path: Option, -} - -impl ContextBuilder { - pub fn new() -> Self { - Self { - port: None, - config_file_path: None, - database_file_path: None, - web_dir_path: None, - swagger_dir_path: None, - cache_dir_path: None, - } - } - - pub fn build(self) -> anyhow::Result { - let paths = Paths::new(); - - let db_path = self - .database_file_path - .unwrap_or(paths.db_dir_path.join("db.sqlite")); - fs::create_dir_all(&db_path.parent().unwrap())?; - let db = DB::new(&db_path)?; - - let web_dir_path = self.web_dir_path.unwrap_or(paths.web_dir_path); - fs::create_dir_all(&web_dir_path)?; - - let swagger_dir_path = self.swagger_dir_path.unwrap_or(paths.swagger_dir_path); - fs::create_dir_all(&swagger_dir_path)?; - - let thumbnails_dir_path = self - .cache_dir_path - .unwrap_or(paths.cache_dir_path) - .join("thumbnails"); - - let vfs_manager = vfs::Manager::new(db.clone()); - let settings_manager = settings::Manager::new(db.clone()); - let auth_secret = settings_manager.get_auth_secret()?; - let ddns_manager = ddns::Manager::new(db.clone()); - let user_manager = user::Manager::new(db.clone(), auth_secret); - let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone()); - let config_manager = config::Manager::new( - settings_manager.clone(), - user_manager.clone(), - vfs_manager.clone(), - ddns_manager.clone(), - ); - let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone()); - let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path); - let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone()); - - if let Some(config_path) = self.config_file_path { - let config = config::Config::from_path(&config_path)?; - config_manager.apply(&config)?; - } - - let auth_secret = settings_manager.get_auth_secret()?; - - Ok(Context { - port: self.port.unwrap_or(5050), - auth_secret, - api_url: "/api".to_owned(), - swagger_url: "/swagger".to_owned(), - web_url: "/".to_owned(), - web_dir_path, - swagger_dir_path, - index, - config_manager, - ddns_manager, - lastfm_manager, - playlist_manager, - settings_manager, - thumbnail_manager, - user_manager, - vfs_manager, - db, - }) - } - - pub fn port(mut self, port: u16) -> Self { - self.port = Some(port); - self - } - - pub fn config_file_path(mut self, path: PathBuf) -> Self { - self.config_file_path = Some(path); - self - } - - pub fn database_file_path(mut self, path: PathBuf) -> Self { - self.database_file_path = Some(path); - self - } - - pub fn web_dir_path(mut self, path: PathBuf) -> Self { - self.web_dir_path = Some(path); - self - } - - pub fn swagger_dir_path(mut self, path: PathBuf) -> Self { - self.swagger_dir_path = Some(path); - self - } - - pub fn cache_dir_path(mut self, path: PathBuf) -> Self { - self.cache_dir_path = Some(path); - self - } -} diff --git a/src/ui/windows.rs b/src/ui/windows.rs index 08ec541..f827b33 100644 --- a/src/ui/windows.rs +++ b/src/ui/windows.rs @@ -1,282 +1,44 @@ use log::info; -use std; -use std::ffi::OsStr; -use std::os::windows::ffi::OsStrExt; -use uuid; -use winapi; -use winapi::shared::minwindef::{DWORD, LOWORD, LPARAM, LRESULT, UINT, WPARAM}; -use winapi::shared::windef::HWND; -use winapi::um::{shellapi, winuser}; +use native_windows_derive::NwgUi; +use native_windows_gui::{self as nwg, NativeUi}; -const IDI_POLARIS_TRAY: isize = 0x102; -const UID_NOTIFICATION_ICON: u32 = 0; -const MESSAGE_NOTIFICATION_ICON: u32 = winuser::WM_USER + 1; -const MESSAGE_NOTIFICATION_ICON_QUIT: u32 = winuser::WM_USER + 2; +const TRAY_ICON: &[u8] = + include_bytes!("../../res/windows/application/icon_polaris_outline_16.png"); -pub trait ToWin { - type Out; - fn to_win(&self) -> Self::Out; +#[derive(Default, NwgUi)] +pub struct SystemTray { + #[nwg_control] + window: nwg::MessageWindow, + + #[nwg_resource(source_bin: Some(TRAY_ICON))] + icon: nwg::Icon, + + #[nwg_control(icon: Some(&data.icon), tip: Some("Polaris"))] + #[nwg_events(MousePressLeftUp: [SystemTray::show_menu], OnContextMenu: [SystemTray::show_menu])] + tray: nwg::TrayNotification, + + #[nwg_control(parent: window, popup: true)] + tray_menu: nwg::Menu, + + #[nwg_control(parent: tray_menu, text: "Quit Polaris")] + #[nwg_events(OnMenuItemSelected: [SystemTray::exit])] + exit_menu_item: nwg::MenuItem, } -impl<'a> ToWin for &'a str { - type Out = Vec; - - fn to_win(&self) -> Self::Out { - OsStr::new(self) - .encode_wide() - .chain(std::iter::once(0)) - .collect() - } -} - -impl ToWin for uuid::Uuid { - type Out = winapi::shared::guiddef::GUID; - - fn to_win(&self) -> Self::Out { - let bytes = self.as_bytes(); - let end = [ - bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], - ]; - - winapi::shared::guiddef::GUID { - Data1: ((bytes[0] as u32) << 24 - | (bytes[1] as u32) << 16 - | (bytes[2] as u32) << 8 - | (bytes[3] as u32)), - Data2: ((bytes[4] as u16) << 8 | (bytes[5] as u16)), - Data3: ((bytes[6] as u16) << 8 | (bytes[7] as u16)), - Data4: end, - } - } -} - -pub trait Constructible { - type Out; - fn new() -> Self::Out; -} - -impl Constructible for shellapi::NOTIFYICONDATAW { - type Out = shellapi::NOTIFYICONDATAW; - - fn new() -> Self::Out { - let mut version_union: shellapi::NOTIFYICONDATAW_u = unsafe { std::mem::zeroed() }; - unsafe { - let version = version_union.uVersion_mut(); - *version = shellapi::NOTIFYICON_VERSION_4; - } - - shellapi::NOTIFYICONDATAW { - cbSize: std::mem::size_of::() as u32, - hWnd: std::ptr::null_mut(), - uFlags: 0, - guidItem: uuid::Uuid::nil().to_win(), - hIcon: std::ptr::null_mut(), - uID: 0, - uCallbackMessage: 0, - szTip: [0; 128], - dwState: 0, - dwStateMask: 0, - szInfo: [0; 256], - u: version_union, - szInfoTitle: [0; 64], - dwInfoFlags: 0, - hBalloonIcon: std::ptr::null_mut(), - } - } -} - -fn create_window() -> Option { - let class_name = "Polaris-class".to_win(); - let window_name = "Polaris-window".to_win(); - - unsafe { - let module_handle = winapi::um::libloaderapi::GetModuleHandleW(std::ptr::null()); - let wnd = winuser::WNDCLASSW { - style: 0, - lpfnWndProc: Some(window_proc), - hInstance: module_handle, - hIcon: std::ptr::null_mut(), - hCursor: std::ptr::null_mut(), - lpszClassName: class_name.as_ptr(), - hbrBackground: winuser::COLOR_WINDOW as winapi::shared::windef::HBRUSH, - lpszMenuName: std::ptr::null_mut(), - cbClsExtra: 0, - cbWndExtra: 0, - }; - - let atom = winuser::RegisterClassW(&wnd); - if atom == 0 { - return None; - } - - let window_handle = winuser::CreateWindowExW( - 0, - atom as winapi::shared::ntdef::LPCWSTR, - window_name.as_ptr(), - winuser::WS_DISABLED, - 0, - 0, - 0, - 0, - winuser::GetDesktopWindow(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ); - - if window_handle.is_null() { - return None; - } - - return Some(window_handle); - } -} - -fn add_notification_icon(window: HWND) { - let mut tooltip = [0 as winapi::um::winnt::WCHAR; 128]; - for (&x, p) in "Polaris".to_win().iter().zip(tooltip.iter_mut()) { - *p = x; +impl SystemTray { + fn show_menu(&self) { + let (x, y) = nwg::GlobalCursor::position(); + self.tray_menu.popup(x, y); } - unsafe { - let module = winapi::um::libloaderapi::GetModuleHandleW(std::ptr::null()); - let icon = winuser::LoadIconW(module, std::mem::transmute(IDI_POLARIS_TRAY)); - let mut flags = shellapi::NIF_MESSAGE | shellapi::NIF_TIP; - if !icon.is_null() { - flags |= shellapi::NIF_ICON; - } - - let mut icon_data = shellapi::NOTIFYICONDATAW::new(); - icon_data.hWnd = window; - icon_data.uID = UID_NOTIFICATION_ICON; - icon_data.uFlags = flags; - icon_data.hIcon = icon; - icon_data.uCallbackMessage = MESSAGE_NOTIFICATION_ICON; - icon_data.szTip = tooltip; - - shellapi::Shell_NotifyIconW(shellapi::NIM_ADD, &mut icon_data); - } -} - -fn remove_notification_icon(window: HWND) { - let mut icon_data = shellapi::NOTIFYICONDATAW::new(); - icon_data.hWnd = window; - icon_data.uID = UID_NOTIFICATION_ICON; - unsafe { - shellapi::Shell_NotifyIconW(shellapi::NIM_DELETE, &mut icon_data); - } -} - -fn open_notification_context_menu(window: HWND) { - info!("Opening notification icon context menu"); - let quit_string = "Quit Polaris".to_win(); - - unsafe { - let context_menu = winuser::CreatePopupMenu(); - if context_menu.is_null() { - return; - } - winuser::InsertMenuW( - context_menu, - 0, - winuser::MF_STRING, - MESSAGE_NOTIFICATION_ICON_QUIT as usize, - quit_string.as_ptr(), - ); - - let mut cursor_position = winapi::shared::windef::POINT { x: 0, y: 0 }; - winuser::GetCursorPos(&mut cursor_position); - - winuser::SetForegroundWindow(window); - let flags = winuser::TPM_RIGHTALIGN | winuser::TPM_BOTTOMALIGN | winuser::TPM_RIGHTBUTTON; - winuser::TrackPopupMenu( - context_menu, - flags, - cursor_position.x, - cursor_position.y, - 0, - window, - std::ptr::null_mut(), - ); - winuser::PostMessageW(window, 0, 0, 0); - - info!("Closing notification context menu"); - winuser::DestroyMenu(context_menu); - } -} - -fn quit(window: HWND) { - info!("Shutting down UI"); - unsafe { - winuser::PostMessageW(window, winuser::WM_CLOSE, 0, 0); + fn exit(&self) { + nwg::stop_thread_dispatch(); } } pub fn run() { - info!("Starting up UI (Windows)"); - - create_window().expect("Could not initialize window"); - - let mut message = winuser::MSG { - hwnd: std::ptr::null_mut(), - message: 0, - wParam: 0, - lParam: 0, - time: 0, - pt: winapi::shared::windef::POINT { x: 0, y: 0 }, - }; - - loop { - let status: i32; - unsafe { - status = winuser::GetMessageW(&mut message, std::ptr::null_mut(), 0, 0); - if status == -1 { - panic!( - "GetMessageW error: {}", - winapi::um::errhandlingapi::GetLastError() - ); - } - if status == 0 { - break; - } - winuser::TranslateMessage(&message); - winuser::DispatchMessageW(&message); - } - } -} - -pub unsafe extern "system" fn window_proc( - window: HWND, - msg: UINT, - w_param: WPARAM, - l_param: LPARAM, -) -> LRESULT { - match msg { - winuser::WM_CREATE => { - add_notification_icon(window); - } - - MESSAGE_NOTIFICATION_ICON => match LOWORD(l_param as DWORD) as u32 { - winuser::WM_RBUTTONUP => { - open_notification_context_menu(window); - } - _ => (), - }, - - winuser::WM_COMMAND => match LOWORD(w_param as DWORD) as u32 { - MESSAGE_NOTIFICATION_ICON_QUIT => { - quit(window); - } - _ => (), - }, - - winuser::WM_DESTROY => { - remove_notification_icon(window); - winuser::PostQuitMessage(0); - } - - _ => (), - }; - - return winuser::DefWindowProcW(window, msg, w_param, l_param); + info!("Starting up UI (Windows system tray)"); + nwg::init().expect("Failed to init Native Windows GUI"); + let _ui = SystemTray::build_ui(Default::default()).expect("Failed to build tray UI"); + nwg::dispatch_thread_events(); }