using System.Collections.Concurrent; using System.ComponentModel.Composition; using System.Diagnostics; using System.Diagnostics.Contracts; using System.IO; using System.Net; using System.Runtime.InteropServices; using System.ServiceModel; using System.ServiceModel.Description; using System.Threading.Tasks; using System.Windows; using Caliburn.Micro; using Hardcodet.Wpf.TaskbarNotification; using Pithos.Client.WPF.Configuration; using Pithos.Client.WPF.Properties; using Pithos.Client.WPF.SelectiveSynch; using Pithos.Core; using Pithos.Interfaces; using System; using System.Collections.Generic; using System.Linq; using System.Text; using Pithos.Network; using StatusService = Pithos.Client.WPF.Services.StatusService; namespace Pithos.Client.WPF { using System.ComponentModel.Composition; [Export(typeof(IShell))] public class ShellViewModel : ViewAware, IStatusNotification, IShell, IHandle, IHandle { private IStatusChecker _statusChecker; private IEventAggregator _events; public PithosSettings Settings { get; private set; } public IScreen Parent { get; set; } private Dictionary _monitors = new Dictionary(); public Dictionary Monitors { get { return _monitors; } } private ServiceHost _statusService { get; set; } private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos"); [ImportingConstructor] public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings) { try { _windowManager = windowManager; OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder); _statusChecker = statusChecker; _events = events; _events.Subscribe(this); Settings = settings; StatusMessage = "In Synch"; foreach (var account in settings.Accounts) { MonitorAccount(account); } StartStatusService(); } catch (Exception exc) { Log.Error("Error while starting the ShellViewModel",exc); throw; } } public void MonitorAccount(AccountSettings account) { Task.Factory.StartNew(() => { PithosMonitor monitor = null; var accountName = account.AccountName; if (_monitors.TryGetValue(accountName, out monitor)) { //If the account is active if (account.IsActive) //Start the monitor. It's OK to start an already started monitor, //it will just ignore the call monitor.Start(); else { //If the account is inactive //Stop and remove the monitor RemoveMonitor(accountName); } return; } //PithosMonitor uses MEF so we need to resolve it monitor = new PithosMonitor { UserName = accountName, ApiKey = account.ApiKey, UsePithos = account.UsePithos, StatusNotification = this, RootPath = account.RootPath }; IoC.BuildUp(monitor); var appSettings = Properties.Settings.Default; monitor.AuthenticationUrl = account.UsePithos ? appSettings.PithosAuthenticationUrl : appSettings.CloudfilesAuthenticationUrl; _monitors[accountName] = monitor; if (account.IsActive) { //Don't start a monitor if it doesn't have an account and ApiKey if (String.IsNullOrWhiteSpace(monitor.UserName) || String.IsNullOrWhiteSpace(monitor.ApiKey)) return; StartMonitor(monitor); } }); } protected override void OnViewLoaded(object view) { var window = (Window)view; window.Hide(); UpdateStatus(); base.OnViewLoaded(view); } #region Status Properties private string _statusMessage; public string StatusMessage { get { return _statusMessage; } set { _statusMessage = value; NotifyOfPropertyChange(() => StatusMessage); } } private ObservableConcurrentCollection _accounts = new ObservableConcurrentCollection(); public ObservableConcurrentCollection Accounts { get { return _accounts; } } private string _pauseSyncCaption="Pause Syncing"; public string PauseSyncCaption { get { return _pauseSyncCaption; } set { _pauseSyncCaption = value; NotifyOfPropertyChange(() => PauseSyncCaption); } } private readonly ObservableConcurrentCollection _recentFiles = new ObservableConcurrentCollection(); public ObservableConcurrentCollection RecentFiles { get { return _recentFiles; } } private string _statusIcon="Images/Tray.ico"; public string StatusIcon { get { return _statusIcon; } set { _statusIcon = value; NotifyOfPropertyChange(() => StatusIcon); } } #endregion #region Commands public void ShowPreferences() { Settings.Reload(); var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings); _windowManager.ShowDialog(preferences); } public PithosCommand OpenPithosFolderCommand { get; private set; } public void OpenPithosFolder() { var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive); if (account == null) return; Process.Start(account.RootPath); } public void OpenPithosFolder(AccountInfo account) { Process.Start(account.AccountPath); } public void GoToSite() { } public void GoToSite(AccountInfo account) { var site = String.Format("{0}/ui/?token={1}&user={2}", Properties.Settings.Default.PithosSite,account.Token, account.UserName); Process.Start(site); } public void ToggleSynching() { bool isPaused=false; foreach (var pair in Monitors) { var monitor = pair.Value; monitor.Pause = !monitor.Pause; isPaused = monitor.Pause; } PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing"; var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch"; StatusIcon = String.Format(@"Images/{0}.ico", iconKey); } public void ExitPithos() { foreach (var pair in Monitors) { var monitor = pair.Value; monitor.Stop(); } ((Window)GetView()).Close(); } #endregion private Dictionary iconNames = new List { new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"), new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"), new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused") }.ToDictionary(s => s.Status); readonly IWindowManager _windowManager; public void UpdateStatus() { var pithosStatus = _statusChecker.GetPithosStatus(); if (iconNames.ContainsKey(pithosStatus)) { var info = iconNames[pithosStatus]; StatusIcon = String.Format(@"Images/{0}.ico", info.IconName); StatusMessage = String.Format("Pithos 1.0\r\n{0}", info.StatusText); } _events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info}); } private Task StartMonitor(PithosMonitor monitor,int retries=0) { return Task.Factory.StartNew(() => { using (log4net.ThreadContext.Stacks["Monitor"].Push("Start")) { try { Log.InfoFormat("Start Monitoring {0}", monitor.UserName); monitor.Start(); } catch (WebException exc) { if (AbandonRetry(monitor, retries)) return; if (IsUnauthorized(exc)) { var message = String.Format("API Key Expired for {0}. Starting Renewal",monitor.UserName); Log.Error(message,exc); TryAuthorize(monitor,retries); } else { TryLater(monitor, exc,retries); } } catch (Exception exc) { if (AbandonRetry(monitor, retries)) return; TryLater(monitor,exc,retries); } } }); } private bool AbandonRetry(PithosMonitor monitor, int retries) { if (retries > 1) { var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry", monitor.UserName); _events.Publish(new Notification {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error}); return true; } return false; } private Task TryAuthorize(PithosMonitor monitor,int retries) { _events.Publish(new Notification { Title = "Authorization failed", Message = "Your API Key has probably expired. You will be directed to a page where you can renew it", Level = TraceLevel.Error }); var authorize= PithosAccount.RetrieveCredentialsAsync(Settings.PithosSite); return authorize.ContinueWith(t => { if (t.IsFaulted) { string message = String.Format("API Key retrieval for {0} failed", monitor.UserName); Log.Error(message,t.Exception.InnerException); _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error }); return; } var credentials = t.Result; var account =Settings.Accounts.FirstOrDefault(act => act.AccountName == credentials.UserName); account.ApiKey = credentials.Password; monitor.ApiKey = credentials.Password; Settings.Save(); Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1)); }); } private static bool IsUnauthorized(WebException exc) { if (exc==null) throw new ArgumentNullException("exc"); Contract.EndContractBlock(); var response = exc.Response as HttpWebResponse; if (response == null) return false; return (response.StatusCode == HttpStatusCode.Unauthorized); } private void TryLater(PithosMonitor monitor, Exception exc,int retries) { var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds"); Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1)); _events.Publish(new Notification {Title = "Error", Message = message, Level = TraceLevel.Error}); Log.Error(message, exc); } public void NotifyChange(string status, TraceLevel level=TraceLevel.Info) { this.StatusMessage = status; _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level }); } public void NotifyChangedFile(string filePath) { var entry = new FileEntry {FullPath=filePath}; IProducerConsumerCollection files=this.RecentFiles; FileEntry popped; while (files.Count > 5) files.TryTake(out popped); files.TryAdd(entry); } public void NotifyAccount(AccountInfo account) { if (account== null) return; account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}", Properties.Settings.Default.PithosSite, account.Token, account.UserName); IProducerConsumerCollection accounts = Accounts; for (var i = 0; i < _accounts.Count; i++) { AccountInfo item; if (accounts.TryTake(out item)) { if (item.UserName!=account.UserName) { accounts.TryAdd(item); } } } accounts.TryAdd(account); } public void RemoveMonitor(string accountName) { if (String.IsNullOrWhiteSpace(accountName)) return; PithosMonitor monitor; if (Monitors.TryGetValue(accountName, out monitor)) { Monitors.Remove(accountName); monitor.Stop(); } } public void RefreshOverlays() { foreach (var pair in Monitors) { var monitor = pair.Value; var path = monitor.RootPath; if (String.IsNullOrWhiteSpace(path)) continue; if (!Directory.Exists(path) && !File.Exists(path)) continue; IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path); try { NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM, HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT, pathPointer, IntPtr.Zero); } finally { Marshal.FreeHGlobal(pathPointer); } } } private void StartStatusService() { // Create a ServiceHost for the CalculatorService type and provide the base address. var baseAddress = new Uri("net.pipe://localhost/pithos"); _statusService = new ServiceHost(typeof(StatusService), baseAddress); var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None); _statusService.AddServiceEndpoint(typeof(IStatusService), binding, "net.pipe://localhost/pithos/statuscache"); _statusService.AddServiceEndpoint(typeof(ISettingsService), binding, "net.pipe://localhost/pithos/settings"); //// Add a mex endpoint var smb = new ServiceMetadataBehavior { HttpGetEnabled = true, HttpGetUrl = new Uri("http://localhost:30000/pithos/mex") }; _statusService.Description.Behaviors.Add(smb); _statusService.Open(); } private void StopStatusService() { if (_statusService == null) return; if (_statusService.State == CommunicationState.Faulted) _statusService.Abort(); else if (_statusService.State != CommunicationState.Closed) _statusService.Close(); _statusService = null; } #region Event Handlers public void Handle(SelectiveSynchChanges message) { var accountName = message.Account.AccountName; PithosMonitor monitor; if (_monitors.TryGetValue(accountName, out monitor)) { monitor.AddSelectivePaths(message.Added); monitor.RemoveSelectivePaths(message.Removed); } } public void Handle(Notification notification) { if (!Settings.ShowDesktopNotifications) return; BalloonIcon icon = BalloonIcon.None; switch (notification.Level) { case TraceLevel.Error: icon = BalloonIcon.Error; break; case TraceLevel.Info: case TraceLevel.Verbose: icon = BalloonIcon.Info; break; case TraceLevel.Warning: icon = BalloonIcon.Warning; break; default: icon = BalloonIcon.None; break; } if (Settings.ShowDesktopNotifications) { var tv = (ShellView) this.GetView(); tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon); } } #endregion } }