1 using System.Collections.Concurrent;
2 using System.ComponentModel;
3 using System.ComponentModel.Composition;
4 using System.Diagnostics;
5 using System.Diagnostics.Contracts;
8 using System.Runtime.InteropServices;
9 using System.ServiceModel;
10 using System.ServiceModel.Description;
11 using System.Threading.Tasks;
14 using Hardcodet.Wpf.TaskbarNotification;
15 using Pithos.Client.WPF.Configuration;
16 using Pithos.Client.WPF.FileProperties;
17 using Pithos.Client.WPF.Properties;
18 using Pithos.Client.WPF.SelectiveSynch;
19 using Pithos.Client.WPF.Services;
20 using Pithos.Client.WPF.Shell;
22 using Pithos.Interfaces;
24 using System.Collections.Generic;
28 using StatusService = Pithos.Client.WPF.Services.StatusService;
30 namespace Pithos.Client.WPF {
31 using System.ComponentModel.Composition;
33 [Export(typeof(IShell))]
35 /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications.
36 /// The shell also hosts the status service called by shell extensions to retrieve file info
39 /// It is a strange "shell" as its main visible element is an icon instead of a window
40 /// The shell subscribes to the following events:
41 /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon
42 /// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here
43 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
45 //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
46 public class ShellViewModel : Screen, IStatusNotification, IShell,
47 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
49 //The Status Checker provides the current synch state
50 //TODO: Could we remove the status checker and use events in its place?
51 private IStatusChecker _statusChecker;
52 private IEventAggregator _events;
54 public PithosSettings Settings { get; private set; }
57 private Dictionary<string, PithosMonitor> _monitors = new Dictionary<string, PithosMonitor>();
59 /// Dictionary of account monitors, keyed by account
62 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
63 /// retrieve account and boject info
65 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
66 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
67 public Dictionary<string, PithosMonitor> Monitors
69 get { return _monitors; }
74 /// The status service is used by Shell extensions to retrieve file status information
76 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
77 private ServiceHost _statusService { get; set; }
79 //Logging in the Pithos client is provided by log4net
80 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
83 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
86 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
88 [ImportingConstructor]
89 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
94 _windowManager = windowManager;
95 //CHECK: Caliburn doesn't need explicit command construction
96 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
97 _statusChecker = statusChecker;
100 _events.Subscribe(this);
104 StatusMessage = "In Synch";
107 catch (Exception exc)
109 Log.Error("Error while starting the ShellViewModel",exc);
114 protected override void OnActivate()
122 private async Task StartMonitoring()
126 foreach (var account in Settings.Accounts)
128 await MonitorAccount(account);
130 _statusService = StatusService.Start();
132 catch (AggregateException exc)
136 Log.Error("Error while starting monitoring", e);
143 protected override void OnDeactivate(bool close)
145 base.OnDeactivate(close);
148 StatusService.Stop(_statusService);
149 _statusService = null;
153 public Task MonitorAccount(AccountSettings account)
155 return Task.Factory.StartNew(() =>
157 PithosMonitor monitor = null;
158 var accountName = account.AccountName;
160 if (_monitors.TryGetValue(accountName, out monitor))
162 //If the account is active
163 if (account.IsActive)
164 //Start the monitor. It's OK to start an already started monitor,
165 //it will just ignore the call
169 //If the account is inactive
170 //Stop and remove the monitor
171 RemoveMonitor(accountName);
176 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
177 monitor = new PithosMonitor
179 UserName = accountName,
180 ApiKey = account.ApiKey,
181 UsePithos = account.UsePithos,
182 StatusNotification = this,
183 RootPath = account.RootPath
185 //PithosMonitor uses MEF so we need to resolve it
186 IoC.BuildUp(monitor);
188 var appSettings = Properties.Settings.Default;
189 monitor.AuthenticationUrl = account.UsePithos
190 ? appSettings.PithosAuthenticationUrl
191 : appSettings.CloudfilesAuthenticationUrl;
193 _monitors[accountName] = monitor;
195 if (account.IsActive)
197 //Don't start a monitor if it doesn't have an account and ApiKey
198 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
199 String.IsNullOrWhiteSpace(monitor.ApiKey))
201 StartMonitor(monitor);
207 protected override void OnViewLoaded(object view)
209 var window = (Window)view;
212 base.OnViewLoaded(view);
216 #region Status Properties
218 private string _statusMessage;
219 public string StatusMessage
221 get { return _statusMessage; }
224 _statusMessage = value;
225 NotifyOfPropertyChange(() => StatusMessage);
229 private ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
230 public ObservableConcurrentCollection<AccountInfo> Accounts
232 get { return _accounts; }
236 private string _pauseSyncCaption="Pause Syncing";
237 public string PauseSyncCaption
239 get { return _pauseSyncCaption; }
242 _pauseSyncCaption = value;
243 NotifyOfPropertyChange(() => PauseSyncCaption);
247 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
248 public ObservableConcurrentCollection<FileEntry> RecentFiles
250 get { return _recentFiles; }
254 private string _statusIcon="../Images/Tray.ico";
255 public string StatusIcon
257 get { return _statusIcon; }
261 NotifyOfPropertyChange(() => StatusIcon);
269 public void ShowPreferences()
272 var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);
273 _windowManager.ShowDialog(preferences);
277 public void AboutPithos()
279 var about = new AboutViewModel();
280 _windowManager.ShowWindow(about);
283 public void SendFeedback()
285 var feedBack = IoC.Get<FeedbackViewModel>();
286 _windowManager.ShowWindow(feedBack);
289 //public PithosCommand OpenPithosFolderCommand { get; private set; }
291 public void OpenPithosFolder()
293 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
296 Process.Start(account.RootPath);
299 public void OpenPithosFolder(AccountInfo account)
301 Process.Start(account.AccountPath);
305 public void GoToSite(AccountInfo account)
307 var site = String.Format("{0}/ui/?token={1}&user={2}",
308 Properties.Settings.Default.PithosSite,account.Token,
313 public void ShowFileProperties()
315 var account = Settings.Accounts.First(acc => acc.IsActive);
316 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
317 var files=dir.GetFiles();
319 var idx=r.Next(0, files.Length);
320 ShowFileProperties(files[idx].FullName);
323 public void ShowFileProperties(string filePath)
325 if (String.IsNullOrWhiteSpace(filePath))
326 throw new ArgumentNullException("filePath");
327 if (!File.Exists(filePath))
328 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
329 Contract.EndContractBlock();
331 var pair=(from monitor in Monitors
332 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
333 select monitor).FirstOrDefault();
334 var account = pair.Key;
335 var accountMonitor = pair.Value;
337 ObjectInfo info = accountMonitor.GetObjectInfo(filePath);
341 var fileProperties = new FilePropertiesViewModel(this, info,filePath);
342 _windowManager.ShowWindow(fileProperties);
345 public void ShowContainerProperties()
347 var account = Settings.Accounts.First(acc => acc.IsActive);
348 var dir = new DirectoryInfo(account.RootPath);
349 var fullName = (from folder in dir.EnumerateDirectories()
350 where (folder.Attributes & FileAttributes.Hidden) == 0
351 select folder.FullName).First();
352 ShowContainerProperties(fullName);
355 public void ShowContainerProperties(string filePath)
357 if (String.IsNullOrWhiteSpace(filePath))
358 throw new ArgumentNullException("filePath");
359 if (!Directory.Exists(filePath))
360 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
361 Contract.EndContractBlock();
363 var pair=(from monitor in Monitors
364 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
365 select monitor).FirstOrDefault();
366 var account = pair.Key;
367 var accountMonitor = pair.Value;
368 ContainerInfo info = accountMonitor.GetContainerInfo(filePath);
372 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
373 _windowManager.ShowWindow(containerProperties);
376 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
378 if (currentInfo==null)
379 throw new ArgumentNullException("currentInfo");
380 Contract.EndContractBlock();
382 var monitor = Monitors[currentInfo.Account];
383 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
387 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
389 if (container == null)
390 throw new ArgumentNullException("container");
391 Contract.EndContractBlock();
393 var monitor = Monitors[container.Account];
394 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
399 public void ToggleSynching()
402 foreach (var pair in Monitors)
404 var monitor = pair.Value;
405 monitor.Pause = !monitor.Pause;
406 isPaused = monitor.Pause;
409 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
410 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
411 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
414 public void ExitPithos()
416 foreach (var pair in Monitors)
418 var monitor = pair.Value;
422 ((Window)GetView()).Close();
427 private Dictionary<PithosStatus, StatusInfo> iconNames = new List<StatusInfo>
429 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
430 new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
431 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
432 }.ToDictionary(s => s.Status);
434 readonly IWindowManager _windowManager;
438 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
440 public void UpdateStatus()
442 var pithosStatus = _statusChecker.GetPithosStatus();
444 if (iconNames.ContainsKey(pithosStatus))
446 var info = iconNames[pithosStatus];
447 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
448 StatusMessage = String.Format("Pithos 1.0\r\n{0}", info.StatusText);
451 _events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
456 private Task StartMonitor(PithosMonitor monitor,int retries=0)
458 return Task.Factory.StartNew(() =>
460 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
464 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
468 catch (WebException exc)
470 if (AbandonRetry(monitor, retries))
473 if (IsUnauthorized(exc))
475 var message = String.Format("API Key Expired for {0}. Starting Renewal",monitor.UserName);
476 Log.Error(message,exc);
477 TryAuthorize(monitor,retries).Wait();
481 TryLater(monitor, exc,retries);
484 catch (Exception exc)
486 if (AbandonRetry(monitor, retries))
489 TryLater(monitor,exc,retries);
495 private bool AbandonRetry(PithosMonitor monitor, int retries)
499 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
501 _events.Publish(new Notification
502 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
509 private async Task TryAuthorize(PithosMonitor monitor,int retries)
511 _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 });
516 var credentials = await PithosAccount.RetrieveCredentialsAsync(Settings.PithosLoginUrl);
518 var account = Settings.Accounts.FirstOrDefault(act => act.AccountName == credentials.UserName);
519 account.ApiKey = credentials.Password;
520 monitor.ApiKey = credentials.Password;
522 await TaskEx.Delay(10000);
523 StartMonitor(monitor, retries + 1);
525 catch (AggregateException exc)
527 string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
528 Log.Error(message, exc.InnerException);
529 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
532 catch (Exception exc)
534 string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
535 Log.Error(message, exc);
536 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
543 private static bool IsUnauthorized(WebException exc)
546 throw new ArgumentNullException("exc");
547 Contract.EndContractBlock();
549 var response = exc.Response as HttpWebResponse;
550 if (response == null)
552 return (response.StatusCode == HttpStatusCode.Unauthorized);
555 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
557 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
558 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
559 _events.Publish(new Notification
560 {Title = "Error", Message = message, Level = TraceLevel.Error});
561 Log.Error(message, exc);
565 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
567 this.StatusMessage = status;
569 _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
572 public void NotifyChangedFile(string filePath)
574 var entry = new FileEntry {FullPath=filePath};
575 IProducerConsumerCollection<FileEntry> files=this.RecentFiles;
577 while (files.Count > 5)
578 files.TryTake(out popped);
582 public void NotifyAccount(AccountInfo account)
587 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
588 Properties.Settings.Default.PithosSite, account.Token,
591 IProducerConsumerCollection<AccountInfo> accounts = Accounts;
592 for (var i = 0; i < _accounts.Count; i++)
595 if (accounts.TryTake(out item))
597 if (item.UserName!=account.UserName)
599 accounts.TryAdd(item);
604 accounts.TryAdd(account);
608 public void RemoveMonitor(string accountName)
610 if (String.IsNullOrWhiteSpace(accountName))
613 PithosMonitor monitor;
614 if (Monitors.TryGetValue(accountName, out monitor))
616 Monitors.Remove(accountName);
621 public void RefreshOverlays()
623 foreach (var pair in Monitors)
625 var monitor = pair.Value;
627 var path = monitor.RootPath;
629 if (String.IsNullOrWhiteSpace(path))
632 if (!Directory.Exists(path) && !File.Exists(path))
635 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
639 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
640 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
641 pathPointer, IntPtr.Zero);
645 Marshal.FreeHGlobal(pathPointer);
650 #region Event Handlers
652 public void Handle(SelectiveSynchChanges message)
654 var accountName = message.Account.AccountName;
655 PithosMonitor monitor;
656 if (_monitors.TryGetValue(accountName, out monitor))
658 monitor.AddSelectivePaths(message.Added);
659 monitor.RemoveSelectivePaths(message.Removed);
666 public void Handle(Notification notification)
668 if (!Settings.ShowDesktopNotifications)
670 BalloonIcon icon = BalloonIcon.None;
671 switch (notification.Level)
673 case TraceLevel.Error:
674 icon = BalloonIcon.Error;
676 case TraceLevel.Info:
677 case TraceLevel.Verbose:
678 icon = BalloonIcon.Info;
680 case TraceLevel.Warning:
681 icon = BalloonIcon.Warning;
684 icon = BalloonIcon.None;
688 if (Settings.ShowDesktopNotifications)
690 var tv = (ShellView) this.GetView();
691 tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
696 public void Handle(ShowFilePropertiesEvent message)
699 throw new ArgumentNullException("message");
700 if (String.IsNullOrWhiteSpace(message.FileName) )
701 throw new ArgumentException("message");
702 Contract.EndContractBlock();
704 var fileName = message.FileName;
706 if (File.Exists(fileName))
707 this.ShowFileProperties(fileName);
708 else if (Directory.Exists(fileName))
709 this.ShowContainerProperties(fileName);