Browse Source

First attempt at portable upgrades

Fixes #82
Antony Male 10 years ago
parent
commit
7ae674006b

+ 4 - 0
Rakefile

@@ -179,6 +179,10 @@ namespace :portable do
           end
         end
 
+        Dir.chdir(File.join('bin', 'PortableInstaller', CONFIG)) do
+          cp_to_portable(portable_dir, 'PortableInstaller.exe')
+        end
+
         cp File.join(SRC_DIR, 'Icons', 'default.ico'), arch_config.portable_output_dir
 
         FileList['*.md', '*.txt'].each do |file|

+ 6 - 0
server/version_check.php

@@ -72,6 +72,12 @@ $versions = [
             'x86' => 'https://github.com/canton7/SyncTrayzor/releases/download/v1.0.32/SyncTrayzorSetup-x86.exe',
          ],
       ],
+      'portable' => [
+         'direct_download_url' => [
+            'x64' => 'https://github.com/canton7/SyncTrayzor/releases/download/v1.0.32/SyncTrayzorPortable-x64.zip',
+            'x86' => 'https://github.com/canton7/SyncTrayzor/releases/download/v1.0.32/SyncTrayzorPortable-x86.zip',
+         ],
+      ],
       'sha1sum_download_url' => 'https://github.com/canton7/SyncTrayzor/releases/download/v1.0.32/sha1sum.txt.asc',
       'release_page_url' => 'https://github.com/canton7/SyncTrayzor/releases/tag/v1.0.32',
       'release_notes' => "- Fix rare crash when trying to save the config file",

+ 6 - 0
src/PortableInstaller/App.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
+    </startup>
+</configuration>

+ 60 - 0
src/PortableInstaller/PortableInstaller.csproj

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{1803AB89-4148-4DFC-AE7B-B4191BD6281C}</ProjectGuid>
+    <OutputType>Exe</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>PortableInstaller</RootNamespace>
+    <AssemblyName>PortableInstaller</AssemblyName>
+    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>..\..\bin\PortableInstaller\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\..\bin\PortableInstaller\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Program.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 161 - 0
src/PortableInstaller/Program.cs

@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PortableInstaller
+{
+    class Program
+    {
+        static int Main(string[] args)
+        {
+            if (args.Length != 4)
+            {
+                Console.WriteLine("You should not invoke this executable directly. It is used as part of the automatic upgrade process for portable installations.");
+                Console.ReadKey();
+                return 0;
+            }
+            try
+            {
+                var destinationPath = args[0];
+                var sourcePath = args[1];
+                var waitForPid = Int32.Parse(args[2]);
+                var pathToRestartApplication = args[3];
+
+                Console.WriteLine("Waiting for process to exit...");
+                try
+                {
+                    using (var process = Process.GetProcessById(waitForPid))
+                    {
+                        if (!process.WaitForExit(5000))
+                            throw new Exception($"SyncTrayzor process with PID {waitForPid} did not exit after 5 seconds");
+                    }
+                }
+                catch (ArgumentException) // It wasn't running to start with. Coolio
+                { }
+
+                // By default ou CWD is the destinationPath, which locks it
+                Directory.SetCurrentDirectory(Path.GetDirectoryName(destinationPath));
+
+                var destinationExists = Directory.Exists(destinationPath);
+
+                if (!Directory.Exists(sourcePath))
+                {
+                    Console.WriteLine($"Unable to find source path {sourcePath}");
+                    return 1;
+                }
+
+                string movedDestinationPath = null;
+                if (destinationExists)
+                {
+                    movedDestinationPath = GenerateBackupDestinationPath(destinationPath);
+                    Console.WriteLine($"Moving {destinationPath} to {movedDestinationPath}");
+                    Directory.Move(destinationPath, movedDestinationPath);
+                }
+
+                Console.WriteLine($"Moving {sourcePath} to {destinationPath}");
+                Directory.Move(sourcePath, destinationPath);
+
+                if (destinationExists)
+                {
+                    var sourceDataFolder = Path.Combine(movedDestinationPath, "data");
+                    if (Directory.Exists(sourceDataFolder))
+                    {
+                        var destDataFolder = Path.Combine(destinationPath, "data");
+                        Console.WriteLine($"Copying data folder {sourceDataFolder} to {destDataFolder}...");
+                        DirectoryCopy(sourceDataFolder, destDataFolder);
+                    }
+                    else
+                    {
+                        Console.WriteLine($"Could not find source data folder {sourceDataFolder}, so not copying");
+                    }
+
+                    var sourceInstallCount = Path.Combine(movedDestinationPath, "InstallCount.txt");
+                    var destInstallCount = Path.Combine(destinationPath, "InstallCount.txt");
+                    if (File.Exists(sourceInstallCount))
+                    {
+                        var installCount = Int32.Parse(File.ReadAllText(sourceInstallCount).Trim());
+                        Console.WriteLine($"Increasing install count to {installCount + 1} from {sourceInstallCount} to {destInstallCount}");
+                        File.WriteAllText(destInstallCount, (installCount + 1).ToString());
+                    }
+                    else
+                    {
+                        Console.WriteLine($"{sourceInstallCount} doesn't exist, so setting installCount to 1 in {destInstallCount}");
+                        File.WriteAllText(destInstallCount, "1");
+                    }
+
+                    Console.WriteLine($"Deleting {movedDestinationPath}");
+                    Directory.Delete(movedDestinationPath, true);
+                }
+
+                Console.WriteLine($"Restarting application {pathToRestartApplication}");
+                Process.Start(pathToRestartApplication);
+
+                return 0;
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine();
+                Console.WriteLine($"--- An error occurred ---");
+                Console.WriteLine($"{e.GetType().Name}: {e.Message}");
+                Console.WriteLine();
+                Console.WriteLine("The upgrade failed to complete successfully. Sorry about that.");
+                Console.WriteLine("Press any key to continue");
+                Console.ReadKey();
+                return 2;
+            }
+        }
+
+        private static string GenerateBackupDestinationPath(string path)
+        {
+            for (int i = 1; i < 1000; i++)
+            {
+                var tempPath = $"{path}_{i}";
+                if (!Directory.Exists(tempPath) && !File.Exists(tempPath))
+                {
+                    return tempPath;
+                }
+            }
+
+            throw new Exception("Count not generate a backup path");
+        }
+
+        // From https://msdn.microsoft.com/en-us/library/bb762914%28v=vs.110%29.aspx
+        private static void DirectoryCopy(string sourceDirName, string destDirName)
+        {
+            // Get the subdirectories for the specified directory.
+            DirectoryInfo dir = new DirectoryInfo(sourceDirName);
+
+            if (!dir.Exists)
+            {
+                throw new DirectoryNotFoundException(
+                    "Source directory does not exist or could not be found: "
+                    + sourceDirName);
+            }
+
+            DirectoryInfo[] dirs = dir.GetDirectories();
+            // If the destination directory doesn't exist, create it.
+            if (!Directory.Exists(destDirName))
+            {
+                Directory.CreateDirectory(destDirName);
+            }
+
+            // Get the files in the directory and copy them to the new location.
+            FileInfo[] files = dir.GetFiles();
+            foreach (FileInfo file in files)
+            {
+                string temppath = Path.Combine(destDirName, file.Name);
+                file.CopyTo(temppath, true); // Overwrite
+            }
+
+            foreach (DirectoryInfo subdir in dirs)
+            {
+                string temppath = Path.Combine(destDirName, subdir.Name);
+                DirectoryCopy(subdir.FullName, temppath);
+            }
+        }
+    }
+}

+ 36 - 0
src/PortableInstaller/Properties/AssemblyInfo.cs

@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("PortableInstaller")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("PortableInstaller")]
+[assembly: AssemblyCopyright("Copyright ©  2015")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("1803ab89-4148-4dfc-ae7b-b4191bd6281c")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 28 - 2
src/SyncTrayzor.sln

@@ -1,10 +1,11 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 2013
-VisualStudioVersion = 12.0.31101.0
+# Visual Studio 14
+VisualStudioVersion = 14.0.24720.0
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncTrayzor", "SyncTrayzor\SyncTrayzor.csproj", "{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}"
 	ProjectSection(ProjectDependencies) = postProject
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C} = {1803AB89-4148-4DFC-AE7B-B4191BD6281C}
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2} = {692BB2F9-CD24-482F-B13A-4335F27F6EC2}
 	EndProjectSection
 EndProject
@@ -12,35 +13,60 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessRunner", "ProcessRun
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChecksumUtil", "ChecksumUtil\ChecksumUtil.csproj", "{58EFC3AF-A52F-4492-A26A-D006890BE508}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PortableInstaller", "PortableInstaller\PortableInstaller.csproj", "{1803AB89-4148-4DFC-AE7B-B4191BD6281C}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
 		Debug|x64 = Debug|x64
 		Debug|x86 = Debug|x86
+		Release|Any CPU = Release|Any CPU
 		Release|x64 = Release|x64
 		Release|x86 = Release|x86
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Debug|Any CPU.ActiveCfg = Debug|x86
 		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Debug|x64.ActiveCfg = Debug|x64
 		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Debug|x64.Build.0 = Debug|x64
 		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Debug|x86.ActiveCfg = Debug|x86
 		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Debug|x86.Build.0 = Debug|x86
+		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|Any CPU.ActiveCfg = Release|x86
 		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x64.ActiveCfg = Release|x64
 		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x64.Build.0 = Release|x64
 		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x86.ActiveCfg = Release|x86
 		{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x86.Build.0 = Release|x86
+		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|Any CPU.ActiveCfg = Debug|x86
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|x64.ActiveCfg = Debug|x64
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|x64.Build.0 = Debug|x64
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|x86.ActiveCfg = Debug|x86
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Debug|x86.Build.0 = Debug|x86
+		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|Any CPU.ActiveCfg = Release|x86
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|x64.ActiveCfg = Release|x64
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|x64.Build.0 = Release|x64
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|x86.ActiveCfg = Release|x86
 		{692BB2F9-CD24-482F-B13A-4335F27F6EC2}.Release|x86.Build.0 = Release|x86
+		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Debug|x64.Build.0 = Debug|Any CPU
 		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Debug|x86.Build.0 = Debug|Any CPU
+		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Release|Any CPU.Build.0 = Release|Any CPU
 		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Release|x64.ActiveCfg = Release|Any CPU
 		{58EFC3AF-A52F-4492-A26A-D006890BE508}.Release|x86.ActiveCfg = Release|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Debug|x64.Build.0 = Debug|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Debug|x86.Build.0 = Debug|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Release|x64.ActiveCfg = Release|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Release|x64.Build.0 = Release|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Release|x86.ActiveCfg = Release|Any CPU
+		{1803AB89-4148-4DFC-AE7B-B4191BD6281C}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 2 - 1
src/SyncTrayzor/Pages/NewVersionAlertToastView.xaml

@@ -29,7 +29,8 @@
                 <Button Command="{s:Action Install}"
                     Visibility="{Binding CanInstall, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
                     <StackPanel Orientation="Horizontal">
-                        <Image Source="{x:Static xaml:UacImageSource.Shield}" VerticalAlignment="Center" Height="17" Margin="0,0,5,0"/>
+                        <Image Source="{x:Static xaml:UacImageSource.Shield}" VerticalAlignment="Center" Height="17" Margin="0,0,5,0"
+                               Visibility="{Binding ShowUacBadge,Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
                         <TextBlock Text="{l:Loc NewVersionAlertView_Button_Install}"/>
                     </StackPanel>
                 </Button>

+ 1 - 0
src/SyncTrayzor/Pages/NewVersionAlertToastViewModel.cs

@@ -7,6 +7,7 @@ namespace SyncTrayzor.Pages
     {
         public bool CanInstall { get; set; }
         public Version Version { get; set; }
+        public bool ShowUacBadge { get; set; }
 
         public bool DontRemindMe { get; private set; }
         public bool ShowMoreDetails { get; private set; }

+ 2 - 1
src/SyncTrayzor/Pages/NewVersionAlertView.xaml

@@ -22,7 +22,8 @@
             <Button IsDefault="True" Command="{s:Action Install}"
                     Visibility="{Binding CanInstall, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
                 <StackPanel Orientation="Horizontal">
-                    <Image Source="{x:Static xaml:UacImageSource.Shield}" VerticalAlignment="Center" Height="17" Margin="0,0,5,0"/>
+                    <Image Source="{x:Static xaml:UacImageSource.Shield}" VerticalAlignment="Center" Height="17" Margin="0,0,5,0"
+                           Visibility="{Binding ShowUacBadge, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
                     <TextBlock Text="{l:Loc NewVersionAlertView_Button_Install}"/>
                 </StackPanel>
             </Button>

+ 1 - 0
src/SyncTrayzor/Pages/NewVersionAlertViewModel.cs

@@ -8,6 +8,7 @@ namespace SyncTrayzor.Pages
         public bool CanInstall { get; set; }
         public Version Version { get; set; }
         public string Changelog { get; set; }
+        public bool ShowUacBadge { get; set; }
 
         public bool DontRemindMe { get; private set; }
 

+ 5 - 2
src/SyncTrayzor/Services/FilesystemProvider.cs

@@ -13,7 +13,7 @@ namespace SyncTrayzor.Services
         FileStream CreateAtomic(string path);
         FileStream OpenRead(string path);
         void Copy(string from, string to);
-        void Move(string from, string to);
+        void MoveFile(string from, string to);
         void CreateDirectory(string path);
         void DeleteFile(string path);
         void DeleteDirectory(string path, bool recursive);
@@ -22,6 +22,7 @@ namespace SyncTrayzor.Services
         string[] GetFiles(string path);
         string ReadAllText(string path);
         string[] GetFiles(string path, string searchPattern, SearchOption searchOption);
+        string[] GetDirectories(string path);
     }
 
     public class FilesystemProvider : IFilesystemProvider
@@ -40,7 +41,7 @@ namespace SyncTrayzor.Services
 
         public void Copy(string from, string to) => File.Copy(from, to);
 
-        public void Move(string from, string to) => File.Move(from, to);
+        public void MoveFile(string from, string to) => File.Move(from, to);
 
         public void CreateDirectory(string path) => Directory.CreateDirectory(path);
 
@@ -57,5 +58,7 @@ namespace SyncTrayzor.Services
         public string ReadAllText(string path) => File.ReadAllText(path);
 
         public string[] GetFiles(string path, string searchPattern, SearchOption searchOption) => Directory.GetFiles(path, searchPattern, searchOption);
+
+        public string[] GetDirectories(string path) => Directory.GetDirectories(path);
     }
 }

+ 4 - 3
src/SyncTrayzor/Services/ProcessStartProvider.cs

@@ -10,7 +10,7 @@ namespace SyncTrayzor.Services
         void Start(string filename);
         void Start(string filename, string arguments);
         void StartDetached(string filename);
-        void StartDetached(string filename, string arguments);
+        void StartDetached(string filename, string arguments, string launchAfterFinished = null);
         void StartElevatedDetached(string filename, string arguments, string launchAfterFinished = null);
     }
 
@@ -42,12 +42,13 @@ namespace SyncTrayzor.Services
             this.StartDetached(filename, null);
         }
 
-        public void StartDetached(string filename, string arguments)
+        public void StartDetached(string filename, string arguments, string launchAfterFinished = null)
         {
             if (arguments == null)
                 arguments = String.Empty;
 
-            var formattedArguments = $"--shell -- \"{filename}\" {arguments}";
+            var launch = launchAfterFinished == null ? null : String.Format("--launch=\"{0}\"", launchAfterFinished.Replace("\"", "\\\""));
+            var formattedArguments = $"--shell {launch} -- \"{filename}\" {arguments}";
 
             logger.Info("Starting {0} {1}", processRunnerPath, formattedArguments);
             var startInfo = new ProcessStartInfo()

+ 2 - 1
src/SyncTrayzor/Services/UpdateManagement/IUpdateVariantHandler.cs

@@ -6,8 +6,9 @@ namespace SyncTrayzor.Services.UpdateManagement
     {
         string VariantName { get; }
         bool CanAutoInstall { get; }
+        bool RequiresUac { get; }
 
         Task<bool> TryHandleUpdateAvailableAsync(VersionCheckResults checkResult);
-        void AutoInstall();
+        void AutoInstall(string pathToRestartApplication);
     }
 }

+ 8 - 14
src/SyncTrayzor/Services/UpdateManagement/InstalledUpdateVariantHandler.cs

@@ -5,33 +5,31 @@ namespace SyncTrayzor.Services.UpdateManagement
 {
     public class InstalledUpdateVariantHandler : IUpdateVariantHandler
     {
+        private const string updateDownloadFileName = "SyncTrayzorUpdate-{0}.exe";
+
         private readonly IUpdateDownloader updateDownloader;
         private readonly IProcessStartProvider processStartProvider;
-        private readonly IAssemblyProvider assemblyProvider;
-        private readonly IApplicationState applicationState;
 
         private string installerPath;
 
         public string VariantName => "installed";
+        public bool RequiresUac => true;
+
         public bool CanAutoInstall { get; private set; }
 
         public InstalledUpdateVariantHandler(
             IUpdateDownloader updateDownloader,
-            IProcessStartProvider processStartProvider,
-            IAssemblyProvider assemblyProvider,
-            IApplicationState applicationState)
+            IProcessStartProvider processStartProvider)
         {
             this.updateDownloader = updateDownloader;
             this.processStartProvider = processStartProvider;
-            this.assemblyProvider = assemblyProvider;
-            this.applicationState = applicationState;
         }
 
         public async Task<bool> TryHandleUpdateAvailableAsync(VersionCheckResults checkResult)
         {
             if (!String.IsNullOrWhiteSpace(checkResult.DownloadUrl) && !String.IsNullOrWhiteSpace(checkResult.Sha1sumDownloadUrl))
             {
-                this.installerPath = await this.updateDownloader.DownloadUpdateAsync(checkResult.DownloadUrl, checkResult.Sha1sumDownloadUrl, checkResult.NewVersion);
+                this.installerPath = await this.updateDownloader.DownloadUpdateAsync(checkResult.DownloadUrl, checkResult.Sha1sumDownloadUrl, checkResult.NewVersion, updateDownloadFileName);
                 this.CanAutoInstall = true;
 
                 // If we return false, the upgrade will be aborted
@@ -46,18 +44,14 @@ namespace SyncTrayzor.Services.UpdateManagement
             }
         }
 
-        public void AutoInstall()
+        public void AutoInstall(string pathToRestartApplication)
         {
             if (!this.CanAutoInstall)
                 throw new InvalidOperationException("Auto-install not available");
             if (this.installerPath == null)
                 throw new InvalidOperationException("TryHandleUpdateAvailableAsync returned false: cannot call AutoInstall");
 
-            var path = $"\"{this.assemblyProvider.Location}\"";
-            if (!this.applicationState.HasMainWindow)
-                path += " -minimized";
-
-            this.processStartProvider.StartElevatedDetached(this.installerPath, "/SILENT", path);
+            this.processStartProvider.StartElevatedDetached(this.installerPath, "/SILENT", pathToRestartApplication);
         }
     }
 }

+ 107 - 6
src/SyncTrayzor/Services/UpdateManagement/PortableUpdateVariantHandler.cs

@@ -1,22 +1,123 @@
-using System;
+using NLog;
+using SyncTrayzor.Services.Config;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
 using System.Threading.Tasks;
 
 namespace SyncTrayzor.Services.UpdateManagement
 {
     public class PortableUpdateVariantHandler : IUpdateVariantHandler
     {
+        private const string updateDownloadFileName = "SyncTrayzorUpdate-{0}.zip";
+        private const string PortableInstallerName = "PortableInstaller.exe";
+
+        private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+        private readonly IUpdateDownloader updateDownloader;
+        private readonly IProcessStartProvider processStartProvider;
+        private readonly IFilesystemProvider filesystem;
+        private readonly IApplicationPathsProvider pathsProvider;
+        private readonly IAssemblyProvider assemblyProvider;
+        private readonly IApplicationState applicationState;
+
+        private string extractedZipPath;
+
         public string VariantName => "portable";
+        public bool RequiresUac => false;
 
-        public bool CanAutoInstall => false;
+        public bool CanAutoInstall { get; private set; }
 
-        public Task<bool> TryHandleUpdateAvailableAsync(VersionCheckResults checkResult)
+        public PortableUpdateVariantHandler(
+            IUpdateDownloader updateDownloader,
+            IProcessStartProvider processStartProvider,
+            IFilesystemProvider filesystem,
+            IApplicationPathsProvider pathsProvider,
+            IAssemblyProvider assemblyProvider,
+            IApplicationState applicationState)
         {
-            return Task.FromResult(true);
+            this.updateDownloader = updateDownloader;
+            this.processStartProvider = processStartProvider;
+            this.filesystem = filesystem;
+            this.pathsProvider = pathsProvider;
+            this.assemblyProvider = assemblyProvider;
+            this.applicationState = applicationState;
         }
 
-        public void AutoInstall()
+        public async Task<bool> TryHandleUpdateAvailableAsync(VersionCheckResults checkResult)
         {
-            throw new InvalidOperationException();
+            if (!String.IsNullOrWhiteSpace(checkResult.DownloadUrl) && !String.IsNullOrWhiteSpace(checkResult.Sha1sumDownloadUrl))
+            {
+                var zipPath = await this.updateDownloader.DownloadUpdateAsync(checkResult.DownloadUrl, checkResult.Sha1sumDownloadUrl, checkResult.NewVersion, updateDownloadFileName);
+                if (zipPath == null)
+                    return false;
+
+                this.extractedZipPath = await this.ExtractDownloadedZip(zipPath);
+
+                this.CanAutoInstall = true;
+
+                // If we return false, the upgrade will be aborted
+                return true;
+            }
+            else
+            {
+                // Can continue, but not auto-install
+                this.CanAutoInstall = false;
+
+                return true;
+            }
+        }
+
+        public void AutoInstall(string pathToRestartApplication)
+        {
+            if (!this.CanAutoInstall)
+                throw new InvalidOperationException("Auto-install not available");
+            if (this.extractedZipPath == null)
+                throw new InvalidOperationException("TryHandleUpdateAvailableAsync returned false: cannot call AutoInstall");
+
+            var portableInstaller = Path.Combine(this.extractedZipPath, PortableInstallerName);
+
+            if (!this.filesystem.FileExists(portableInstaller))
+            {
+                var e = new Exception($"Unable to find portable installer at {portableInstaller}");
+                logger.Error(e);
+                throw e;
+            }
+
+            // Need to move the portable installer out of its extracted archive, otherwise it won't be able to move the archive...
+
+            var destPortableInstaller = Path.Combine(this.pathsProvider.UpdatesDownloadPath, PortableInstallerName);
+            if (this.filesystem.FileExists(destPortableInstaller))
+                this.filesystem.DeleteFile(destPortableInstaller);
+            this.filesystem.MoveFile(portableInstaller, destPortableInstaller);
+
+            var pid = Process.GetCurrentProcess().Id;
+
+            var args = $"\"{Path.GetDirectoryName(this.assemblyProvider.Location)}\" \"{this.extractedZipPath}\" {pid} \"{pathToRestartApplication}\"";
+
+            this.processStartProvider.StartDetached(destPortableInstaller, args);
+
+            this.applicationState.Shutdown();
+        }
+
+        private async Task<string> ExtractDownloadedZip(string zipPath)
+        {
+            var destinationDir = Path.Combine(Path.GetDirectoryName(zipPath), Path.GetFileNameWithoutExtension(zipPath));
+            if (this.filesystem.DirectoryExists(destinationDir))
+            {
+                logger.Info($"Extracted directory {destinationDir} already exists. Deleting...");
+                this.filesystem.DeleteDirectory(destinationDir, true);
+            }
+
+            await Task.Run(() => ZipFile.ExtractToDirectory(zipPath, destinationDir));
+
+            // We expect a single folder inside the extracted dir, called e.g. SyncTrayzorPortable-x86
+            var children = this.filesystem.GetDirectories(destinationDir);
+            if (children.Length != 1)
+                throw new Exception($"Expected 1 child in {destinationDir}, found {String.Join(", ", children)}");
+
+            return children[0]; // Includes the path
         }
     }
 }

+ 3 - 4
src/SyncTrayzor/Services/UpdateManagement/UpdateDownloader.cs

@@ -11,7 +11,7 @@ namespace SyncTrayzor.Services.UpdateManagement
 {
     public interface IUpdateDownloader
     {
-        Task<string> DownloadUpdateAsync(string updateUrl, string sha1sumUrl, Version version);
+        Task<string> DownloadUpdateAsync(string updateUrl, string sha1sumUrl, Version version, string downloadedFileNameTemplate);
     }
 
     public class UpdateDownloader : IUpdateDownloader
@@ -19,7 +19,6 @@ namespace SyncTrayzor.Services.UpdateManagement
         private static readonly Logger logger = LogManager.GetCurrentClassLogger();
 
         private static readonly TimeSpan fileMaxAge = TimeSpan.FromDays(3); // Arbitrary, but long
-        private const string updateDownloadFileName = "SyncTrayzorUpdate-{0}.exe";
         private const string sham1sumDownloadFileName = "sha1sum-{0}.txt.asc";
 
         private readonly string downloadsDir;
@@ -33,10 +32,10 @@ namespace SyncTrayzor.Services.UpdateManagement
             this.installerVerifier = installerVerifier;
         }
 
-        public async Task<string> DownloadUpdateAsync(string updateUrl, string sha1sumUrl, Version version)
+        public async Task<string> DownloadUpdateAsync(string updateUrl, string sha1sumUrl, Version version, string downloadedFileNameTemplate)
         {
             var sha1sumDownloadPath = Path.Combine(this.downloadsDir, String.Format(sham1sumDownloadFileName, version.ToString(3)));
-            var updateDownloadPath = Path.Combine(this.downloadsDir, String.Format(updateDownloadFileName, version.ToString(3)));
+            var updateDownloadPath = Path.Combine(this.downloadsDir, String.Format(downloadedFileNameTemplate, version.ToString(3)));
 
             var sha1sumOutcome = await this.DownloadAndVerifyFileAsync<Stream>(sha1sumUrl, version, sha1sumDownloadPath, () =>
                 {

+ 17 - 5
src/SyncTrayzor/Services/UpdateManagement/UpdateManager.cs

@@ -39,6 +39,7 @@ namespace SyncTrayzor.Services.UpdateManagement
         private readonly IUpdateCheckerFactory updateCheckerFactory;
         private readonly IProcessStartProvider processStartProvider;
         private readonly IUpdatePromptProvider updatePromptProvider;
+        private readonly IAssemblyProvider assemblyProvider;
         private readonly Func<IUpdateVariantHandler> updateVariantHandlerFactory;
         private readonly DispatcherTimer promptTimer;
 
@@ -72,6 +73,7 @@ namespace SyncTrayzor.Services.UpdateManagement
             IUpdateCheckerFactory updateCheckerFactory,
             IProcessStartProvider processStartProvider,
             IUpdatePromptProvider updatePromptProvider,
+            IAssemblyProvider assemblyProvider,
             Func<IUpdateVariantHandler> updateVariantHandlerFactory)
         {
             this.applicationState = applicationState;
@@ -80,6 +82,7 @@ namespace SyncTrayzor.Services.UpdateManagement
             this.updateCheckerFactory = updateCheckerFactory;
             this.processStartProvider = processStartProvider;
             this.updatePromptProvider = updatePromptProvider;
+            this.assemblyProvider = assemblyProvider;
             this.updateVariantHandlerFactory = updateVariantHandlerFactory;
 
             this.promptTimer = new DispatcherTimer();
@@ -182,7 +185,7 @@ namespace SyncTrayzor.Services.UpdateManagement
                 VersionPromptResult promptResult;
                 if (this.applicationState.HasMainWindow)
                 {
-                    promptResult = this.updatePromptProvider.ShowDialog(checkResult, variantHandler.CanAutoInstall);
+                    promptResult = this.updatePromptProvider.ShowDialog(checkResult, variantHandler.CanAutoInstall, variantHandler.RequiresUac);
                 }
                 else
                 {
@@ -193,21 +196,21 @@ namespace SyncTrayzor.Services.UpdateManagement
                     try
                     {
                         this.toastCts = new CancellationTokenSource();
-                        promptResult = await this.updatePromptProvider.ShowToast(checkResult, variantHandler.CanAutoInstall, this.toastCts.Token);
+                        promptResult = await this.updatePromptProvider.ShowToast(checkResult, variantHandler.CanAutoInstall, variantHandler.RequiresUac, this.toastCts.Token);
                         this.toastCts = null;
 
                         // Special case
                         if (promptResult == VersionPromptResult.ShowMoreDetails)
                         {
                             this.applicationWindowState.EnsureInForeground();
-                            promptResult = this.updatePromptProvider.ShowDialog(checkResult, variantHandler.CanAutoInstall);
+                            promptResult = this.updatePromptProvider.ShowDialog(checkResult, variantHandler.CanAutoInstall, variantHandler.RequiresUac);
                         }
                     }
                     catch (OperationCanceledException)
                     {
                         this.toastCts = null;
                         logger.Info("Update toast cancelled. Moving to a dialog");
-                        promptResult = this.updatePromptProvider.ShowDialog(checkResult, variantHandler.CanAutoInstall);
+                        promptResult = this.updatePromptProvider.ShowDialog(checkResult, variantHandler.CanAutoInstall, variantHandler.RequiresUac);
                     }
                 }
 
@@ -216,7 +219,7 @@ namespace SyncTrayzor.Services.UpdateManagement
                     case VersionPromptResult.InstallNow:
                         Debug.Assert(variantHandler.CanAutoInstall);
                         logger.Info("Auto-installing {0}", checkResult.NewVersion);
-                        variantHandler.AutoInstall();
+                        variantHandler. AutoInstall(this.PathToRestartApplication());
                         break;
 
                     case VersionPromptResult.Download:
@@ -248,6 +251,15 @@ namespace SyncTrayzor.Services.UpdateManagement
             }
         }
 
+        private string PathToRestartApplication()
+        {
+            var path = $"\"{this.assemblyProvider.Location}\"";
+            if (!this.applicationState.HasMainWindow)
+                path += " -minimized";
+
+            return path;
+        }
+
         public Task<VersionCheckResults> CheckForAcceptableUpdateAsync()
         {
             var variantHandler = this.updateVariantHandlerFactory();

+ 6 - 4
src/SyncTrayzor/Services/UpdateManagement/UpdatePromptProvider.cs

@@ -18,8 +18,8 @@ namespace SyncTrayzor.Services.UpdateManagement
 
     public interface IUpdatePromptProvider
     {
-        VersionPromptResult ShowDialog(VersionCheckResults checkResults, bool canAutoInstall);
-        Task<VersionPromptResult> ShowToast(VersionCheckResults checkResults, bool canAutoInstall, CancellationToken cancellationToken);
+        VersionPromptResult ShowDialog(VersionCheckResults checkResults, bool canAutoInstall, bool requiresUac);
+        Task<VersionPromptResult> ShowToast(VersionCheckResults checkResults, bool canAutoInstall, bool requiresUac, CancellationToken cancellationToken);
     }
 
     public class UpdatePromptProvider : IUpdatePromptProvider
@@ -41,12 +41,13 @@ namespace SyncTrayzor.Services.UpdateManagement
             this.upgradeAvailableToastViewModelFactory = upgradeAvailableToastViewModelFactory;
         }
 
-        public VersionPromptResult ShowDialog(VersionCheckResults checkResults, bool canAutoInstall)
+        public VersionPromptResult ShowDialog(VersionCheckResults checkResults, bool canAutoInstall, bool requiresUac)
         {
             var vm = this.newVersionAlertViewModelFactory();
             vm.Changelog = checkResults.ReleaseNotes;
             vm.Version = checkResults.NewVersion;
             vm.CanInstall = canAutoInstall;
+            vm.ShowUacBadge = requiresUac;
             var dialogResult = this.windowManager.ShowDialog(vm);
 
             if (dialogResult == true)
@@ -56,11 +57,12 @@ namespace SyncTrayzor.Services.UpdateManagement
             return VersionPromptResult.RemindLater;
         }
 
-        public async Task<VersionPromptResult> ShowToast(VersionCheckResults checkResults, bool canAutoInstall, CancellationToken cancellationToken)
+        public async Task<VersionPromptResult> ShowToast(VersionCheckResults checkResults, bool canAutoInstall, bool requiresUac, CancellationToken cancellationToken)
         {
             var vm = this.upgradeAvailableToastViewModelFactory();
             vm.Version = checkResults.NewVersion;
             vm.CanInstall = canAutoInstall;
+            vm.ShowUacBadge = requiresUac;
 
             var dialogResult = await this.notifyIconManager.ShowBalloonAsync(vm, cancellationToken: cancellationToken);
 

+ 1 - 0
src/SyncTrayzor/SyncTrayzor.csproj

@@ -129,6 +129,7 @@
     <Reference Include="System" />
     <Reference Include="System.Data" />
     <Reference Include="System.Drawing" />
+    <Reference Include="System.IO.Compression.FileSystem" />
     <Reference Include="System.Net.Http" />
     <Reference Include="System.Net.Http.WebRequest" />
     <Reference Include="System.Runtime.Serialization" />