From a4f118d9e69862745365f0fa13cd50f15767d5b2 Mon Sep 17 00:00:00 2001 From: Antoine Gersant Date: Sat, 17 Sep 2016 03:05:31 -0700 Subject: [PATCH] Setup Windows UI with inert notification icon --- .gitignore | 3 +- Cargo.lock | 55 +++++++++++++++ Cargo.toml | 11 ++- build_release_windows.ps1 | 17 +++++ res/application.manifest | 15 ++++ res/application.rc | 5 ++ src/main.rs | 55 +++++++++------ src/ui/headless.rs | 9 +++ src/ui/mod.rs | 11 +++ src/ui/windows.rs | 144 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 302 insertions(+), 23 deletions(-) create mode 100644 build_release_windows.ps1 create mode 100644 res/application.manifest create mode 100644 res/application.rc create mode 100644 src/ui/headless.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/windows.rs diff --git a/.gitignore b/.gitignore index 6dd8b75..71a1362 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target release -*.dll \ No newline at end of file +*.dll +*.res \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f30a242..eb61873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,15 +4,20 @@ version = "0.1.0" dependencies = [ "id3 0.1.10 (git+https://github.com/jameshurst/rust-id3)", "iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (git+https://github.com/retep998/winapi-rs)", "mount 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "oven 1.0.0 (git+https://github.com/agersant/oven?branch=remove_cookie_dep)", "params 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)", "router 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "shell32-sys 0.1.1 (git+https://github.com/retep998/winapi-rs)", "staticfile 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "user32-sys 0.2.0 (git+https://github.com/retep998/winapi-rs)", + "uuid 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (git+https://github.com/retep998/winapi-rs)", ] [[package]] @@ -274,6 +279,15 @@ name = "itoa" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "git+https://github.com/retep998/winapi-rs#77588710a683e1a4ec085b0f3d6c8939ecc7f030" +dependencies = [ + "winapi 0.2.8 (git+https://github.com/retep998/winapi-rs)", + "winapi-build 0.1.1 (git+https://github.com/retep998/winapi-rs)", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -671,6 +685,20 @@ dependencies = [ "serde 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "sha1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "shell32-sys" +version = "0.1.1" +source = "git+https://github.com/retep998/winapi-rs#77588710a683e1a4ec085b0f3d6c8939ecc7f030" +dependencies = [ + "winapi 0.2.8 (git+https://github.com/retep998/winapi-rs)", + "winapi-build 0.1.1 (git+https://github.com/retep998/winapi-rs)", +] + [[package]] name = "solicit" version = "0.4.4" @@ -819,6 +847,15 @@ dependencies = [ "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "user32-sys" +version = "0.2.0" +source = "git+https://github.com/retep998/winapi-rs#77588710a683e1a4ec085b0f3d6c8939ecc7f030" +dependencies = [ + "winapi 0.2.8 (git+https://github.com/retep998/winapi-rs)", + "winapi-build 0.1.1 (git+https://github.com/retep998/winapi-rs)", +] + [[package]] name = "user32-sys" version = "0.2.0" @@ -841,11 +878,29 @@ dependencies = [ "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "uuid" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "git+https://github.com/retep998/winapi-rs#77588710a683e1a4ec085b0f3d6c8939ecc7f030" + [[package]] name = "winapi" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "git+https://github.com/retep998/winapi-rs#77588710a683e1a4ec085b0f3d6c8939ecc7f030" + [[package]] name = "winapi-build" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 9104212..98e88dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ name = "polaris" version = "0.1.0" authors = ["Antoine Gersant "] +[features] +ui = [] + [dependencies] id3 = { git = "https://github.com/jameshurst/rust-id3" } iron = "0.4.0" @@ -14,4 +17,10 @@ router = "0.4.0" rustc-serialize = "0.3" staticfile = "0.3.0" toml = "0.2" -url = "1.2.0" \ No newline at end of file +url = "1.2.0" + +winapi = { git = "https://github.com/retep998/winapi-rs" } +kernel32-sys = { git = "https://github.com/retep998/winapi-rs" } +shell32-sys = { git = "https://github.com/retep998/winapi-rs" } +user32-sys = { git = "https://github.com/retep998/winapi-rs" } +uuid = { version = "0.3.0", features = ["v5"] } \ No newline at end of file diff --git a/build_release_windows.ps1 b/build_release_windows.ps1 new file mode 100644 index 0000000..b0b7755 --- /dev/null +++ b/build_release_windows.ps1 @@ -0,0 +1,17 @@ +"Compiling resource file" +RC /fo res\application.res res\application.rc + +"Compiling executable" +cargo rustc --release --features "ui" -- -C link-args="/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup res\application.res" + +"Creating output directory" +New-Item .\release\windows -type directory -Force +Remove-Item -Recurse .\release\windows\* + +"Copying to output directory" +Copy-Item .\target\release\polaris.exe .\release\windows\ +Copy-Item .\res\libeay32.dll .\release\windows\ +Copy-Item .\res\libeay32md.dll .\release\windows\ +Copy-Item .\web\ .\release\windows\ -recurse + +Read-Host -Prompt "All clear! Press Enter to exit" diff --git a/res/application.manifest b/res/application.manifest new file mode 100644 index 0000000..283a35d --- /dev/null +++ b/res/application.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + + true + + + \ No newline at end of file diff --git a/res/application.rc b/res/application.rc new file mode 100644 index 0000000..89d5d1b --- /dev/null +++ b/res/application.rc @@ -0,0 +1,5 @@ +#define IDI_POLARIS 0x101 + +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "application.manifest" + +IDI_POLARIS ICON "icon_polaris_32.ico" diff --git a/src/main.rs b/src/main.rs index 75d9c24..2c11b2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,17 @@ extern crate staticfile; extern crate toml; extern crate url; +#[cfg(windows)] +extern crate uuid; +#[cfg(windows)] +extern crate winapi; +#[cfg(windows)] +extern crate kernel32; +#[cfg(windows)] +extern crate shell32; +#[cfg(windows)] +extern crate user32; + use std::path::Path; use std::sync::Arc; use std::sync::Mutex; @@ -21,33 +32,35 @@ use staticfile::Static; mod api; mod collection; mod error; +mod ui; mod vfs; -use api::*; -use collection::*; - fn main() { - - let mut api_chain; - { - let api_handler; + println!("Spawning server thread"); + std::thread::spawn(move || { + let mut api_chain; { - let mut collection = Collection::new(); - collection.load_config(Path::new("Polaris.toml")).unwrap(); - let collection = Arc::new(Mutex::new(collection)); - api_handler = get_api_handler(collection); + let api_handler; + { + let mut collection = collection::Collection::new(); + collection.load_config(Path::new("Polaris.toml")).unwrap(); + let collection = Arc::new(Mutex::new(collection)); + api_handler = api::get_api_handler(collection); + } + api_chain = Chain::new(api_handler); + + let auth_secret = std::env::var("POLARIS_SECRET") + .expect("Environment variable POLARIS_SECRET must be set"); + let cookie_middleware = oven::new(auth_secret.into_bytes()); + api_chain.link(cookie_middleware); } - api_chain = Chain::new(api_handler); - - let auth_secret = std::env::var("POLARIS_SECRET").expect("Environment variable POLARIS_SECRET must be set"); - let cookie_middleware = oven::new(auth_secret.into_bytes()); - api_chain.link(cookie_middleware); - } - let mut mount = Mount::new(); - mount.mount("/api/", api_chain); - mount.mount("/", Static::new(Path::new("web"))); + let mut mount = Mount::new(); + mount.mount("/api/", api_chain); + mount.mount("/", Static::new(Path::new("web"))); + Iron::new(mount).http("localhost:3000").unwrap(); + }); - Iron::new(mount).http("localhost:3000").unwrap(); + ui::run(); } diff --git a/src/ui/headless.rs b/src/ui/headless.rs new file mode 100644 index 0000000..1e5606f --- /dev/null +++ b/src/ui/headless.rs @@ -0,0 +1,9 @@ +use std::time; +use std::thread; + +pub fn run() { + println!("Starting up UI (headless)"); + loop { + thread::sleep(time::Duration::from_secs(10)); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..e815739 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,11 @@ +#[cfg(all(windows, feature = "ui"))] +mod windows; + +#[cfg(all(windows, feature = "ui"))] +pub use self::windows::*; + +#[cfg(not(all(windows, feature = "ui")))] +mod headless; + +#[cfg(not(all(windows, feature = "ui")))] +pub use self::headless::*; diff --git a/src/ui/windows.rs b/src/ui/windows.rs new file mode 100644 index 0000000..f325209 --- /dev/null +++ b/src/ui/windows.rs @@ -0,0 +1,144 @@ +use kernel32; +use shell32; +use std; +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; +use std::thread; +use std::time; +use user32; +use uuid; +use winapi; + +const IDI_POLARIS: isize = 0x101; + +pub trait ToWin { + type Out; + fn to_win(&self) -> Self::Out; +} + +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::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::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, + } + } +} + +fn create_window() -> Option { + + let class_name = "Polaris-class".to_win(); + let window_name = "Polaris-window".to_win(); + + unsafe { + let module_handle = kernel32::GetModuleHandleW(std::ptr::null()); + let wnd = winapi::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: winapi::COLOR_WINDOW as winapi::HBRUSH, + lpszMenuName: std::ptr::null_mut(), + cbClsExtra: 0, + cbWndExtra: 0, + }; + + let atom = user32::RegisterClassW(&wnd); + if atom == 0 { + return None; + } + + let window_handle = user32::CreateWindowExW(0, + atom as winapi::LPCWSTR, + window_name.as_ptr(), + winapi::WS_DISABLED, + 0, + 0, + 0, + 0, + user32::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_handle: winapi::HWND, id: winapi::GUID) { + unsafe { + let module = kernel32::GetModuleHandleW(std::ptr::null()); + let icon = user32::LoadIconW(module, std::mem::transmute(IDI_POLARIS)); + assert!(!icon.is_null()); + + let mut icon_data = winapi::NOTIFYICONDATAW { + cbSize: std::mem::size_of::() as u32, + hWnd: window_handle, + uFlags: winapi::NIF_ICON | winapi::NIF_GUID, + guidItem: id, + hIcon: icon, + uID: 0, + uCallbackMessage: 0, + szTip: [0; 128], + dwState: 0, + dwStateMask: 0, + szInfo: [0; 256], + uTimeout: 0, + szInfoTitle: [0; 64], + dwInfoFlags: 0, + hBalloonIcon: std::ptr::null_mut(), + }; + + shell32::Shell_NotifyIconW(winapi::NIM_ADD, &mut icon_data); + } +} + +pub fn run() { + println!("Starting up UI (Windows)"); + + let path = std::env::current_exe().expect("Could not get current path"); + let guid = uuid::Uuid::new_v5(&uuid::NAMESPACE_OID, path.to_str().unwrap()).to_win(); + let window_handle = create_window().expect("Could not initialize window"); + add_notification_icon(window_handle, guid); + + loop { + thread::sleep(time::Duration::from_secs(10)); + } +} + +pub unsafe extern "system" fn window_proc(h_wnd: winapi::HWND, + msg: winapi::UINT, + w_param: winapi::WPARAM, + l_param: winapi::LPARAM) + -> winapi::LRESULT { + if msg == winapi::winuser::WM_DESTROY { + user32::PostQuitMessage(0); + } + return user32::DefWindowProcW(h_wnd, msg, w_param, l_param); +}