Browse Source

Improved auto updater

flabbet 8 months ago
parent
commit
11340f56f8
30 changed files with 295 additions and 353 deletions
  1. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  2. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  3. 1 0
      src/PixiEditor.OperatingSystem/IProcessUtility.cs
  4. 32 0
      src/PixiEditor.UpdateInstaller.Exe/PixiEditor.UpdateInstaller.Exe.csproj
  5. 36 0
      src/PixiEditor.UpdateInstaller.Exe/Program.cs
  6. 0 35
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/PixiEditor.UpdateInstaller.Desktop.csproj
  7. 0 24
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/Program.cs
  8. 0 18
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/app.manifest
  9. 0 19
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml
  10. 0 29
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml.cs
  11. 1 1
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Extensions.cs
  12. 1 26
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj
  13. 9 24
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/UpdateController.cs
  14. 0 7
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/ViewModelBase.cs
  15. 0 20
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainView.axaml
  16. 0 17
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainView.axaml.cs
  17. 0 13
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainWindow.axaml
  18. 0 52
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainWindow.axaml.cs
  19. 6 0
      src/PixiEditor.UpdateModule/UpdateChecker.cs
  20. 15 5
      src/PixiEditor.UpdateModule/UpdateDownloader.cs
  21. 24 27
      src/PixiEditor.UpdateModule/UpdateInstaller.cs
  22. 14 6
      src/PixiEditor.Windows/WindowsProcessUtility.cs
  23. 33 18
      src/PixiEditor.sln
  24. BIN
      src/PixiEditor/Extensions/PixiEditor.Beta.pixiext
  25. 5 0
      src/PixiEditor/Helpers/ProcessHelper.cs
  26. 3 3
      src/PixiEditor/Models/ExceptionHandling/CrashReport.cs
  27. 2 0
      src/PixiEditor/Models/IO/Paths.cs
  28. 111 7
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  29. 1 1
      windows-x64-release-dev.yml
  30. 1 1
      windows-x64-release.yml

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll


+ 1 - 0
src/PixiEditor.OperatingSystem/IProcessUtility.cs

@@ -5,6 +5,7 @@ namespace PixiEditor.OperatingSystem;
 public interface IProcessUtility
 {
     public Process RunAsAdmin(string path);
+    public Process RunAsAdmin(string path, bool createWindow);
     public bool IsRunningAsAdministrator();
     public void ShellExecute(string toExecute);
 }

+ 32 - 0
src/PixiEditor.UpdateInstaller.Exe/PixiEditor.UpdateInstaller.Exe.csproj

@@ -0,0 +1,32 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
+    <PublishSingleFile>true</PublishSingleFile>
+  </PropertyGroup>
+
+  <!--Debug symbols none on release-->
+  <PropertyGroup Condition="'$(Configuration)'=='Release'">
+    <DebugType>none</DebugType>
+    <DebugSymbols>false</DebugSymbols>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Tools.InnoSetup" Version="6.3.1"/>
+    <ProjectReference Include="..\PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj"/>
+  </ItemGroup>
+  
+  <Target Name="Rename" AfterTargets="AfterBuild">
+    <Move SourceFiles="$(OutDir)PixiEditor.UpdateInstaller.Exe.exe" DestinationFiles="$(OutDir)PixiEditor.UpdateInstaller.exe"/>
+    <Message Text="Renamed build executable file." Importance="high"/>
+  </Target>
+
+  <Target Name="Rename" AfterTargets="Publish">
+    <Move SourceFiles="$(PublishDir)PixiEditor.UpdateInstaller.Exe.exe" DestinationFiles="$(PublishDir)PixiEditor.UpdateInstaller.exe"/>
+    <Message Text="Renamed published executable file." Importance="high"/>
+  </Target>
+</Project>

+ 36 - 0
src/PixiEditor.UpdateInstaller.Exe/Program.cs

@@ -0,0 +1,36 @@
+using System.Diagnostics;
+using System.Text;
+using PixiEditor.UpdateInstaller.ViewModels;
+
+UpdateController controller = new UpdateController();
+StringBuilder log = new StringBuilder();
+
+try
+{
+    log.AppendLine($"{DateTime.Now}: Starting update installation...");
+    controller.InstallUpdate(log);
+}
+catch (Exception ex)
+{
+    log.AppendLine($"{DateTime.Now}: Error during update installation: {ex.Message}");
+    File.AppendAllText("ErrorLog.txt",
+        $"Error PixiEditor.UpdateInstaller: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
+}
+finally
+{
+    try
+    {
+        File.WriteAllText("UpdateLog.txt", log.ToString());
+    }
+    catch
+    {
+       // probably permissions or disk full, the best we can do is to ignore this 
+    }
+    
+    var files = Directory.GetFiles(controller.UpdateDirectory, "PixiEditor.exe");
+    if (files.Length > 0)
+    {
+        string pixiEditorExecutablePath = files[0];
+        Process.Start(pixiEditorExecutablePath);
+    }
+}

+ 0 - 35
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/PixiEditor.UpdateInstaller.Desktop.csproj

@@ -1,35 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-  <PropertyGroup>
-    <OutputType>WinExe</OutputType>
-    <!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
-    One for Windows with net7.0-windows TFM, one for MacOS with net7.0-macos and one with net7.0 TFM for Linux.-->
-    <TargetFramework>net8.0</TargetFramework>
-    <Nullable>enable</Nullable>
-    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
-  </PropertyGroup>
-
-  <PropertyGroup>
-    <ApplicationManifest>app.manifest</ApplicationManifest>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)"/>
-    <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
-    <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)"/>
-    <PackageReference Include="Tools.InnoSetup" Version="6.3.1" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj"/>
-  </ItemGroup>
-  
-  <Target Name="Rename" AfterTargets="Build">
-    <Move SourceFiles="$(OutDir)PixiEditor.UpdateInstaller.Desktop.exe" DestinationFiles="$(OutDir)PixiEditor.UpdateInstaller.exe"/>
-    <Message Text="Renamed build executable file." Importance="high"/>
-  </Target>
-
-  <Target Name="Rename" AfterTargets="Publish">
-    <Move SourceFiles="$(PublishDir)PixiEditor.UpdateInstaller.Desktop.exe" DestinationFiles="$(PublishDir)PixiEditor.UpdateInstaller.exe"/>
-    <Message Text="Renamed published executable file." Importance="high"/>
-  </Target>
-</Project>

+ 0 - 24
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/Program.cs

@@ -1,24 +0,0 @@
-using System;
-using Avalonia;
-using Avalonia.ReactiveUI;
-using PixiEditor.UpdateInstaller.New;
-
-namespace PixiEditor.UpdateInstaller.Desktop;
-
-class Program
-{
-    // Initialization code. Don't use any Avalonia, third-party APIs or any
-    // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
-    // yet and stuff might break.
-    [STAThread]
-    public static void Main(string[] args) => BuildAvaloniaApp()
-        .StartWithClassicDesktopLifetime(args);
-
-    // Avalonia configuration, don't remove; also used by visual designer.
-    public static AppBuilder BuildAvaloniaApp()
-        => AppBuilder.Configure<App>()
-            .UsePlatformDetect()
-            .WithInterFont()
-            .LogToTrace()
-            .UseReactiveUI();
-}

+ 0 - 18
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/app.manifest

@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
-  <!-- This manifest is used on Windows only.
-       Don't remove it as it might cause problems with window transparency and embeded controls.
-       For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
-  <assemblyIdentity version="1.0.0.0" name="PixiEditor.UpdateInstaller.New.Desktop"/>
-
-  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
-    <application>
-      <!-- A list of the Windows versions that this application has been tested on
-           and is designed to work with. Uncomment the appropriate elements
-           and Windows will automatically select the most compatible environment. -->
-
-      <!-- Windows 10 -->
-      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
-    </application>
-  </compatibility>
-</assembly>

+ 0 - 19
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml

@@ -1,19 +0,0 @@
-<Application xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             xmlns:themes="clr-namespace:PixiEditor.UI.Common.Themes;assembly=PixiEditor.UI.Common"
-             x:Class="PixiEditor.UpdateInstaller.New.App"
-             RequestedThemeVariant="Dark">
-             <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
-
-    <Application.Styles>
-        <themes:PixiEditorTheme />
-        <StyleInclude Source="avares://PixiEditor.UI.Common/Styles/PixiEditor.Controls.axaml"/>
-    </Application.Styles>
-    <Application.Resources>
-        <ResourceDictionary>
-            <ResourceDictionary.MergedDictionaries>
-                <ResourceInclude Source="avares://PixiEditor.UI.Common/Accents/Base.axaml"/>
-            </ResourceDictionary.MergedDictionaries>
-        </ResourceDictionary>
-    </Application.Resources>
-</Application>

+ 0 - 29
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml.cs

@@ -1,29 +0,0 @@
-using Avalonia;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Markup.Xaml;
-using PixiEditor.UpdateInstaller.New.ViewModels;
-using PixiEditor.UpdateInstaller.New.Views;
-
-namespace PixiEditor.UpdateInstaller.New;
-
-public partial class App : Application
-{
-    public override void Initialize()
-    {
-        AvaloniaXamlLoader.Load(this);
-    }
-
-    public override void OnFrameworkInitializationCompleted()
-    {
-        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-        {
-            desktop.MainWindow = new MainWindow { DataContext = new MainViewModel() };
-        }
-        else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
-        {
-            singleViewPlatform.MainView = new MainView { DataContext = new MainViewModel() };
-        }
-
-        base.OnFrameworkInitializationCompleted();
-    }
-}

+ 1 - 1
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Extensions.cs

@@ -3,7 +3,7 @@ using System.Diagnostics;
 using System.Runtime.InteropServices;
 using System.Text;
 
-namespace PixiEditor.UpdateInstaller.New;
+namespace PixiEditor.UpdateInstaller;
 
 public static class Extensions
 {

+ 1 - 26
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj

@@ -3,35 +3,10 @@
         <TargetFramework>net8.0</TargetFramework>
         <Nullable>enable</Nullable>
         <LangVersion>latest</LangVersion>
-        <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
-        <RootNamespace>PixiEditor.UpdateInstaller.New</RootNamespace>
+        <RootNamespace>PixiEditor.UpdateInstaller</RootNamespace>
     </PropertyGroup>
 
     <ItemGroup>
-        <AvaloniaResource Include="Assets\**" />
-    </ItemGroup>
-
-    <ItemGroup>
-        <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
-        <PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
-        <PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
-        <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
-        <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
-        <PackageReference Include="MessageBox.Avalonia" Version="2.3.1-prev7.0" />
-    </ItemGroup>
-
-    <ItemGroup>
-      <ProjectReference Include="..\..\PixiEditor.UI.Common\PixiEditor.UI.Common.csproj" />
       <ProjectReference Include="..\..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj" />
     </ItemGroup>
-
-    <ItemGroup>
-      <UpToDateCheckInput Remove="Assets\avalonia-logo.ico" />
-    </ItemGroup>
-
-    <ItemGroup>
-      <AvaloniaResource Include="..\..\PixiEditor\Images\favicon.ico">
-        <Link>Assets\favicon.ico</Link>
-      </AvaloniaResource>
-    </ItemGroup>
 </Project>

+ 9 - 24
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/MainViewModel.cs → src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/UpdateController.cs

@@ -1,16 +1,14 @@
 using System;
 using System.IO;
 using System.Linq;
+using System.Text;
 using PixiEditor.UpdateModule;
-using ReactiveUI;
 
-namespace PixiEditor.UpdateInstaller.New.ViewModels;
+namespace PixiEditor.UpdateInstaller.ViewModels;
 
-public class MainViewModel : ViewModelBase
+public class UpdateController
 {
-    private float progressValue;
-
-    public MainViewModel()
+    public UpdateController()
     {
         Current = this;
 
@@ -22,36 +20,23 @@ public class MainViewModel : ViewModelBase
         UpdateDirectory = updateDirectory;
     }
 
-    public MainViewModel Current { get; private set; }
+    public UpdateController Current { get; private set; }
 
     public UpdateModule.UpdateInstaller Installer { get; set; }
 
     public string UpdateDirectory { get; private set; }
 
-    public float ProgressValue
-    {
-        get => progressValue;
-        set => this.RaiseAndSetIfChanged(ref this.progressValue, value);
-    }
 
-    public void InstallUpdate()
+    public void InstallUpdate(StringBuilder log)
     {
         string[] files = Directory.GetFiles(UpdateDownloader.DownloadLocation, "update-*.zip");
+        log.AppendLine($"Found {files.Length} update files.");
 
         if (files.Length > 0)
         {
             Installer = new UpdateModule.UpdateInstaller(files[0], UpdateDirectory);
-            Installer.ProgressChanged += Installer_ProgressChanged;
-            Installer.Install();
-        }
-        else
-        {
-            ProgressValue = 100;
+            log.AppendLine($"Installing update from {files[0]} to {UpdateDirectory}");
+            Installer.Install(log);
         }
     }
-
-    private void Installer_ProgressChanged(object? sender, UpdateProgressChangedEventArgs e)
-    {
-        ProgressValue = e.Progress;
-    }
 }

+ 0 - 7
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/ViewModelBase.cs

@@ -1,7 +0,0 @@
-using ReactiveUI;
-
-namespace PixiEditor.UpdateInstaller.New.ViewModels;
-
-public class ViewModelBase : ReactiveObject
-{
-}

+ 0 - 20
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainView.axaml

@@ -1,20 +0,0 @@
-<UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
-             xmlns:vm="clr-namespace:PixiEditor.UpdateInstaller.New.ViewModels"
-             mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="350"
-             x:Class="PixiEditor.UpdateInstaller.New.Views.MainView"
-             x:DataType="vm:MainViewModel">
-  <Design.DataContext>
-    <!-- This only sets the DataContext for the previewer in an IDE,
-         to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
-    <vm:MainViewModel />
-  </Design.DataContext>
-
-    <StackPanel Margin="40" Spacing="50">
-        <Image Width="128" Source="avares://PixiEditor.UI.Common/Assets/PixiEditorLogo.png"/>
-        <TextBlock Classes="h4" Text="Installing Update"/>
-        <ProgressBar Minimum="0" Maximum="100" Value="{Binding ProgressValue}" ShowProgressText="True"/>
-        </StackPanel>
-</UserControl>

+ 0 - 17
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainView.axaml.cs

@@ -1,17 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using PixiEditor.UpdateInstaller.New.ViewModels;
-
-namespace PixiEditor.UpdateInstaller.New.Views;
-
-public partial class MainView : UserControl
-{
-    public MainView()
-    {
-        InitializeComponent();
-    }
-}

+ 0 - 13
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainWindow.axaml

@@ -1,13 +0,0 @@
-<Window xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:vm="using:PixiEditor.UpdateInstaller.New.ViewModels"
-        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
-        xmlns:views="clr-namespace:PixiEditor.UpdateInstaller.New.Views"
-        mc:Ignorable="d" d:DesignWidth="350" d:DesignHeight="250"
-        ExtendClientAreaChromeHints="NoChrome" ExtendClientAreaTitleBarHeightHint="30" ExtendClientAreaToDecorationsHint="True"
-        Width="250" Height="350" CanResize="False" WindowStartupLocation="CenterScreen"
-        x:Class="PixiEditor.UpdateInstaller.New.Views.MainWindow" Loaded="Window_OnLoaded"
-        Icon="/Assets/favicon.ico" Background="{DynamicResource Background0}">
-        <views:MainView />
-</Window>

+ 0 - 52
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainWindow.axaml.cs

@@ -1,52 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Threading;
-using PixiEditor.UpdateInstaller.New.ViewModels;
-
-namespace PixiEditor.UpdateInstaller.New.Views;
-
-public partial class MainWindow : Window
-{
-    public MainWindow()
-    {
-        InitializeComponent();
-    }
-
-    private async void Window_OnLoaded(object? sender, RoutedEventArgs e)
-    {
-        MainViewModel vmm = (MainViewModel)DataContext;
-        await Task.Run(() =>
-        {
-            try
-            {
-                vmm.InstallUpdate();
-            }
-            catch (Exception ex)
-            {
-                File.AppendAllText("ErrorLog.txt", $"Error PixiEditor.UpdateInstaller: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
-
-                Dispatcher.UIThread.Invoke(() =>
-                {
-                    var messageBoxStandardWindow = MessageBox.Avalonia.MessageBoxManager
-                        .GetMessageBoxStandardWindow("Update error", ex.Message);
-                    messageBoxStandardWindow.Show();
-                });
-            }
-            finally
-            {
-                var files = Directory.GetFiles(vmm.UpdateDirectory, "PixiEditor.exe");
-                if (files.Length > 0)
-                {
-                    string pixiEditorExecutablePath = files[0];
-                    Process.Start(pixiEditorExecutablePath);
-                }
-            }
-        });
-
-        this.Close();
-    }
-}

+ 6 - 0
src/PixiEditor.UpdateModule/UpdateChecker.cs

@@ -70,6 +70,12 @@ public class UpdateChecker
 
     private static bool TryParseToFloatVersion(string normalizedString, out float ver)
     {
+        if (string.IsNullOrEmpty(normalizedString))
+        {
+            ver = 0;
+            return false;
+        }
+        
         return float.TryParse(normalizedString.Replace(".", string.Empty).Insert(1, "."), NumberStyles.Any, CultureInfo.InvariantCulture, out ver);
     }
 

+ 15 - 5
src/PixiEditor.UpdateModule/UpdateDownloader.cs

@@ -13,7 +13,12 @@ public static class UpdateDownloader
 
     public static async Task DownloadReleaseZip(ReleaseInfo release)
     {
-        Asset matchingAsset = GetMatchingAsset(release);
+        Asset? matchingAsset = GetMatchingAsset(release);
+        
+        if(matchingAsset == null)
+        {
+            throw new FileNotFoundException("No matching update for your system found.");
+        }
 
         using (HttpClient client = new HttpClient())
         {
@@ -31,7 +36,12 @@ public static class UpdateDownloader
 
     public static async Task DownloadInstaller(ReleaseInfo info)
     {
-        Asset matchingAsset = GetMatchingAsset(info, "application/x-msdownload");
+        Asset? matchingAsset = GetMatchingAsset(info, "application/x-msdownload");
+
+        if(matchingAsset == null)
+        {
+            throw new FileNotFoundException("No matching update for your system found.");
+        }
 
         using (HttpClient client = new HttpClient())
         {
@@ -55,10 +65,10 @@ public static class UpdateDownloader
         }
     }
 
-    private static Asset GetMatchingAsset(ReleaseInfo release, string assetType = "zip")
+    private static Asset? GetMatchingAsset(ReleaseInfo release, string assetType = "zip")
     {
         string arch = IntPtr.Size == 8 ? "x64" : "x86";
-        return release.Assets.First(x => x.ContentType.Contains(assetType)
+        return release.Assets.FirstOrDefault(x => x.ContentType.Contains(assetType)
                                          && x.Name.Contains(arch));
     }
-}
+}

+ 24 - 27
src/PixiEditor.UpdateModule/UpdateInstaller.cs

@@ -2,6 +2,7 @@
 using System.Diagnostics;
 using System.IO;
 using System.IO.Compression;
+using System.Text;
 
 namespace PixiEditor.UpdateModule;
 
@@ -9,46 +10,39 @@ public class UpdateInstaller
 {
     public const string TargetDirectoryName = "UpdateFiles";
 
-    private float progress = 0;
-
     public UpdateInstaller(string archiveFileName, string targetDirectory)
     {
         ArchiveFileName = archiveFileName;
         TargetDirectory = targetDirectory;
     }
 
-    public event EventHandler<UpdateProgressChangedEventArgs> ProgressChanged;
-
     public static string UpdateFilesPath { get; set; } = Path.Join(UpdateDownloader.DownloadLocation, TargetDirectoryName);
 
-    public float Progress
-    {
-        get => progress;
-        set
-        {
-            progress = value;
-            ProgressChanged?.Invoke(this, new UpdateProgressChangedEventArgs(value));
-        }
-    }
-
     public string ArchiveFileName { get; set; }
 
     public string TargetDirectory { get; set; }
 
-    public void Install()
+    public void Install(StringBuilder log)
     {
         var processes = Process.GetProcessesByName("PixiEditor");
+        log.AppendLine($"Found {processes.Length} PixiEditor processes running.");
         if (processes.Length > 0)
         {
+            log.AppendLine("Killing PixiEditor processes...");
             processes[0].WaitForExit();
+            log.AppendLine("Processes killed.");
         }
 
+        log.AppendLine("Extracting files");
         ZipFile.ExtractToDirectory(ArchiveFileName, UpdateFilesPath, true);
-        Progress = 25; // 25% for unzip
+        
+        log.AppendLine("Files extracted");
         string dirWithFiles = Directory.GetDirectories(UpdateFilesPath)[0];
-        CopyFilesToDestination(dirWithFiles);
+        log.AppendLine($"Copying files from {dirWithFiles} to {TargetDirectory}");
+        CopyFilesToDestination(dirWithFiles, log);
+        log.AppendLine("Files copied");
+        log.AppendLine("Deleting archive and update files");
         DeleteArchive();
-        Progress = 100;
     }
 
     private void DeleteArchive()
@@ -57,33 +51,37 @@ public class UpdateInstaller
         Directory.Delete(UpdateFilesPath, true);
     }
 
-    private void CopyFilesToDestination(string sourceDirectory)
+    private void CopyFilesToDestination(string sourceDirectory, StringBuilder log)
     {
         int totalFiles = Directory.GetFiles(UpdateFilesPath, "*", SearchOption.AllDirectories).Length;
+        log.AppendLine($"Found {totalFiles} files to copy.");
 
         string[] files = Directory.GetFiles(sourceDirectory);
-        float fileCopiedVal = 74f / totalFiles; // 74% is reserved for copying
 
         foreach (string file in files)
         {
             string targetFileName = Path.GetFileName(file);
-            File.Copy(file, Path.Join(TargetDirectory, targetFileName), true);
-            Progress += fileCopiedVal;
+            string targetFilePath = Path.Join(TargetDirectory, targetFileName);
+            log.AppendLine($"Copying {file} to {targetFilePath}");
+            File.Copy(file, targetFilePath, true);
         }
 
-        CopySubDirectories(sourceDirectory, TargetDirectory, fileCopiedVal);
+        CopySubDirectories(sourceDirectory, TargetDirectory, log);
     }
 
-    private void CopySubDirectories(string originDirectory, string targetDirectory, float percentPerFile)
+    private void CopySubDirectories(string originDirectory, string targetDirectory, StringBuilder log)
     {
         string[] subDirs = Directory.GetDirectories(originDirectory);
+        log.AppendLine($"Found {subDirs.Length} subdirectories to copy.");
         if(subDirs.Length == 0) return;
 
         foreach (string subDir in subDirs)
         {
             string targetDirPath = Path.Join(targetDirectory, Path.GetFileName(subDir));
+            
+            log.AppendLine($"Copying {subDir} to {targetDirPath}");
 
-            CopySubDirectories(subDir, targetDirPath, percentPerFile);
+            CopySubDirectories(subDir, targetDirPath, log);
 
             string[] files = Directory.GetFiles(subDir);
 
@@ -95,10 +93,9 @@ public class UpdateInstaller
             foreach (string file in files)
             {
                 string targetFileName = Path.GetFileName(file);
+                log.AppendLine($"Copying {file} to {Path.Join(targetDirPath, targetFileName)}");
                 File.Copy(file, Path.Join(targetDirPath, targetFileName), true);
             }
-
-            Progress += percentPerFile;
         }
     }
 }

+ 14 - 6
src/PixiEditor.Windows/WindowsProcessUtility.cs

@@ -9,13 +9,21 @@ public class WindowsProcessUtility : IProcessUtility
 {
     public Process RunAsAdmin(string path)
     {
-        var proc = new Process();
-        proc.StartInfo.FileName = path;
-        proc.StartInfo.Verb = "runas";
-        proc.StartInfo.UseShellExecute = true;
-        proc.Start();
+        return RunAsAdmin(path, true);
+    }
+
+    public Process RunAsAdmin(string path, bool createWindow)
+    {
+        ProcessStartInfo startInfo = new ProcessStartInfo
+        {
+            FileName = path,
+            Verb = "runas",
+            UseShellExecute = true,
+            CreateNoWindow = !createWindow,
+            WindowStyle = createWindow ? ProcessWindowStyle.Normal : ProcessWindowStyle.Minimized,
+        };
 
-        return proc;
+        return Process.Start(startInfo);  
     }
 
     public bool IsRunningAsAdministrator()

+ 33 - 18
src/PixiEditor.sln

@@ -48,8 +48,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Platform.Standal
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Extensions", "PixiEditor.Extensions\PixiEditor.Extensions.csproj", "{1249EE2B-EB0D-411C-B311-53A7A22B7743}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateInstaller.Desktop", "PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.Desktop\PixiEditor.UpdateInstaller.Desktop.csproj", "{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UI.Common", "PixiEditor.UI.Common\PixiEditor.UI.Common.csproj", "{FA98BFA6-2E83-41C6-9102-76875B261F51}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OperatingSystems", "OperatingSystems", "{2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}"
@@ -138,6 +136,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.RenderApi.OpenGl", "
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Interop.Avalonia.Core", "Drawie\src\Drawie.Interop.Avalonia.Core\Drawie.Interop.Avalonia.Core.csproj", "{9CD22D8F-3F37-44F8-B106-7C1E02016F82}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.UpdateInstaller.Exe", "PixiEditor.UpdateInstaller.Exe\PixiEditor.UpdateInstaller.Exe.csproj", "{0258658B-78D5-4790-AF27-94B065DF529C}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
@@ -336,21 +336,6 @@ Global
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Steam|x64.Build.0 = Release|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|ARM64.Build.0 = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.Debug|x64.ActiveCfg = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.Debug|x64.Build.0 = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.DevRelease|x64.ActiveCfg = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.DevRelease|x64.Build.0 = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.DevSteam|x64.ActiveCfg = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.DevSteam|x64.Build.0 = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.MSIX Debug|x64.Build.0 = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.MSIX|x64.ActiveCfg = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.MSIX|x64.Build.0 = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.Release|x64.ActiveCfg = Release|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.Release|x64.Build.0 = Release|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.Steam|x64.ActiveCfg = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Debug|x64.Build.0 = Debug|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -819,6 +804,8 @@ Global
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x64.ActiveCfg = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x64.ActiveCfg = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x64.Build.0 = Release|Any CPU
 		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|x64.Build.0 = Debug|Any CPU
 		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
@@ -1237,6 +1224,34 @@ Global
 		{9CD22D8F-3F37-44F8-B106-7C1E02016F82}.Steam|x64.Build.0 = Debug|Any CPU
 		{9CD22D8F-3F37-44F8-B106-7C1E02016F82}.Steam|ARM64.ActiveCfg = Debug|Any CPU
 		{9CD22D8F-3F37-44F8-B106-7C1E02016F82}.Steam|ARM64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Debug|x64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.DevRelease|ARM64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.DevSteam|ARM64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.MSIX|x64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.MSIX|ARM64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Release|x64.ActiveCfg = Release|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Release|x64.Build.0 = Release|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Release|ARM64.Build.0 = Release|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Steam|x64.Build.0 = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Steam|ARM64.ActiveCfg = Debug|Any CPU
+		{0258658B-78D5-4790-AF27-94B065DF529C}.Steam|ARM64.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1257,7 +1272,6 @@ Global
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6} = {9A81B795-66AB-4743-9284-90565941343D}
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE} = {9A81B795-66AB-4743-9284-90565941343D}
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
-		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
 		{FA98BFA6-2E83-41C6-9102-76875B261F51} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
 		{3DF64622-87E3-4870-B694-05D565251BB9} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
@@ -1300,6 +1314,7 @@ Global
 		{7BD495CA-2EB5-4ABC-BDDB-0E1765C40C19} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{FA293BD3-2D99-47BA-8C4F-53F4997CE99C} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{9CD22D8F-3F37-44F8-B106-7C1E02016F82} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
+		{0258658B-78D5-4790-AF27-94B065DF529C} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}

BIN
src/PixiEditor/Extensions/PixiEditor.Beta.pixiext


+ 5 - 0
src/PixiEditor/Helpers/ProcessHelper.cs

@@ -14,4 +14,9 @@ internal static class ProcessHelper
     {
         return IOperatingSystem.Current.ProcessUtility.IsRunningAsAdministrator();
     }
+
+    public static void RunAsAdmin(string updaterPath, bool showWindow)
+    {
+        IOperatingSystem.Current.ProcessUtility.RunAsAdmin(updaterPath, showWindow);
+    }
 }

+ 3 - 3
src/PixiEditor/Models/ExceptionHandling/CrashReport.cs

@@ -341,10 +341,10 @@ internal class CrashReport : IDisposable
         Process process = new();
 
         //TODO: Handle different name for the executable, .Desktop.exe
-#if DEBUG
-        string fileName = Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location) + ".Desktop.exe";
-        #else
         string fileName = Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location) + ".exe";
+#if DEBUG
+        if(!File.Exists(fileName))
+            fileName = Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location) + ".Desktop.exe";
 #endif
 
         process.StartInfo = new()

+ 2 - 0
src/PixiEditor/Models/IO/Paths.cs

@@ -22,4 +22,6 @@ public static class Paths
         $"avares://{Assembly.GetExecutingAssembly().GetName().Name}/Data";
     
     public static string TempRenderingPath { get; } = Path.Combine(Path.GetTempPath(), "PixiEditor", "Rendering");
+    
+    public static string TempFilesPath { get; } = Path.Combine(Path.GetTempPath(), "PixiEditor");
 }

+ 111 - 7
src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -3,6 +3,7 @@ using System.ComponentModel;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
+using System.Text;
 using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
@@ -12,6 +13,7 @@ using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.IO;
 using PixiEditor.Platform;
 using PixiEditor.UpdateModule;
 
@@ -19,6 +21,7 @@ namespace PixiEditor.ViewModels.SubViewModels;
 
 internal class UpdateViewModel : SubViewModel<ViewModelMain>
 {
+    public const int MaxRetryCount = 3;
     private bool updateReadyToInstall = false;
 
     public UpdateChecker UpdateChecker { get; set; }
@@ -70,24 +73,34 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     public async Task<bool> CheckForUpdate()
     {
         bool updateAvailable = await UpdateChecker.CheckUpdateAvailable();
+        if(!UpdateChecker.LatestReleaseInfo.WasDataFetchSuccessful || string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName))
+        {
+            return false;
+        }
+        
         bool updateCompatible = await UpdateChecker.IsUpdateCompatible();
+        bool autoUpdateFailed = CheckAutoupdateFailed();
         bool updateFileDoesNotExists = !File.Exists(
             Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
         bool updateExeDoesNotExists = !File.Exists(
             Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe"));
-        if (updateAvailable && updateFileDoesNotExists && updateExeDoesNotExists)
+        if (updateAvailable && (updateFileDoesNotExists && updateExeDoesNotExists) || autoUpdateFailed)
         {
             UpdateReadyToInstall = false;
             VersionText = new LocalizedString("DOWNLOADING_UPDATE");
             try
             {
-                if (updateCompatible)
+                if (updateCompatible && !autoUpdateFailed)
                 {
                     await UpdateDownloader.DownloadReleaseZip(UpdateChecker.LatestReleaseInfo);
                 }
                 else
                 {
                     await UpdateDownloader.DownloadInstaller(UpdateChecker.LatestReleaseInfo);
+                    if (autoUpdateFailed)
+                    {
+                        RemoveZipIfExists();
+                    }
                 }
 
                 UpdateReadyToInstall = true;
@@ -108,7 +121,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         return false;
     }
 
-    private async void AskToInstall()
+    private async void Install()
     {
 #if RELEASE || DEVRELEASE
         if (!PixiEditorSettings.Update.CheckUpdatesOnStartup.Value)
@@ -137,13 +150,27 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
         if (!updateFileExists && !updateExeExists)
         {
+            EnsureUpdateFilesDeleted();
             return;
         }
 
         ViewModelMain.Current.UpdateSubViewModel.UpdateReadyToInstall = true;
-        var result = await ConfirmationDialog.Show("UPDATE_READY", "NEW_UPDATE");
+        if (!UpdateInfoExists())
+        {
+            CreateUpdateInfo(UpdateChecker.LatestReleaseInfo.TagName);
+            return;
+        }
+
+        string[] info = IncrementUpdateInfo();
+
+        if (!UpdateInfoValid(UpdateChecker.LatestReleaseInfo.TagName, info))
+        {
+            File.Delete(Path.Join(Paths.TempFilesPath, "updateInfo.txt"));
+            CreateUpdateInfo(UpdateChecker.LatestReleaseInfo.TagName);
+            return;
+        }
         
-        if (result != ConfirmationType.Yes)
+        if(!CanInstallUpdate(UpdateChecker.LatestReleaseInfo.TagName, info) && !updateExeExists)
         {
             return;
         }
@@ -163,7 +190,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     {
         try
         {
-            ProcessHelper.RunAsAdmin(updaterPath);
+            ProcessHelper.RunAsAdmin(updaterPath, false);
             Shutdown();
         }
         catch (Win32Exception)
@@ -228,6 +255,10 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             try
             {
                 await CheckForUpdate();
+                if(UpdateChecker.LatestReleaseInfo != null && UpdateChecker.LatestReleaseInfo.TagName == VersionHelpers.GetCurrentAssemblyVersionString())
+                {
+                    EnsureUpdateFilesDeleted();
+                }
             }
             catch (System.Net.Http.HttpRequestException)
             {
@@ -239,7 +270,80 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
                 NoticeDialog.Show("COULD_NOT_CHECK_FOR_UPDATES", "UPDATE_CHECK_FAILED");
             }
 
-            AskToInstall();
+            Install();
+        }
+    }
+    
+    private bool UpdateInfoExists()
+    {
+        return File.Exists(Path.Join(Paths.TempFilesPath, "updateInfo.txt"));
+    }
+    
+    private void CreateUpdateInfo(string targetVersion)
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.AppendLine(targetVersion);
+        sb.AppendLine("0");
+        File.WriteAllText(Path.Join(Paths.TempFilesPath, "updateInfo.txt"), sb.ToString());
+    }
+    
+    private string[] IncrementUpdateInfo()
+    {
+        string[] lines = File.ReadAllLines(Path.Join(Paths.TempFilesPath, "updateInfo.txt"));
+        int.TryParse(lines[1], out int count);
+        count++;
+        lines[1] = count.ToString();
+        File.WriteAllLines(Path.Join(Paths.TempFilesPath, "updateInfo.txt"), lines);
+        
+        return lines;
+    }
+    
+    private void EnsureUpdateFilesDeleted()
+    {
+        string path = Path.Combine(Paths.TempFilesPath, "updateInfo.txt");
+        if(File.Exists(path))
+        {
+            File.Delete(path);
+        }
+    }
+    
+    private bool CanInstallUpdate(string forVersion, string[] lines)
+    {
+        if (lines.Length != 2) return false;
+        
+        if (lines[0] != forVersion) return false;
+        
+        return int.TryParse(lines[1], out int count) && count < MaxRetryCount;
+    }
+    
+    private bool UpdateInfoValid(string forVersion, string[] lines)
+    {
+        if (lines.Length != 2) return false;
+        
+        if (lines[0] != forVersion) return false;
+        
+        return int.TryParse(lines[1], out _);
+    }
+    
+    private bool CheckAutoupdateFailed()
+    {
+        string path = Path.Combine(Paths.TempFilesPath, "updateInfo.txt");
+        if (!File.Exists(path)) return false;
+        
+        string[] lines = File.ReadAllLines(path);
+        if (lines.Length != 2) return false;
+        
+        if (!int.TryParse(lines[1], out int count)) return false;
+        
+        return count >= MaxRetryCount;
+    }
+    
+    private void RemoveZipIfExists()
+    {
+        string zipPath = Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip");
+        if (File.Exists(zipPath))
+        {
+            File.Delete(zipPath);
         }
     }
 

+ 1 - 1
windows-x64-release-dev.yml

@@ -97,7 +97,7 @@ steps:
   inputs:
     command: 'publish'
     publishWebProjects: false
-    projects: '**/PixiEditor.UpdateInstaller.Desktop.csproj'
+    projects: '**/PixiEditor.UpdateInstaller.Exe.csproj'
     arguments: '-o "UpdateInstaller" -r "$(buildPlatform)" --self-contained=false -p:PublishSingleFile=true -c $(buildConfiguration)'
     zipAfterPublish: false
 

+ 1 - 1
windows-x64-release.yml

@@ -97,7 +97,7 @@ steps:
   inputs:
     command: 'publish'
     publishWebProjects: false
-    projects: '**/PixiEditor.UpdateInstaller.Desktop.csproj'
+    projects: '**/PixiEditor.UpdateInstaller.Exe.csproj'
     arguments: '-o "UpdateInstaller" -r "$(buildPlatform)" --self-contained=false -p:PublishSingleFile=true -c $(buildConfiguration)'
     zipAfterPublish: false