Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
LauncherApp.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 using System;
4 using System.Collections.Generic;
5 using System.Diagnostics;
6 using System.Globalization;
7 using System.IO;
8 using System.Linq;
9 using System.Reflection;
10 using System.Threading;
11 using System.Windows.Forms;
12 using NuGet;
13 using SiliconStudio.Assets;
14 
15 namespace SiliconStudio.LauncherApp
16 {
17  public class LauncherApp : IDisposable, IProgressProvider, ILogger
18  {
19  private const string MainPackageKey = "mainPackage";
20  private const string MainExecutableKey = "mainExecutable";
21 
22  private const string LauncherAppCallbackParam = "LauncherAppCallback";
23  private readonly NugetStore store;
24  private readonly string mainPackage;
25  private readonly string mainExecutable;
26  private bool isSynchronous = false;
27  private readonly List<Thread> downloadThreads;
28  private readonly Stopwatch clock;
29  private int maxPercentage;
30  private bool isInNegativeMode; // workaround for download progression
31 
32  private bool isDownloading;
33 
34  public bool IsSelfUpdated { get; private set; }
35 
36  public bool IsNewPackageAvailable { get; private set; }
37 
38  public IntPtr MainWindowHandle { get; set; }
39 
40  /// <summary>
41  /// Gets or sets a value indicating whether this instance is diagnostic mode (all logs redirected to a file)
42  /// </summary>
43  /// <value><c>true</c> if this instance is diagnostic mode; otherwise, <c>false</c>.</value>
44  public bool IsDiagnosticMode { get; set; }
45 
46  public event EventHandler<ProgressEventArgs> ProgressAvailable;
47 
48  internal event EventHandler<NugetLogEventArgs> LogAvailable;
49 
50  public event EventHandler<LoadingEventArgs> Loading;
51 
52  public event EventHandler<EventArgs> DownloadFinished;
53 
54  public event EventHandler<DialogEventArgs> DialogAvailable;
55 
56  public event EventHandler<Exception> UnhandledException;
57 
58  public event EventHandler<EventArgs> Running;
59 
60  public bool IsDownloading
61  {
62  get
63  {
64  return isDownloading;
65  }
66  set
67  {
68  var previousValue = isDownloading;
69  isDownloading = value;
70  if (previousValue && !isDownloading)
71  {
72  OnDownloadFinished();
73  }
74  }
75  }
76 
77  public static readonly string Version;
78 
79  static LauncherApp()
80  {
81  var assembly = typeof(Program).Assembly;
82 
83  var assemblyInformationalVersion = CustomAttributeProviderExtensions.GetCustomAttribute<AssemblyInformationalVersionAttribute>(assembly);
84  Version = assemblyInformationalVersion.InformationalVersion;
85  }
86 
87  public LauncherApp()
88  {
89  clock = Stopwatch.StartNew();
90 
91  // TODO: Add a way to clear the cache more othen than the default nuget (>= 200 files)
92 
93  // Check config file
94  DebugStep("Load store");
95 
96  // Get the package name and executable to launch/update
97  var thisExeDirectory = Path.GetDirectoryName(typeof(LauncherApp).Assembly.Location);
98 
99  store = new NugetStore(thisExeDirectory);
100  store.Manager.Logger = this;
101  store.SourceRepository.Logger = this;
102 
103  mainPackage = store.Settings.GetConfigValue(MainPackageKey);
104  if (string.IsNullOrWhiteSpace(mainPackage))
105  {
106  throw new LauncherAppException("Invalid configuration. Expecting [{0}] in config", MainPackageKey);
107  }
108  store.DefaultPackageId = mainPackage;
109 
110  mainExecutable = store.Settings.GetConfigValue(MainExecutableKey);
111  if (string.IsNullOrWhiteSpace(mainExecutable))
112  {
113  throw new LauncherAppException("Invalid configuration. Expecting [{0}] in config", MainExecutableKey);
114  }
115 
116  var aggregateRepo = (AggregateRepository)store.Manager.SourceRepository;
117  foreach (var repo in aggregateRepo.Repositories)
118  {
119  var progressProvider = repo as IProgressProvider;
120  if (progressProvider != null)
121  {
122  progressProvider.ProgressAvailable += OnProgressAvailable;
123  }
124  }
125 
126  downloadThreads = new List<Thread>();
127 
128  // Update the targets everytime the launcher is used in order to make sure targets are up-to-date
129  // with packages installed (rewrite for example after a self-update)
130  store.UpdateTargets();
131  }
132 
133  public void Dispose()
134  {
135  if (downloadThreads.Count > 0)
136  {
137  foreach (var downloadThread in downloadThreads)
138  {
139  DebugStep("Waiting for thread {0} to terminate");
140  downloadThread.Join();
141  }
142  downloadThreads.Clear();
143  }
144  }
145 
146  public int Run(string[] args)
147  {
148  // Start self update
149  downloadThreads.Add(RunThread(SelfUpdate));
150  DebugStep("SelfUpdate launched");
151 
152  // Find locally installed package
153  var installedPackage = FindLatestInstalledPackage(store.Manager.LocalRepository);
154 
155  DebugStep("Find installed package");
156 
157  // Do we have a package in the cache?
158  var cachePackage = FindLatestInstalledPackage(MachineCache.Default);
159 
160  DebugStep("Find cache package");
161 
162  // If a package is installed
163  if (installedPackage != null)
164  {
165  if (cachePackage != null && cachePackage.Version > installedPackage.Version)
166  {
167  var processCount = GetProcessCount();
168  bool isSafeToUpdate = processCount <= 1;
169 
170  // If we are safe to update, install the new package
171  if (isSafeToUpdate)
172  {
173  IsDownloading = true;
174  Info("Preparing installer for new {0} version {1}", mainPackage, cachePackage.Version);
175  PackageUpdate(installedPackage, false);
176  IsDownloading = false;
177  DebugStep("Update package");
178  }
179  else
180  {
181  ShowInformationDialog(
182  string.Format("Cannot update {0} as there are [{1}] instances currently running.\n\nClose all your applications and restart", mainPackage, processCount));
183  }
184  }
185  else
186  {
187  DebugStep("Start download package in cache");
188 
189  var localPackage = installedPackage;
190  downloadThreads.Add(RunThread(() => PackageUpdate(localPackage, true)));
191  }
192  }
193  else
194  {
195  if (store.CheckSource())
196  {
197  //store.Manager.InstallPackage(mainPackage);
198  IsDownloading = true;
199  Info("Preparing installer for {0}", mainPackage);
200 
201  store.InstallPackage(mainPackage, null);
202 
203  IsDownloading = false;
204  DebugStep("Package installed");
205  }
206  else
207  {
208  ShowErrorDialog("Download server not available. Please try again later");
209  return 1;
210  }
211 
212  }
213 
214  installedPackage = FindLatestInstalledPackage(store.Manager.LocalRepository);
215 
216  if (installedPackage == null)
217  {
218  ShowErrorDialog("No package installed");
219  return 1;
220  }
221 
222  // Load the assembly and call the default entry point:
223  var fullExePath = GetMainExecutable(installedPackage);
224  Environment.CurrentDirectory = Path.GetDirectoryName(fullExePath);
225  var appDomainSetup = new AppDomainSetup { ApplicationBase = Path.GetDirectoryName(fullExePath) };
226  var newAppDomain = AppDomain.CreateDomain("LauncherAppDomain", null, appDomainSetup);
227 
228  DebugStep("Run executable");
229  OnLoading(new LoadingEventArgs(installedPackage.Id, installedPackage.Version.ToString()));
230 
231  var newArgList = new List<string> { "/LauncherWindowHandle", MainWindowHandle.ToInt64().ToString(CultureInfo.InvariantCulture) };
232  newArgList.AddRange(args);
233 
234  try
235  {
236  return newAppDomain.ExecuteAssembly(fullExePath, newArgList.ToArray());
237  }
238  finally
239  {
240  // Important!: Force back current directory to this application as previous ExecuteAssembly is changing it
241  Environment.CurrentDirectory = Path.GetDirectoryName(typeof(LauncherApp).Assembly.Location);
242 
243  try
244  {
245  AppDomain.Unload(newAppDomain);
246  }
247  catch (Exception)
248  {
249  }
250  }
251  }
252 
253  protected virtual void OnRunning()
254  {
255  EventHandler<EventArgs> handler = Running;
256  if (handler != null) handler(this, EventArgs.Empty);
257  }
258 
259  static public int GetProcessCount()
260  {
261  var currentModule = Assembly.GetAssembly(typeof(LauncherApp));
262  return Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName)
263  .Count(x => string.Equals(x.MainModule.FileName, currentModule.Location, StringComparison.OrdinalIgnoreCase));
264  }
265 
266  private string GetMainExecutable(IPackage package)
267  {
268  var packagePath = store.PathResolver.GetInstallPath(package);
269  return Path.Combine(packagePath, mainExecutable);
270  }
271 
272  private IPackage FindLatestInstalledPackage(IPackageRepository packageRepository)
273  {
274  return packageRepository.FindPackagesById(mainPackage).OrderByDescending(p => p.Version).FirstOrDefault();
275  }
276 
277  private IPackage FindPackageUpdate(IPackage previousPackage)
278  {
279  return store.Manager.SourceRepository.GetUpdates(new[] { previousPackage }, true, false).FirstOrDefault();
280  }
281 
282  private void PackageUpdate(IPackage package, bool putInMachineCache)
283  {
284  //var latestPackages = packages.Where(p => p.IsLatestVersion);
285  DebugStep("Find Package Update");
286  var newPackage = FindPackageUpdate(package);
287 
288  if (newPackage != null && newPackage.Version > package.Version)
289  {
290  DebugStep("Package Update Found {0} {1}", newPackage.Id, newPackage.Version);
291 
292  if (putInMachineCache)
293  {
294  IsDownloading = true;
295  IsNewPackageAvailable = true;
296  ShowInformationDialog("A new version " + newPackage.Version + " of " + mainPackage + @" is available.
297 
298 The download will start in the background.
299 
300 The new version will be available on next run after all GameStudio are closed");
301 
302  MachineCache.Default.AddPackage(newPackage);
303 
304  // Notfy that the download is finished
305  IsDownloading = false;
306  }
307  else
308  {
309  store.UpdatePackage(newPackage);
310  }
311  }
312  }
313 
314  private Thread RunThread(ThreadStart threadStart)
315  {
316  // Start self update
317  if (isSynchronous)
318  {
319  threadStart();
320  return null;
321  }
322  else
323  {
324  var thread = new Thread(
325  () =>
326  {
327  try
328  {
329  threadStart();
330  }
331  catch (Exception exception)
332  {
333  OnUnhandledException(exception);
334  }
335  }) { IsBackground = true };
336  thread.Start();
337  return thread;
338  }
339  }
340 
341  private void SelfUpdate()
342  {
343  var version = new SemanticVersion(Version);
344  var productAttribute = CustomAttributeProviderExtensions.GetCustomAttribute<AssemblyProductAttribute>(typeof(LauncherApp).Assembly);
345  var packageId = productAttribute.Product;
346 
347  var package = store.Manager.SourceRepository.GetUpdates(new[] { new PackageName(packageId, version) }, true, false).FirstOrDefault();
348 
349  // Check to see if an update is needed
350  if (package != null && version < package.Version)
351  {
352  var movedFiles = new List<string>();
353 
354  // Copy files from tools\ to the current directory
355  var inputFiles = package.GetFiles();
356  const string directoryRoot = "tools\\"; // Important!: this is matching where files are store in the nuspec
357  foreach (var file in inputFiles.Where(file => file.Path.StartsWith(directoryRoot)))
358  {
359  var fileName = Path.Combine(store.RootDirectory, file.Path.Substring(directoryRoot.Length));
360 
361  // Move previous files to .old
362  string renamedPath = fileName + ".old";
363 
364  try
365  {
366  if (File.Exists(fileName))
367  {
368  Move(fileName, renamedPath);
369  movedFiles.Add(fileName);
370  }
371 
372  // Update the file
373  UpdateFile(fileName, file);
374  }
375  catch (Exception)
376  {
377  // Revert all olds files if a file didn't work well
378  foreach (var oldFile in movedFiles)
379  {
380  renamedPath = oldFile + ".old";
381  Move(renamedPath, oldFile);
382  }
383  return;
384  }
385  }
386 
387 
388  // Remove .old files
389  foreach (var oldFile in movedFiles)
390  {
391  try
392  {
393  var renamedPath = oldFile + ".old";
394 
395  if (File.Exists(renamedPath))
396  {
397  File.Delete(renamedPath);
398  }
399  }
400  catch (Exception)
401  {
402  }
403  }
404 
405 
406  IsSelfUpdated = true;
407  }
408  }
409 
410  private static void EnsureDirectory(string filePath)
411  {
412  // Create dest directory if it exists
413  var directory = Path.GetDirectoryName(filePath);
414  if (directory != null && !Directory.Exists(directory))
415  {
416  Directory.CreateDirectory(directory);
417  }
418  }
419 
420  private static void UpdateFile(string newFilePath, IPackageFile file)
421  {
422  EnsureDirectory(newFilePath);
423  using (Stream fromStream = file.GetStream(), toStream = File.Create(newFilePath))
424  {
425  fromStream.CopyTo(toStream);
426  }
427  }
428 
429  private static void Move(string oldPath, string newPath)
430  {
431  EnsureDirectory(newPath);
432  try
433  {
434  if (File.Exists(newPath))
435  {
436  File.Delete(newPath);
437  }
438  }
439  catch (FileNotFoundException)
440  {
441 
442  }
443 
444  File.Move(oldPath, newPath);
445  }
446 
447  private void OnProgressAvailable(object sender, ProgressEventArgs e)
448  {
449 
450  // Bug with nuget? At some point, the percent complete revert to negative going downward
451  var percentComplete = e.PercentComplete;
452  if (percentComplete > 0 && !isInNegativeMode)
453  {
454  maxPercentage = percentComplete;
455  }
456 
457  if (percentComplete < 0 || isInNegativeMode)
458  {
459  isInNegativeMode = true;
460  percentComplete = maxPercentage * 2 + percentComplete + 1;
461  }
462 
463  // clamp
464  percentComplete = percentComplete < 0 ? 0 : percentComplete > 100 ? 100 : percentComplete;
465 
466  e = new ProgressEventArgs(e.Operation, percentComplete);
467 
468  var handler = ProgressAvailable;
469  if (handler != null) handler(sender, e);
470  }
471 
472  FileConflictResolution IFileConflictResolver.ResolveFileConflict(string message)
473  {
474  // TODO handle ignore
475  return FileConflictResolution.Ignore;
476  }
477 
478  void ILogger.Log(MessageLevel level, string message, params object[] args)
479  {
480  OnLogAvailable(new NugetLogEventArgs(level, message, args));
481  }
482 
483  private void Info(string message)
484  {
485  OnLogAvailable(new NugetLogEventArgs(MessageLevel.Info, message));
486  }
487 
488  private void Info(string message, params object[] args)
489  {
490  OnLogAvailable(new NugetLogEventArgs(MessageLevel.Info, message, args));
491  }
492 
493  private void Error(string message)
494  {
495  OnLogAvailable(new NugetLogEventArgs(MessageLevel.Error, message));
496  }
497 
498  private void Error(string message, params object[] args)
499  {
500  OnLogAvailable(new NugetLogEventArgs(MessageLevel.Error, message, args));
501  }
502 
503  private void OnLogAvailable(NugetLogEventArgs e)
504  {
505  var handler = LogAvailable;
506  if (handler != null) handler(this, e);
507  }
508 
509  private void OnLoading(LoadingEventArgs e)
510  {
511  var handler = Loading;
512  if (handler != null) handler(this, e);
513  }
514 
515  private void DebugStep(string step)
516  {
517  Console.WriteLine("Step {0} ({1}ms)", step, clock.ElapsedMilliseconds);
518  }
519  private void DebugStep(string step, params object[] args)
520  {
521  DebugStep(string.Format(step, args));
522  }
523 
524  private DialogResult ShowQuestionDialog(string text)
525  {
526  var arg = new DialogEventArgs(text, "Launcher information", MessageBoxButtons.YesNo, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1, 0);
527  OnDialogAvailable(arg);
528  return arg.Result;
529  }
530 
531  private DialogResult ShowInformationDialog(string text)
532  {
533  var arg = new DialogEventArgs(text, "Launcher information", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1, 0);
534  OnDialogAvailable(arg);
535  return arg.Result;
536  }
537 
538  private void ShowErrorDialog(string text)
539  {
540  Error(text);
541  OnDialogAvailable(new DialogEventArgs(text, "Error in Launcher", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, 0));
542  }
543 
544  private void OnDialogAvailable(DialogEventArgs e)
545  {
546  var handler = DialogAvailable;
547  if (handler != null) handler(this, e);
548  }
549 
550  private void OnDownloadFinished()
551  {
552  EventHandler<EventArgs> handler = DownloadFinished;
553  if (handler != null) handler(this, EventArgs.Empty);
554  }
555 
556  private void OnUnhandledException(Exception e)
557  {
558  EventHandler<Exception> handler = UnhandledException;
559  if (handler != null) handler(this, e);
560  }
561  }
562 }
static readonly string Version
Definition: LauncherApp.cs:77
EventHandler< DialogEventArgs > DialogAvailable
Definition: LauncherApp.cs:54
EventHandler< EventArgs > DownloadFinished
Definition: LauncherApp.cs:52
EventHandler< ProgressEventArgs > ProgressAvailable
Definition: LauncherApp.cs:46
System.IO.File File
DialogResult
An enum representing the result of a dialog invocation.
Definition: DialogResult.cs:9
The pointer is moving onto the digitizer.
An error message (level 4).
EventHandler< EventArgs > Running
Definition: LauncherApp.cs:58
An regular info message (level 2).
EventHandler< Exception > UnhandledException
Definition: LauncherApp.cs:56
EventHandler< LoadingEventArgs > Loading
Definition: LauncherApp.cs:50