From a15459366e7cd76ddb33eb882fd117f5aa79493d Mon Sep 17 00:00:00 2001
From: MelonSpeedruns <melonspeedruns@outlook.com>
Date: Tue, 29 Sep 2020 16:05:25 -0400
Subject: Appveyor Ryujinx Updater (#1403)

Co-authored-by: Xpl0itR <xpl0itr@outlook.com>
---
 Ryujinx/Updater/UpdateDialog.cs    |  86 +++++++++
 Ryujinx/Updater/UpdateDialog.glade | 127 ++++++++++++++
 Ryujinx/Updater/Updater.cs         | 347 +++++++++++++++++++++++++++++++++++++
 3 files changed, 560 insertions(+)
 create mode 100644 Ryujinx/Updater/UpdateDialog.cs
 create mode 100644 Ryujinx/Updater/UpdateDialog.glade
 create mode 100644 Ryujinx/Updater/Updater.cs

(limited to 'Ryujinx/Updater')

diff --git a/Ryujinx/Updater/UpdateDialog.cs b/Ryujinx/Updater/UpdateDialog.cs
new file mode 100644
index 00000000..5420baf2
--- /dev/null
+++ b/Ryujinx/Updater/UpdateDialog.cs
@@ -0,0 +1,86 @@
+using Gdk;
+using Gtk;
+using Mono.Unix;
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Ui
+{
+    public class UpdateDialog : Gtk.Window
+    {
+#pragma warning disable CS0649, IDE0044
+        [Builder.Object] public Label MainText;
+        [Builder.Object] public Label SecondaryText;
+        [Builder.Object] public LevelBar ProgressBar;
+        [Builder.Object] public Button YesButton;
+        [Builder.Object] public Button NoButton;
+#pragma warning restore CS0649, IDE0044
+
+        private readonly MainWindow _mainWindow;
+        private readonly string _buildUrl;
+        private bool _restartQuery;
+
+        public UpdateDialog(MainWindow mainWindow, Version newVersion, string buildUrl) : this(new Builder("Ryujinx.Updater.UpdateDialog.glade"), mainWindow, newVersion, buildUrl) { }
+
+        private UpdateDialog(Builder builder, MainWindow mainWindow, Version newVersion, string buildUrl) : base(builder.GetObject("UpdateDialog").Handle)
+        {
+            builder.Autoconnect(this);
+
+            _mainWindow = mainWindow;
+            _buildUrl   = buildUrl;
+
+            MainText.Text      = "Do you want to update Ryujinx to the latest version?";
+            SecondaryText.Text = $"{Program.Version} -> {newVersion}";
+
+            ProgressBar.Hide();
+
+            YesButton.Pressed += YesButton_Pressed;
+            NoButton.Pressed  += NoButton_Pressed;
+        }
+        
+        private void YesButton_Pressed(object sender, EventArgs args)
+        {
+            if (_restartQuery)
+            {
+                string ryuName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Ryujinx.exe" : "Ryujinx";
+                string ryuExe  = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ryuName);
+
+                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                {
+                    UnixFileInfo unixFileInfo = new UnixFileInfo(ryuExe);
+                    unixFileInfo.FileAccessPermissions |= FileAccessPermissions.UserExecute;
+                }
+
+                Process.Start(ryuExe);
+
+                Environment.Exit(0);
+            }
+            else
+            {
+                this.Window.Functions = _mainWindow.Window.Functions = WMFunction.All & WMFunction.Close;
+                _mainWindow.ExitMenuItem.Sensitive = false;
+
+                YesButton.Hide();
+                NoButton.Hide();
+                ProgressBar.Show();
+
+                SecondaryText.Text = "";
+                _restartQuery      = true;
+
+                Updater.UpdateRyujinx(this, _buildUrl);
+            }
+        }
+
+        private void NoButton_Pressed(object sender, EventArgs args)
+        {
+            Updater.Running = false;
+            _mainWindow.Window.Functions = WMFunction.All;
+
+            _mainWindow.ExitMenuItem.Sensitive   = true;
+            _mainWindow.UpdateMenuItem.Sensitive = true;
+
+            this.Dispose();
+        }
+    }
+}
diff --git a/Ryujinx/Updater/UpdateDialog.glade b/Ryujinx/Updater/UpdateDialog.glade
new file mode 100644
index 00000000..cc80167e
--- /dev/null
+++ b/Ryujinx/Updater/UpdateDialog.glade
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkWindow" id="UpdateDialog">
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes">Ryujinx - Updater</property>
+    <property name="resizable">False</property>
+    <property name="window_position">center</property>
+    <property name="default_width">400</property>
+    <property name="default_height">130</property>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <object class="GtkBox" id="MainBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">10</property>
+        <property name="margin_right">10</property>
+        <property name="margin_top">10</property>
+        <property name="margin_bottom">10</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkBox" id="BodyBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkLabel" id="MainText">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_top">5</property>
+                <property name="margin_bottom">5</property>
+                <attributes>
+                  <attribute name="weight" value="bold"/>
+                  <attribute name="size" value="10000"/>
+                </attributes>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="SecondaryText">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_top">5</property>
+                <property name="margin_bottom">5</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLevelBar" id="ProgressBar">
+                <property name="height_request">20</property>
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_top">5</property>
+                <property name="margin_bottom">5</property>
+                <property name="max_value">100</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="ButtonBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkButton" id="YesButton">
+                <property name="label" translatable="yes">Yes</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="margin_right">5</property>
+                <property name="margin_top">5</property>
+                <property name="margin_bottom">5</property>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="NoButton">
+                <property name="label" translatable="yes">No</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="margin_left">5</property>
+                <property name="margin_top">5</property>
+                <property name="margin_bottom">5</property>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/Ryujinx/Updater/Updater.cs b/Ryujinx/Updater/Updater.cs
new file mode 100644
index 00000000..3c164e31
--- /dev/null
+++ b/Ryujinx/Updater/Updater.cs
@@ -0,0 +1,347 @@
+using Gtk;
+using ICSharpCode.SharpZipLib.GZip;
+using ICSharpCode.SharpZipLib.Tar;
+using ICSharpCode.SharpZipLib.Zip;
+using Newtonsoft.Json.Linq;
+using Ryujinx.Common.Logging;
+using Ryujinx.Ui;
+using System;
+using System.IO;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Ryujinx
+{
+    public static class Updater
+    {
+        internal static bool Running;
+
+        private static readonly string HomeDir          = AppDomain.CurrentDomain.BaseDirectory;
+        private static readonly string UpdateDir        = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
+        private static readonly string UpdatePublishDir = Path.Combine(UpdateDir, "publish");
+
+        private static string _jobId;
+        private static string _buildVer;
+        private static string _platformExt;
+        private static string _buildUrl;
+        
+        private const string AppveyorApiUrl = "https://ci.appveyor.com/api";
+
+        public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate)
+        {
+            if (Running) return;
+
+            Running = true;
+            mainWindow.UpdateMenuItem.Sensitive = false;
+
+            // Detect current platform
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                _platformExt = "osx_x64.zip";
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                _platformExt = "win_x64.zip";
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                _platformExt = "linux_x64.tar.gz";
+            }
+
+            Version newVersion;
+            Version currentVersion;
+
+            try
+            {
+                currentVersion = Version.Parse(Program.Version);
+            }
+            catch
+            {
+                GtkDialog.CreateWarningDialog("Failed to convert the current Ryujinx version.", "Cancelling Update!");
+                Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
+
+                return;
+            }
+
+            // Get latest version number from Appveyor
+            try
+            {
+                using (WebClient jsonClient = new WebClient())
+                {
+                    string  fetchedJson = await jsonClient.DownloadStringTaskAsync($"{AppveyorApiUrl}/projects/gdkchan/ryujinx/branch/master");
+                    JObject jsonRoot    = JObject.Parse(fetchedJson);
+                    JToken  buildToken  = jsonRoot["build"];
+
+                    _jobId    = (string)buildToken["jobs"][0]["jobId"];
+                    _buildVer = (string)buildToken["version"];
+                    _buildUrl = $"{AppveyorApiUrl}/buildjobs/{_jobId}/artifacts/ryujinx-{_buildVer}-{_platformExt}";
+                }
+            }
+            catch (Exception exception)
+            {
+                Logger.Error?.Print(LogClass.Application, exception.Message);
+                GtkDialog.CreateErrorDialog("An error has occurred when trying to get release information from AppVeyor.");
+
+                return;
+            }
+
+            try
+            {
+                newVersion = Version.Parse(_buildVer);
+            }
+            catch
+            {
+                GtkDialog.CreateWarningDialog("Failed to convert the received Ryujinx version from AppVeyor.", "Cancelling Update!");
+                Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from AppVeyor!");
+
+                return;
+            }
+
+            if (newVersion <= currentVersion)
+            {
+                if (showVersionUpToDate)
+                {
+                    GtkDialog.CreateInfoDialog("Ryujinx - Updater", "You are already using the most updated version of Ryujinx!", "");
+                }
+
+                Running = false;
+                mainWindow.UpdateMenuItem.Sensitive = true;
+
+                return;
+            }
+
+            // Show a message asking the user if they want to update
+            UpdateDialog updateDialog = new UpdateDialog(mainWindow, newVersion, _buildUrl);
+            updateDialog.Show();
+        }
+
+        public static async Task UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl)
+        {
+            // Empty update dir, although it shouldn't ever have anything inside it
+            if (Directory.Exists(UpdateDir))
+            {
+                Directory.Delete(UpdateDir, true);
+            }
+
+            Directory.CreateDirectory(UpdateDir);
+
+            string updateFile = Path.Combine(UpdateDir, "update.bin");
+
+            // Download the update .zip
+            updateDialog.MainText.Text        = "Downloading Update...";
+            updateDialog.ProgressBar.Value    = 0;
+            updateDialog.ProgressBar.MaxValue = 100;
+
+            using (WebClient client = new WebClient())
+            {
+                client.DownloadProgressChanged += (_, args) =>
+                {
+                    updateDialog.ProgressBar.Value = args.ProgressPercentage;
+                };
+
+                await client.DownloadFileTaskAsync(downloadUrl, updateFile);
+            }
+
+            // Extract Update
+            updateDialog.MainText.Text     = "Extracting Update...";
+            updateDialog.ProgressBar.Value = 0;
+
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                using (Stream         inStream   = File.OpenRead(updateFile))
+                using (Stream         gzipStream = new GZipInputStream(inStream))
+                using (TarInputStream tarStream  = new TarInputStream(gzipStream))
+                {
+                    updateDialog.ProgressBar.MaxValue = inStream.Length;
+
+                    await Task.Run(() =>
+                    {
+                        TarEntry tarEntry;
+                        while ((tarEntry = tarStream.GetNextEntry()) != null)
+                        {
+                            if (tarEntry.IsDirectory) continue;
+
+                            string outPath = Path.Combine(UpdateDir, tarEntry.Name);
+
+                            Directory.CreateDirectory(Path.GetDirectoryName(outPath));
+
+                            using (FileStream outStream = File.OpenWrite(outPath))
+                            {
+                                tarStream.CopyEntryContents(outStream);
+                            }
+
+                            File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
+
+                            TarEntry entry = tarEntry;
+
+                            Application.Invoke(delegate
+                            {
+                                updateDialog.ProgressBar.Value += entry.Size;
+                            });
+                        }
+                    });
+
+                    updateDialog.ProgressBar.Value = inStream.Length;
+                }
+            }
+            else
+            {
+                using (Stream  inStream = File.OpenRead(updateFile))
+                using (ZipFile zipFile  = new ZipFile(inStream))
+                {
+                    updateDialog.ProgressBar.MaxValue = zipFile.Count;
+
+                    await Task.Run(() =>
+                    {
+                        foreach (ZipEntry zipEntry in zipFile)
+                        {
+                            if (zipEntry.IsDirectory) continue;
+
+                            string outPath = Path.Combine(UpdateDir, zipEntry.Name);
+
+                            Directory.CreateDirectory(Path.GetDirectoryName(outPath));
+
+                            using (Stream     zipStream = zipFile.GetInputStream(zipEntry))
+                            using (FileStream outStream = File.OpenWrite(outPath))
+                            {
+                                zipStream.CopyTo(outStream);
+                            }
+
+                            File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
+
+                            Application.Invoke(delegate
+                            {
+                                updateDialog.ProgressBar.Value++;
+                            });
+                        }
+                    });
+                }
+            }
+
+            // Delete downloaded zip
+            File.Delete(updateFile);
+
+            string[] allFiles = Directory.GetFiles(HomeDir, "*", SearchOption.AllDirectories);
+
+            updateDialog.MainText.Text        = "Renaming Old Files...";
+            updateDialog.ProgressBar.Value    = 0;
+            updateDialog.ProgressBar.MaxValue = allFiles.Length;
+
+            // Replace old files
+            await Task.Run(() =>
+            {
+                foreach (string file in allFiles)
+                {
+                    if (!Path.GetExtension(file).Equals(".log"))
+                    {
+                        try
+                        {
+                            File.Move(file, file + ".ryuold");
+
+                            Application.Invoke(delegate
+                            {
+                                updateDialog.ProgressBar.Value++;
+                            });
+                        }
+                        catch
+                        {
+                            Logger.Warning?.Print(LogClass.Application, "Updater wasn't able to rename file: " + file);
+                        }
+                    }
+                }
+
+                Application.Invoke(delegate
+                {
+                    updateDialog.MainText.Text        = "Adding New Files...";
+                    updateDialog.ProgressBar.Value    = 0;
+                    updateDialog.ProgressBar.MaxValue = Directory.GetFiles(UpdatePublishDir, "*", SearchOption.AllDirectories).Length;
+                });
+
+                MoveAllFilesOver(UpdatePublishDir, HomeDir, updateDialog);
+            });
+
+            Directory.Delete(UpdateDir, true);
+
+            updateDialog.MainText.Text      = "Update Complete!";
+            updateDialog.SecondaryText.Text = "Do you want to restart Ryujinx now?";
+            updateDialog.Modal              = true;
+
+            updateDialog.ProgressBar.Hide();
+            updateDialog.YesButton.Show();
+            updateDialog.NoButton.Show();
+        }
+
+        public static bool CanUpdate(bool showWarnings)
+        {
+            if (RuntimeInformation.OSArchitecture != Architecture.X64)
+            {
+                if (showWarnings)
+                {
+                    GtkDialog.CreateWarningDialog("You are not running a supported system architecture!", "(Only x64 systems are supported!)");
+                }
+
+                return false;
+            }
+
+            if (!NetworkInterface.GetIsNetworkAvailable())
+            {
+                if (showWarnings)
+                {
+                    GtkDialog.CreateWarningDialog("You are not connected to the Internet!", "Please verify that you have a working Internet connection!");
+                }
+
+                return false;
+            }
+
+            if (Program.Version.Contains("dirty"))
+            {
+                if (showWarnings)
+                {
+                    GtkDialog.CreateWarningDialog("You Cannot update a Dirty build of Ryujinx!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version.");
+                }
+
+                return false;
+            }
+
+            return true;
+        }
+
+        private static void MoveAllFilesOver(string root, string dest, UpdateDialog dialog)
+        {
+            foreach (string directory in Directory.GetDirectories(root))
+            {
+                string dirName = Path.GetFileName(directory);
+
+                if (!Directory.Exists(Path.Combine(dest, dirName)))
+                {
+                    Directory.CreateDirectory(Path.Combine(dest, dirName));
+                }
+
+                MoveAllFilesOver(directory, Path.Combine(dest, dirName), dialog);
+            }
+
+            foreach (string file in Directory.GetFiles(root))
+            {
+                File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
+
+                Application.Invoke(delegate
+                {
+                    dialog.ProgressBar.Value++;
+                });
+            }
+        }
+
+        public static void CleanupUpdate()
+        {
+            foreach (string file in Directory.GetFiles(HomeDir, "*", SearchOption.AllDirectories))
+            {
+                if (Path.GetExtension(file).EndsWith(".ryuold"))
+                {
+                    File.Delete(file);
+                }
+            }
+        }
+    }
+}
-- 
cgit v1.2.3-70-g09d2