Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / ShellViewModel.cs @ 0bd56b7c

History | View | Annotate | Download (17.6 kB)

1
using System.Collections.Concurrent;
2
using System.ComponentModel.Composition;
3
using System.Diagnostics;
4
using System.Diagnostics.Contracts;
5
using System.IO;
6
using System.Net;
7
using System.Runtime.InteropServices;
8
using System.ServiceModel;
9
using System.ServiceModel.Description;
10
using System.Threading.Tasks;
11
using System.Windows;
12
using Caliburn.Micro;
13
using Hardcodet.Wpf.TaskbarNotification;
14
using Pithos.Client.WPF.Configuration;
15
using Pithos.Client.WPF.Properties;
16
using Pithos.Client.WPF.SelectiveSynch;
17
using Pithos.Core;
18
using Pithos.Interfaces;
19
using System;
20
using System.Collections.Generic;
21
using System.Linq;
22
using System.Text;
23
using Pithos.Network;
24
using StatusService = Pithos.Client.WPF.Services.StatusService;
25

    
26
namespace Pithos.Client.WPF {
27
    using System.ComponentModel.Composition;
28

    
29
    [Export(typeof(IShell))]
30
    public class ShellViewModel : ViewAware, IStatusNotification, IShell, 
31
        IHandle<Notification>, IHandle<SelectiveSynchChanges>
32
    {
33
       
34
        private IStatusChecker _statusChecker;
35
        private IEventAggregator _events;
36

    
37
        public PithosSettings Settings { get; private set; }
38

    
39
        public IScreen Parent { get; set; }
40

    
41

    
42
        private Dictionary<string, PithosMonitor> _monitors = new Dictionary<string, PithosMonitor>();
43
        public Dictionary<string, PithosMonitor> Monitors
44
        {
45
            get { return _monitors; }
46
        }
47

    
48
        private ServiceHost _statusService { get; set; }
49

    
50
        private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
51

    
52
        [ImportingConstructor]
53
        public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
54
        {
55
            _windowManager = windowManager;
56
            OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
57
            _statusChecker = statusChecker;
58
            _events = events;
59
            _events.Subscribe(this);
60

    
61
            Settings = settings;
62
                                   
63
            StatusMessage = "In Synch";
64

    
65
            foreach (var account in settings.Accounts)
66
            {
67

    
68
                MonitorAccount(account);
69
            }
70

    
71
            StartStatusService();
72
        }
73
        
74

    
75
        
76
        public void MonitorAccount(AccountSettings account)
77
        {
78
            PithosMonitor monitor = null;
79
            var accountName = account.AccountName;
80

    
81
            if (_monitors.TryGetValue(accountName,out monitor))
82
            {
83
                //If the account is active
84
                if (account.IsActive)                    
85
                    //Start the monitor. It's OK to start an already started monitor,
86
                    //it will just ignore the call
87
                    monitor.Start();
88
                else
89
                {
90
                    //If the account is inactive
91
                    //Stop and remove the monitor
92
                    RemoveMonitor(accountName);
93
                }
94
                return;
95
            }
96

    
97
            //PithosMonitor uses MEF so we need to resolve it
98
            monitor = new PithosMonitor
99
                          {
100
                              UserName = accountName,
101
                              ApiKey = account.ApiKey,
102
                              UsePithos = account.UsePithos,
103
                              StatusNotification = this,
104
                              RootPath=account.RootPath                             
105
                          };          
106
            IoC.BuildUp(monitor);
107

    
108
            var appSettings = Properties.Settings.Default;
109
            monitor.AuthenticationUrl = account.UsePithos
110
                                            ? appSettings.PithosAuthenticationUrl
111
                                            : appSettings.CloudfilesAuthenticationUrl;
112

    
113
            _monitors[accountName] = monitor;
114

    
115
            if (account.IsActive)
116
            {                
117
                //Don't start a monitor if it doesn't have an account and ApiKey
118
                if (String.IsNullOrWhiteSpace(monitor.UserName) || String.IsNullOrWhiteSpace(monitor.ApiKey))
119
                    return;
120
                StartMonitor(monitor);
121
            }
122
        }
123

    
124

    
125
        protected override void OnViewLoaded(object view)
126
        {
127
            var window = (Window)view;
128
            window.Hide();
129
            UpdateStatus();
130
            base.OnViewLoaded(view);
131
        }
132

    
133

    
134
        #region Status Properties
135

    
136
        private string _statusMessage;
137
        public string StatusMessage
138
        {
139
            get { return _statusMessage; }
140
            set
141
            {
142
                _statusMessage = value;
143
                NotifyOfPropertyChange(() => StatusMessage);
144
            }
145
        }
146

    
147
        private ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
148
        public ObservableConcurrentCollection<AccountInfo> Accounts
149
        {
150
            get { return _accounts; }
151
        }
152

    
153

    
154
        private string _pauseSyncCaption="Pause Syncing";
155
        public string PauseSyncCaption
156
        {
157
            get { return _pauseSyncCaption; }
158
            set
159
            {
160
                _pauseSyncCaption = value;
161
                NotifyOfPropertyChange(() => PauseSyncCaption);
162
            }
163
        }
164

    
165
        private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
166
        public ObservableConcurrentCollection<FileEntry> RecentFiles
167
        {
168
            get { return _recentFiles; }
169
        }
170

    
171

    
172
        private string _statusIcon="Images/Tray.ico";
173
        public string StatusIcon
174
        {
175
            get { return _statusIcon; }
176
            set
177
            {
178
                _statusIcon = value;
179
                NotifyOfPropertyChange(() => StatusIcon);
180
            }
181
        }
182

    
183
        #endregion
184

    
185
        #region Commands
186

    
187
        public void ShowPreferences()
188
        {
189
            Settings.Reload();
190
            var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
191
            _windowManager.ShowDialog(preferences);
192
            
193
        }
194

    
195

    
196
        public PithosCommand OpenPithosFolderCommand { get; private set; }
197

    
198
        public void OpenPithosFolder()
199
        {
200
            Process.Start(Settings.PithosPath);
201
        }
202

    
203
        public void GoToSite()
204
        {
205
            
206
        }
207

    
208
        public void GoToSite(AccountInfo account)
209
        {
210
            var site = String.Format("{0}/ui/?token={1}&user={2}",
211
                Properties.Settings.Default.PithosSite,account.Token,
212
                account.UserName);
213
            Process.Start(site);
214
        }
215

    
216

    
217
        public void ToggleSynching()
218
        {
219
            bool isPaused=false;
220
            foreach (var pair in Monitors)
221
            {
222
                var monitor = pair.Value;
223
                monitor.Pause = !monitor.Pause;
224
                isPaused = monitor.Pause;
225
            }
226

    
227
            PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
228
            var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
229
            StatusIcon = String.Format(@"Images/{0}.ico", iconKey);
230
        }
231

    
232
        public void ExitPithos()
233
        {
234
            foreach (var pair in Monitors)
235
            {
236
                var monitor = pair.Value;
237
                monitor.Stop();
238
            }
239

    
240
            ((Window)GetView()).Close();
241
        }
242
        #endregion
243

    
244

    
245
        private Dictionary<PithosStatus, StatusInfo> iconNames = new List<StatusInfo>
246
            {
247
                new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
248
                new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
249
                new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
250
            }.ToDictionary(s => s.Status);
251

    
252
        readonly IWindowManager _windowManager;
253

    
254

    
255
        public void UpdateStatus()
256
        {
257
            var pithosStatus = _statusChecker.GetPithosStatus();
258

    
259
            if (iconNames.ContainsKey(pithosStatus))
260
            {
261
                var info = iconNames[pithosStatus];
262
                StatusIcon = String.Format(@"Images/{0}.ico", info.IconName);
263
                StatusMessage = String.Format("Pithos 1.0\r\n{0}", info.StatusText);
264
            }
265

    
266
            var tv=this.GetView();
267
            _events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
268
        }
269

    
270

    
271
       
272
        private Task StartMonitor(PithosMonitor monitor,int retries=0)
273
        {
274
            return Task.Factory.StartNew(() =>
275
            {
276
                using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
277
                {
278
                    try
279
                    {
280
                        Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
281

    
282
                        monitor.Start();
283
                    }
284
                    catch (WebException exc)
285
                    {
286
                        if (AbandonRetry(monitor, retries))
287
                            return;
288

    
289
                        if (IsUnauthorized(exc))
290
                        {
291
                            var message = String.Format("API Key Expired for {0}. Starting Renewal",monitor.UserName);                            
292
                            Log.Error(message,exc);
293
                            TryAuthorize(monitor,retries);
294
                        }
295
                        else
296
                        {
297
                            TryLater(monitor, exc,retries);
298
                        }
299
                    }
300
                    catch (Exception exc)
301
                    {
302
                        if (AbandonRetry(monitor, retries)) 
303
                            return;
304

    
305
                        TryLater(monitor,exc,retries);
306
                    }
307
                }
308
            });
309
        }
310

    
311
        private bool AbandonRetry(PithosMonitor monitor, int retries)
312
        {
313
            if (retries > 1)
314
            {
315
                var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
316
                                            monitor.UserName);
317
                _events.Publish(new Notification
318
                                    {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
319
                return true;
320
            }
321
            return false;
322
        }
323

    
324

    
325
        private Task TryAuthorize(PithosMonitor monitor,int retries)
326
        {
327
            _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 });
328

    
329
            var authorize= PithosAccount.RetrieveCredentialsAsync(Settings.PithosSite);
330

    
331
            return authorize.ContinueWith(t =>
332
            {
333
                if (t.IsFaulted)
334
                {                    
335
                    string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
336
                    Log.Error(message,t.Exception.InnerException);
337
                    _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
338
                    return;
339
                }
340
                var credentials = t.Result;                
341
                var account =Settings.Accounts.FirstOrDefault(act => act.AccountName == credentials.UserName);
342
                account.ApiKey = credentials.Password;
343
                monitor.ApiKey = credentials.Password;
344
                Settings.Save();
345
                Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
346
            });
347
        }
348

    
349
        private static bool IsUnauthorized(WebException exc)
350
        {
351
            if (exc==null)
352
                throw new ArgumentNullException("exc");
353
            Contract.EndContractBlock();
354

    
355
            var response = exc.Response as HttpWebResponse;
356
            if (response == null)
357
                return false;
358
            return (response.StatusCode == HttpStatusCode.Unauthorized);
359
        }
360

    
361
        private void TryLater(PithosMonitor monitor, Exception exc,int retries)
362
        {
363
            var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
364
            Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
365
            _events.Publish(new Notification
366
                                {Title = "Error", Message = message, Level = TraceLevel.Error});
367
            Log.Error(message, exc);
368
        }
369

    
370

    
371
        public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
372
        {
373
            this.StatusMessage = status;
374
            
375
            _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
376
        }
377

    
378
        public void NotifyChangedFile(string filePath)
379
        {
380
            var entry = new FileEntry {FullPath=filePath};
381
            IProducerConsumerCollection<FileEntry> files=this.RecentFiles;
382
            FileEntry popped;
383
            while (files.Count > 5)
384
                files.TryTake(out popped);
385
            files.TryAdd(entry);
386
        }
387

    
388
        public void NotifyAccount(AccountInfo account)
389
        {
390
            if (account== null)
391
                return;
392

    
393
            account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
394
                Properties.Settings.Default.PithosSite, account.Token,
395
                account.UserName);
396

    
397
            IProducerConsumerCollection<AccountInfo> accounts = Accounts;
398
            for (var i = 0; i < _accounts.Count; i++)
399
            {
400
                AccountInfo item;
401
                if (accounts.TryTake(out item))
402
                {
403
                    if (item.UserName!=account.UserName)
404
                    {
405
                        accounts.TryAdd(item);
406
                    }
407
                }
408
            }
409

    
410
            accounts.TryAdd(account);
411
        }
412

    
413

    
414
        public void RemoveMonitor(string accountName)
415
        {
416
            if (String.IsNullOrWhiteSpace(accountName))
417
                return;
418

    
419
            PithosMonitor monitor;
420
            if (Monitors.TryGetValue(accountName, out monitor))
421
            {
422
                Monitors.Remove(accountName);
423
                monitor.Stop();
424
            }
425
        }
426

    
427
        public void RefreshOverlays()
428
        {
429
            foreach (var pair in Monitors)
430
            {
431
                var monitor = pair.Value;
432

    
433
                var path = monitor.RootPath;
434

    
435
                if (String.IsNullOrWhiteSpace(path))
436
                    continue;
437

    
438
                if (!Directory.Exists(path) && !File.Exists(path))
439
                    continue;
440

    
441
                IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
442

    
443
                try
444
                {
445
                    NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
446
                                                 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
447
                                                 pathPointer, IntPtr.Zero);
448
                }
449
                finally
450
                {
451
                    Marshal.FreeHGlobal(pathPointer);
452
                }
453
            }
454
        }
455

    
456
        private void StartStatusService()
457
        {
458
            // Create a ServiceHost for the CalculatorService type and provide the base address.
459
            var baseAddress = new Uri("net.pipe://localhost/pithos");
460
            _statusService = new ServiceHost(typeof(StatusService), baseAddress);
461

    
462
            var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
463

    
464
            _statusService.AddServiceEndpoint(typeof(IStatusService), binding, "net.pipe://localhost/pithos/statuscache");
465
            _statusService.AddServiceEndpoint(typeof(ISettingsService), binding, "net.pipe://localhost/pithos/settings");
466

    
467

    
468
            //// Add a mex endpoint
469
            var smb = new ServiceMetadataBehavior
470
            {
471
                HttpGetEnabled = true,
472
                HttpGetUrl = new Uri("http://localhost:30000/pithos/mex")
473
            };
474
            _statusService.Description.Behaviors.Add(smb);
475

    
476

    
477
            _statusService.Open();
478
        }
479

    
480
        private void StopStatusService()
481
        {
482
            if (_statusService == null)
483
                return;
484

    
485
            if (_statusService.State == CommunicationState.Faulted)
486
                _statusService.Abort();
487
            else if (_statusService.State != CommunicationState.Closed)
488
                _statusService.Close();
489
            _statusService = null;
490

    
491
        }
492
        #region Event Handlers
493
        
494
        public void Handle(SelectiveSynchChanges message)
495
        {
496
            var accountName = message.Account.AccountName;
497
            PithosMonitor monitor;
498
            if (_monitors.TryGetValue(accountName, out monitor))
499
            {
500
                monitor.AddSelectivePaths(message.Added);
501
                monitor.RemoveSelectivePaths(message.Removed);
502

    
503
            }
504
            
505
        }
506

    
507

    
508
        public void Handle(Notification notification)
509
        {
510
            if (!Settings.ShowDesktopNotifications)
511
                return;
512
            BalloonIcon icon = BalloonIcon.None;
513
            switch (notification.Level)
514
            {
515
                case TraceLevel.Error:
516
                    icon = BalloonIcon.Error;
517
                    break;
518
                case TraceLevel.Info:
519
                case TraceLevel.Verbose:
520
                    icon = BalloonIcon.Info;
521
                    break;
522
                case TraceLevel.Warning:
523
                    icon = BalloonIcon.Warning;
524
                    break;
525
                default:
526
                    icon = BalloonIcon.None;
527
                    break;
528
            }
529

    
530
            var tv = (ShellView)this.GetView();
531
            tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
532
        }
533
        #endregion
534
    }
535
}