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
This commit is contained in:
Antoine Gersant 2020-12-30 21:41:57 -08:00 committed by GitHub
parent 7edcc38483
commit 4ad8d922f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 382 additions and 666 deletions

1
.gitignore vendored
View file

@ -9,6 +9,7 @@ TestConfig.toml
# Runtime artifacts
*.sqlite
polaris.log
/thumbnails
# Release process artifacts (usually runs on CI)

70
Cargo.lock generated
View file

@ -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"

View file

@ -3,11 +3,12 @@ name = "polaris"
version = "0.0.0"
authors = ["Antoine Gersant <antoine.gersant@lesforges.org>"]
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"

9
build.rs Normal file
View file

@ -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() {}

View file

@ -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

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View file

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View file

@ -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"

View file

@ -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<Self> {
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,
})
}
}

View file

@ -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<PathBuf> {
match self.retrieve_thumbnail(image_path, thumbnailoptions) {
Some(path) => Ok(path),

View file

@ -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<ConnectionManager<SqliteConnection>>,
location: PathBuf,
}
#[derive(Debug)]
@ -42,22 +41,16 @@ impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
impl DB {
pub fn new(path: &Path) -> Result<DB> {
std::fs::create_dir_all(&path.parent().unwrap())?;
let manager = ConnectionManager::<SqliteConnection>::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<PooledConnection<ConnectionManager<SqliteConnection>>> {
self.pool.get().map_err(Error::new)
}

View file

@ -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<std::path::PathBuf>,
log_file_path: &Option<std::path::PathBuf>,
) -> 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();

View file

@ -7,6 +7,7 @@ pub struct CLIOptions {
#[cfg(unix)]
pub foreground: bool,
pub log_file_path: Option<PathBuf>,
#[cfg(unix)]
pub pid_file_path: Option<PathBuf>,
pub config_file_path: Option<PathBuf>,
pub database_file_path: Option<PathBuf>,
@ -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),

106
src/paths.rs Normal file
View file

@ -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<PathBuf>,
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::<PathBuf>());
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;
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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::<PathBuf>());
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<u16>,
config_file_path: Option<PathBuf>,
database_file_path: Option<PathBuf>,
web_dir_path: Option<PathBuf>,
swagger_dir_path: Option<PathBuf>,
cache_dir_path: Option<PathBuf>,
}
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<Context> {
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
}
}

View file

@ -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<u16>;
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::<shellapi::NOTIFYICONDATAW>() 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<HWND> {
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();
}