summary history branches tags files
commit:e3f794c7ac5fa72e4fd5134fae740fce4d546719
author:Trevor Bentley
committer:Trevor Bentley
date:Sun May 7 19:09:40 2017 +0200
parents:11212f4a3b9037a8a5efd9041a9db71ae10d6d7f
Fix OS X memory leaks, extra error handling, launch Wine if it exists (not added to repo yet).
diff --git a/Cargo.toml b/Cargo.toml
line changes: +1/-0
index 056d364..680a185
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,6 +40,7 @@ chrono = "0.3.0"
 libc = "0.2"
 log = "0.3.7"
 log4rs = "0.6.3"
+ctrlc = "3.0.1"
 
 [target."cfg(windows)".dependencies]
 

diff --git a/src/lib.rs b/src/lib.rs
line changes: +43/-0
index 9e2da61..8bb66c3
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -38,8 +38,13 @@ pub mod spotify_api {
 
 #[cfg(target_os = "macos")]
 pub type Object = osx::Object;
+#[cfg(target_os = "macos")]
+pub type StatusBar = osx::OSXStatusBar;
+
 #[cfg(not(target_os = "macos"))]
 pub type Object = u64;
+#[cfg(not(target_os = "macos"))]
+pub type StatusBar = DummyStatusBar;
 
 pub type MenuItem = *mut Object;
 pub trait TStatusBar {
@@ -75,3 +80,41 @@ impl TStatusBar for DummyStatusBar {
     fn set_tooltip(&self, _: &str) {}
     fn run(&mut self, _: bool) {}
 }
+
+pub fn search_paths() -> Vec<String> {
+    use std::collections::BTreeSet;
+    //let mut v = Vec::<String>::new();
+    let mut v = BTreeSet::<String>::new();
+
+    // $HOME
+    if let Some(dir) = std::env::home_dir() {
+        v.insert(dir.display().to_string());
+    }
+
+    #[cfg(not(target_os = "macos"))]
+    let bundle: Option<String> = None;
+    #[cfg(target_os = "macos")]
+    let bundle = osx::resource_dir();
+
+    // OS bundle/resource dir
+    if let Some(dir) = bundle {
+        v.insert(dir);
+    }
+
+    // $CWD
+    if let Ok(dir) = std::env::current_dir() {
+        v.insert(dir.display().to_string());
+    }
+
+    // exe_dir
+    if let Ok(mut dir) = std::env::current_exe() {
+        dir.pop(); // remove the actual executable
+        v.insert(dir.display().to_string());
+    }
+
+    let mut list: Vec<String> = Vec::new();
+    for dir in v {
+        list.push(dir);
+    }
+    list
+}

diff --git a/src/main.rs b/src/main.rs
line changes: +67/-12
index de3a66b..07932db
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,6 +4,10 @@ use connectr::TStatusBar;
 use connectr::MenuItem;
 use connectr::NSCallback;
 
+extern crate ctrlc;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+
 #[macro_use]
 extern crate log;
 extern crate log4rs;
@@ -19,6 +23,8 @@ extern crate time;
 extern crate rustc_serialize;
 use rustc_serialize::json;
 
+use std::process;
+
 // How often to refresh Spotify state (if nothing triggers a refresh earlier).
 pub const REFRESH_PERIOD: i64 = 30;
 
@@ -39,9 +45,6 @@ struct MenuCallbackCommand {
     data: String,
 }
 
-#[cfg(target_os = "macos")]
-use connectr::osx;
-
 struct MenuItems {
     device: Vec<(MenuItem, String)>,
     play: MenuItem,
@@ -64,8 +67,16 @@ fn play_action_label(is_playing: bool) -> &'static str {
 }
 
 fn fill_menu<T: TStatusBar>(app: &mut ConnectrApp, spotify: &mut connectr::SpotifyConnectr, status: &mut T) {
-    app.device_list = Some(spotify.request_device_list());
-    app.player_state = Some(spotify.request_player_state());
+    let dev_list = spotify.request_device_list();
+    let player_state = spotify.request_player_state();
+    match dev_list {
+        Some(_) => { app.device_list = dev_list },
+        None => { return },
+    }
+    match player_state {
+        Some(_) => { app.player_state = player_state },
+        None => { return },
+    }
     let ref device_list = app.device_list.as_ref().unwrap();
     let ref player_state = app.player_state.as_ref().unwrap();
 
@@ -253,7 +264,11 @@ fn handle_callback<T: TStatusBar>(app: &mut ConnectrApp, spotify: &mut connectr:
             require(spotify.transfer(cmd.data.clone(), true));
         },
         CallbackAction::PlayPause => {
-            let player_state = spotify.request_player_state();
+            let fresh_player_state = spotify.request_player_state();
+            let player_state = match fresh_player_state {
+                Some(ref state) => state,
+                None => app.player_state.as_ref().unwrap(),
+            };
             match player_state.is_playing {
                 true => {require(spotify.pause());},
                 false => {require(spotify.play(None));},
@@ -307,14 +322,35 @@ fn refresh_time(app: &mut ConnectrApp, now: i64) -> i64 {
         None => REFRESH_PERIOD,
     };
     let refresh_offset = std::cmp::min(REFRESH_PERIOD, refresh_offset) as i64;
-    println!("State refresh in {} seconds.", refresh_offset);
+    info!("State refresh in {} seconds.", refresh_offset);
     now + refresh_offset
 }
 
+fn find_wine_path() -> Option<std::path::PathBuf> {
+    let search_paths = connectr::search_paths();
+    info!("Search paths: {:?}", search_paths);
+    for search_path in search_paths {
+        let path = std::path::PathBuf::from(search_path).join("wine");
+        if path.exists() && path.is_dir() {
+            return Some(path);
+        }
+    }
+    None
+}
+
 fn main() {
     create_logger();
     info!("Started Connectr");
 
+    let running = Arc::new(AtomicBool::new(true));
+    let r = running.clone();
+    match ctrlc::set_handler(move || {
+        r.store(false, Ordering::SeqCst);
+    }) {
+        Ok(_) => {},
+        Err(_) => { error!("Failed to register Ctrl-C handler."); }
+    }
+
     let mut app = ConnectrApp {
         menu: MenuItems {
             device: Vec::<(MenuItem, String)>::new(),
@@ -334,13 +370,27 @@ fn main() {
     spotify.connect();
     info!("Created Spotify connection.");
     spotify.set_target_device(None);
-    #[cfg(target_os = "macos")]
-    let mut status = osx::OSXStatusBar::new(tx);
+    let mut status = connectr::StatusBar::new(tx);
     info!("Created status bar.");
-    #[cfg(not(target_os = "macos"))]
-    let mut status = connectr::DummyStatusBar::new(tx);
 
-    loop {
+    let mut tiny: Option<process::Child> = None;
+    if let Some(wine_dir) = find_wine_path() {
+        info!("Found wine root: {:?}", wine_dir);
+        let wine_exe = wine_dir.join("wine");
+        let tiny_exe = wine_dir.join("tiny.exe");
+        let config_dir = wine_dir.join("config");
+        debug!("{:?} / {:?} / {:?} / {:?}", wine_dir, wine_exe, config_dir, tiny_exe);
+        tiny = Some(process::Command::new(wine_exe)
+                    .env("WINEPREFIX", config_dir)
+                    .current_dir(wine_dir)
+                    .args(&[tiny_exe])
+                    .spawn().unwrap());
+    }
+    else {
+        warn!("Didn't find Wine in search path.");
+    }
+
+    while running.load(Ordering::SeqCst) {
         let now = time::now_utc().to_timespec().sec as i64;
         if now > refresh_time_utc {
             // Redraw the whole menu once every 60 seconds, or sooner if a
@@ -361,6 +411,11 @@ fn main() {
         status.run(false);
         sleep(Duration::from_millis(10));
     }
+    info!("Exiting.\n");
+    if let Some(mut tiny_proc) = tiny {
+        let _ = tiny_proc.kill();
+        let _ = tiny_proc.wait();
+    }
 }
 
 fn require(response: SpotifyResponse) {

diff --git a/src/osx/mod.rs b/src/osx/mod.rs
line changes: +49/-18
index cfa30f5..b5d75b3
--- a/src/osx/mod.rs
+++ b/src/osx/mod.rs
@@ -49,14 +49,12 @@ impl TStatusBar for OSXStatusBar {
     fn new(tx: Sender<String>) -> OSXStatusBar {
         let mut bar;
         unsafe {
-            let _ = NSAutoreleasePool::new(nil);
             let app = NSApp();
             let status_bar = NSStatusBar::systemStatusBar(nil);
             bar = OSXStatusBar {
                 app: app,
-                //status_bar_item: status_bar.statusItemWithLength_(NSSquareStatusItemLength),
                 status_bar_item: status_bar.statusItemWithLength_(NSVariableStatusItemLength),
-                menu_bar: NSMenu::new(nil).autorelease(),
+                menu_bar: NSMenu::new(nil),
                 object: NSObj::alloc(tx).setup(),
             };
             bar.app.setActivationPolicy_(NSApplicationActivationPolicyAccessory);
@@ -67,10 +65,14 @@ impl TStatusBar for OSXStatusBar {
             };
             let img = NSString::alloc(nil).init_str(&img_path);
             let icon = NSImage::alloc(nil).initWithContentsOfFile_(img);
+            let _ = msg_send![img, release];
             //let icon = NSImage::alloc(nil).initWithContentsOfFile_(img);
             //let icon = NSImage::imageNamed_(img, img);
-            NSButton::setTitle_(bar.status_bar_item, NSString::alloc(nil).init_str("connectr"));
+            let title = NSString::alloc(nil).init_str("connectr");
+            NSButton::setTitle_(bar.status_bar_item, title);
+            let _ = msg_send![title, release];
             bar.status_bar_item.button().setImage_(icon);
+            let _ = msg_send![icon, release];
             bar.status_bar_item.setMenu_(bar.menu_bar);
             bar.object.cb_fn = Some(Box::new(
                 move |s, sender| {
@@ -78,19 +80,25 @@ impl TStatusBar for OSXStatusBar {
                     cb(sender, &s.tx);
                 }
             ));
+            //unsafe { let _: () = msg_send![self.app, finishLaunching]; }
+            let _: () = msg_send![app, finishLaunching];
         }
         bar
     }
     fn clear_items(&mut self) {
         unsafe {
-            self.menu_bar = NSMenu::new(nil).autorelease();
+            let old_menu = self.menu_bar;
+            self.menu_bar = NSMenu::new(nil);
             self.status_bar_item.setMenu_(self.menu_bar);
+            let _ = msg_send![old_menu, removeAllItems];
+            let _ = msg_send![old_menu, release];
         }
     }
     fn set_tooltip(&self, text: &str) {
         unsafe {
             let img = NSString::alloc(nil).init_str(text);
             let _ = msg_send![self.status_bar_item.button(), setToolTip: img];
+            let _ = msg_send![img, release];
         }
     }
     fn add_label(&mut self, label: &str) {
@@ -98,9 +106,11 @@ impl TStatusBar for OSXStatusBar {
             let txt = NSString::alloc(nil).init_str(label);
             let quit_key = NSString::alloc(nil).init_str("");
             let app_menu_item = NSMenuItem::alloc(nil)
-                .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key)
-                .autorelease();
+                .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key);
+            let _ = msg_send![txt, release];
+            let _ = msg_send![quit_key, release];
             self.menu_bar.addItem_(app_menu_item);
+            let _ = msg_send![app_menu_item, release];
         }
     }
     fn add_quit(&mut self, label: &str) {
@@ -108,9 +118,11 @@ impl TStatusBar for OSXStatusBar {
             let txt = NSString::alloc(nil).init_str(label);
             let quit_key = NSString::alloc(nil).init_str("");
             let app_menu_item = NSMenuItem::alloc(nil)
-                .initWithTitle_action_keyEquivalent_(txt, sel!(terminate:), quit_key)
-                .autorelease();
+                .initWithTitle_action_keyEquivalent_(txt, sel!(terminate:), quit_key);
+            let _ = msg_send![txt, release];
+            let _ = msg_send![quit_key, release];
             self.menu_bar.addItem_(app_menu_item);
+            let _ = msg_send![app_menu_item, release];
         }
     }
     fn add_separator(&mut self) {
@@ -125,8 +137,9 @@ impl TStatusBar for OSXStatusBar {
             let txt = NSString::alloc(nil).init_str(item);
             let quit_key = NSString::alloc(nil).init_str("");
             let app_menu_item = NSMenuItem::alloc(nil)
-                .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key)
-                .autorelease();
+                .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key);
+            let _ = msg_send![txt, release];
+            let _ = msg_send![quit_key, release];
             self.object.add_callback(app_menu_item, callback);
             let objc = self.object.take_objc();
             let _: () = msg_send![app_menu_item, setTarget: objc];
@@ -135,6 +148,7 @@ impl TStatusBar for OSXStatusBar {
             }
             let item: *mut Object = app_menu_item;
             self.menu_bar.addItem_(app_menu_item);
+            let _ = msg_send![app_menu_item, release];
             item
         }
     }
@@ -142,6 +156,7 @@ impl TStatusBar for OSXStatusBar {
         unsafe {
             let ns_label = NSString::alloc(nil).init_str(label);
             let _: () = msg_send![item, setTitle: ns_label];
+            let _ = msg_send![ns_label, release];
         }
     }
     fn sel_item(&mut self, sender: u64) {
@@ -157,15 +172,9 @@ impl TStatusBar for OSXStatusBar {
         }
     }
     fn run(&mut self, block: bool) {
-        //unsafe {
-            //self.app.run();
-        //}
-        let _ = unsafe {NSAutoreleasePool::new(nil)};
-        unsafe { let _: () = msg_send![self.app, finishLaunching]; }
         loop {
-            sleep(Duration::from_millis(50));
             unsafe {
-                let _ = NSAutoreleasePool::new(nil);
+                let pool = NSAutoreleasePool::new(nil);
                 let cls = Class::get("NSDate").unwrap();
                 let date: Id<Object> = msg_send![cls, distantPast];
                 let mode = NSString::alloc(nil).init_str("kCFRunLoopDefaultMode");
@@ -173,8 +182,11 @@ impl TStatusBar for OSXStatusBar {
                                                   untilDate: date inMode:mode dequeue: YES];
                 let _ = msg_send![self.app, sendEvent: event];
                 let _ = msg_send![self.app, updateWindows];
+                let _ = msg_send![mode, release];
+                let _ = msg_send![pool, drain];
             }
             if !block { break; }
+            sleep(Duration::from_millis(50));
         }
     }
 }
@@ -189,6 +201,23 @@ pub fn osx_alert(text: &str) {
         let _ = msg_send![alert, setMessageText: ns_text];
         let _ = msg_send![alert, addButtonWithTitle: button];
         let _ = msg_send![alert, runModal];
+        let _ = msg_send![ns_text, release];
+        let _ = msg_send![button, release];
+        let _ = msg_send![alert, release];
+    }
+}
+
+pub fn resource_dir() -> Option<String> {
+    unsafe {
+        let cls = Class::get("NSBundle").unwrap();
+        let bundle: *mut Object = msg_send![cls, mainBundle];
+        let path: *mut Object = msg_send![bundle, resourcePath];
+        let cstr: *const libc::c_char = msg_send![path, UTF8String];
+        if cstr != ptr::null() {
+            let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned();
+            return Some(rstr);
+        }
+        None
     }
 }
 
@@ -199,6 +228,8 @@ pub fn bundled_resource_path(name: &str, extension: &str) -> Option<String> {
         let res = NSString::alloc(nil).init_str(name);
         let ext = NSString::alloc(nil).init_str(extension);
         let ini: *mut Object = msg_send![bundle, pathForResource:res ofType:ext];
+        let _ = msg_send![res, release];
+        let _ = msg_send![ext, release];
         let cstr: *const libc::c_char = msg_send![ini, UTF8String];
         if cstr != ptr::null() {
             let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned();

diff --git a/src/settings/mod.rs b/src/settings/mod.rs
line changes: +1/-1
index 07877cc..7133d14
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -52,13 +52,13 @@ fn inifile() -> String {
         return path;
     }
 
+    // Default to looking in current working directory
     let path = INIFILE.to_string();
     if path::Path::new(&path).exists() {
         info!("Found config: {}", path);
         return path;
     }
 
-    // Default to looking in current working directory
     String::new()
 }
 

diff --git a/src/webapi/mod.rs b/src/webapi/mod.rs
line changes: +29/-12
index b4166a8..9f70e59
--- a/src/webapi/mod.rs
+++ b/src/webapi/mod.rs
@@ -393,12 +393,7 @@ impl SpotifyConnectr {
                                       &refresh_token,
                                       self.expire_utc.unwrap());
     }
-    pub fn connect(&mut self) {
-        if self.access_token.is_some() && !self.is_token_expired() {
-            println!("Reusing saved credentials.");
-            let _ = self.schedule_token_refresh();
-            return ()
-        }
+    pub fn authenticate(&mut self) {
         println!("Requesting fresh credentials.");
         self.auth_code = http::authenticate(&self.settings);
         let (access_token, refresh_token, expires_in) = request_oauth_tokens(&self.auth_code, &self.settings);
@@ -409,6 +404,14 @@ impl SpotifyConnectr {
         self.expire_utc = Some(expire_utc);
         let _ = self.schedule_token_refresh();
     }
+    pub fn connect(&mut self) {
+        if self.access_token.is_some() && !self.is_token_expired() {
+            println!("Reusing saved credentials.");
+            let _ = self.schedule_token_refresh();
+            return ()
+        }
+        self.authenticate()
+    }
     pub fn bearer_token(&self) -> http::AccessToken {
         match self.access_token {
             Some(ref x) => http::AccessToken::Bearer(x),
@@ -433,15 +436,29 @@ impl SpotifyConnectr {
         let (access_token, _, expires_in) = parse_spotify_token(&json_response);
         (access_token, expires_in)
     }
-    pub fn request_device_list(&self) -> ConnectDeviceList {
+    pub fn request_device_list(&mut self) -> Option<ConnectDeviceList> {
         let json_response = http::http(spotify_api::DEVICES, "", "",
-                                       http::HttpMethod::GET, self.bearer_token()).unwrap();
-        json::decode(&json_response).unwrap()
+                                       http::HttpMethod::GET, self.bearer_token());
+        match json_response.code {
+            Some(200) => Some(json::decode(&json_response.data.unwrap()).unwrap()),
+            Some(401) => {
+                self.authenticate();
+                None
+            }
+            _ => None
+        }
     }
-    pub fn request_player_state(&self) -> PlayerState {
+    pub fn request_player_state(&mut self) -> Option<PlayerState> {
         let json_response = http::http(spotify_api::PLAYER_STATE, "", "",
-                                       http::HttpMethod::GET, self.bearer_token()).unwrap();
-        json::decode(&json_response).unwrap()
+                                       http::HttpMethod::GET, self.bearer_token());
+        match json_response.code {
+            Some(200) => Some(json::decode(&json_response.data.unwrap()).unwrap()),
+            Some(401) => {
+                self.authenticate();
+                None
+            }
+            _ => None
+        }
     }
     pub fn set_target_device(&mut self, device: Option<DeviceId>) {
         self.device = device;