Updater.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. using Gtk;
  2. using ICSharpCode.SharpZipLib.GZip;
  3. using ICSharpCode.SharpZipLib.Tar;
  4. using ICSharpCode.SharpZipLib.Zip;
  5. using Newtonsoft.Json.Linq;
  6. using Ryujinx.Common;
  7. using Ryujinx.Common.Logging;
  8. using Ryujinx.Ui;
  9. using Ryujinx.Ui.Widgets;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Diagnostics;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Net;
  16. using System.Net.Http;
  17. using System.Net.NetworkInformation;
  18. using System.Runtime.InteropServices;
  19. using System.Text;
  20. using System.Threading;
  21. using System.Threading.Tasks;
  22. namespace Ryujinx.Modules
  23. {
  24. public static class Updater
  25. {
  26. private const string GitHubApiURL = "https://api.github.com";
  27. private const int ConnectionCount = 4;
  28. internal static bool Running;
  29. private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory;
  30. private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
  31. private static readonly string UpdatePublishDir = Path.Combine(UpdateDir, "publish");
  32. private static string _buildVer;
  33. private static string _platformExt;
  34. private static string _buildUrl;
  35. private static long _buildSize;
  36. // On Windows, GtkSharp.Dependencies adds these extra dirs that must be cleaned during updates.
  37. private static readonly string[] WindowsDependencyDirs = new string[] { "bin", "etc", "lib", "share" };
  38. private static HttpClient ConstructHttpClient()
  39. {
  40. HttpClient result = new HttpClient();
  41. // Required by GitHub to interact with APIs.
  42. result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
  43. return result;
  44. }
  45. public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate)
  46. {
  47. if (Running) return;
  48. Running = true;
  49. mainWindow.UpdateMenuItem.Sensitive = false;
  50. int artifactIndex = -1;
  51. // Detect current platform
  52. if (OperatingSystem.IsMacOS())
  53. {
  54. _platformExt = "osx_x64.zip";
  55. artifactIndex = 1;
  56. }
  57. else if (OperatingSystem.IsWindows())
  58. {
  59. _platformExt = "win_x64.zip";
  60. artifactIndex = 2;
  61. }
  62. else if (OperatingSystem.IsLinux())
  63. {
  64. _platformExt = "linux_x64.tar.gz";
  65. artifactIndex = 0;
  66. }
  67. if (artifactIndex == -1)
  68. {
  69. GtkDialog.CreateErrorDialog("Your platform is not supported!");
  70. return;
  71. }
  72. Version newVersion;
  73. Version currentVersion;
  74. try
  75. {
  76. currentVersion = Version.Parse(Program.Version);
  77. }
  78. catch
  79. {
  80. GtkDialog.CreateWarningDialog("Failed to convert the current Ryujinx version.", "Cancelling Update!");
  81. Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
  82. return;
  83. }
  84. // Get latest version number from GitHub API
  85. try
  86. {
  87. using HttpClient jsonClient = ConstructHttpClient();
  88. string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
  89. // Fetch latest build information
  90. string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
  91. JObject jsonRoot = JObject.Parse(fetchedJson);
  92. JToken assets = jsonRoot["assets"];
  93. _buildVer = (string)jsonRoot["name"];
  94. foreach (JToken asset in assets)
  95. {
  96. string assetName = (string)asset["name"];
  97. string assetState = (string)asset["state"];
  98. string downloadURL = (string)asset["browser_download_url"];
  99. if (assetName.StartsWith("ryujinx") && assetName.EndsWith(_platformExt))
  100. {
  101. _buildUrl = downloadURL;
  102. if (assetState != "uploaded")
  103. {
  104. if (showVersionUpToDate)
  105. {
  106. GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
  107. }
  108. return;
  109. }
  110. break;
  111. }
  112. }
  113. if (_buildUrl == null)
  114. {
  115. if (showVersionUpToDate)
  116. {
  117. GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
  118. }
  119. return;
  120. }
  121. }
  122. catch (Exception exception)
  123. {
  124. Logger.Error?.Print(LogClass.Application, exception.Message);
  125. GtkDialog.CreateErrorDialog("An error occurred when trying to get release information from GitHub Release. This can be caused if a new release is being compiled by GitHub Actions. Try again in a few minutes.");
  126. return;
  127. }
  128. try
  129. {
  130. newVersion = Version.Parse(_buildVer);
  131. }
  132. catch
  133. {
  134. GtkDialog.CreateWarningDialog("Failed to convert the received Ryujinx version from GitHub Release.", "Cancelling Update!");
  135. Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from GitHub Release!");
  136. return;
  137. }
  138. if (newVersion <= currentVersion)
  139. {
  140. if (showVersionUpToDate)
  141. {
  142. GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
  143. }
  144. Running = false;
  145. mainWindow.UpdateMenuItem.Sensitive = true;
  146. return;
  147. }
  148. // Fetch build size information to learn chunk sizes.
  149. using (HttpClient buildSizeClient = ConstructHttpClient())
  150. {
  151. try
  152. {
  153. buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
  154. HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
  155. _buildSize = message.Content.Headers.ContentRange.Length.Value;
  156. }
  157. catch (Exception ex)
  158. {
  159. Logger.Warning?.Print(LogClass.Application, ex.Message);
  160. Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater");
  161. _buildSize = -1;
  162. }
  163. }
  164. // Show a message asking the user if they want to update
  165. UpdateDialog updateDialog = new UpdateDialog(mainWindow, newVersion, _buildUrl);
  166. updateDialog.Show();
  167. }
  168. public static void UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl)
  169. {
  170. // Empty update dir, although it shouldn't ever have anything inside it
  171. if (Directory.Exists(UpdateDir))
  172. {
  173. Directory.Delete(UpdateDir, true);
  174. }
  175. Directory.CreateDirectory(UpdateDir);
  176. string updateFile = Path.Combine(UpdateDir, "update.bin");
  177. // Download the update .zip
  178. updateDialog.MainText.Text = "Downloading Update...";
  179. updateDialog.ProgressBar.Value = 0;
  180. updateDialog.ProgressBar.MaxValue = 100;
  181. if (_buildSize >= 0)
  182. {
  183. DoUpdateWithMultipleThreads(updateDialog, downloadUrl, updateFile);
  184. }
  185. else
  186. {
  187. DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
  188. }
  189. }
  190. private static void DoUpdateWithMultipleThreads(UpdateDialog updateDialog, string downloadUrl, string updateFile)
  191. {
  192. // Multi-Threaded Updater
  193. long chunkSize = _buildSize / ConnectionCount;
  194. long remainderChunk = _buildSize % ConnectionCount;
  195. int completedRequests = 0;
  196. int totalProgressPercentage = 0;
  197. int[] progressPercentage = new int[ConnectionCount];
  198. List<byte[]> list = new List<byte[]>(ConnectionCount);
  199. List<WebClient> webClients = new List<WebClient>(ConnectionCount);
  200. for (int i = 0; i < ConnectionCount; i++)
  201. {
  202. list.Add(Array.Empty<byte>());
  203. }
  204. for (int i = 0; i < ConnectionCount; i++)
  205. {
  206. #pragma warning disable SYSLIB0014
  207. // TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
  208. using WebClient client = new WebClient();
  209. #pragma warning restore SYSLIB0014
  210. webClients.Add(client);
  211. if (i == ConnectionCount - 1)
  212. {
  213. client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
  214. }
  215. else
  216. {
  217. client.Headers.Add("Range", $"bytes={chunkSize * i}-{chunkSize * (i + 1) - 1}");
  218. }
  219. client.DownloadProgressChanged += (_, args) =>
  220. {
  221. int index = (int)args.UserState;
  222. Interlocked.Add(ref totalProgressPercentage, -1 * progressPercentage[index]);
  223. Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
  224. Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
  225. updateDialog.ProgressBar.Value = totalProgressPercentage / ConnectionCount;
  226. };
  227. client.DownloadDataCompleted += (_, args) =>
  228. {
  229. int index = (int)args.UserState;
  230. if (args.Cancelled)
  231. {
  232. webClients[index].Dispose();
  233. return;
  234. }
  235. list[index] = args.Result;
  236. Interlocked.Increment(ref completedRequests);
  237. if (Equals(completedRequests, ConnectionCount))
  238. {
  239. byte[] mergedFileBytes = new byte[_buildSize];
  240. for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
  241. {
  242. Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
  243. destinationOffset += list[connectionIndex].Length;
  244. }
  245. File.WriteAllBytes(updateFile, mergedFileBytes);
  246. try
  247. {
  248. InstallUpdate(updateDialog, updateFile);
  249. }
  250. catch (Exception e)
  251. {
  252. Logger.Warning?.Print(LogClass.Application, e.Message);
  253. Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
  254. DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
  255. return;
  256. }
  257. }
  258. };
  259. try
  260. {
  261. client.DownloadDataAsync(new Uri(downloadUrl), i);
  262. }
  263. catch (WebException ex)
  264. {
  265. Logger.Warning?.Print(LogClass.Application, ex.Message);
  266. Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
  267. foreach (WebClient webClient in webClients)
  268. {
  269. webClient.CancelAsync();
  270. }
  271. DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
  272. return;
  273. }
  274. }
  275. }
  276. private static void DoUpdateWithSingleThreadWorker(UpdateDialog updateDialog, string downloadUrl, string updateFile)
  277. {
  278. using HttpClient client = new HttpClient();
  279. // We do not want to timeout while downloading
  280. client.Timeout = TimeSpan.FromDays(1);
  281. using (HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result)
  282. using (Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result)
  283. {
  284. using (Stream updateFileStream = File.Open(updateFile, FileMode.Create))
  285. {
  286. long totalBytes = response.Content.Headers.ContentLength.Value;
  287. long byteWritten = 0;
  288. byte[] buffer = new byte[32 * 1024];
  289. while (true)
  290. {
  291. int readSize = remoteFileStream.Read(buffer);
  292. if (readSize == 0)
  293. {
  294. break;
  295. }
  296. byteWritten += readSize;
  297. updateDialog.ProgressBar.Value = ((double)byteWritten / totalBytes) * 100;
  298. updateFileStream.Write(buffer, 0, readSize);
  299. }
  300. }
  301. }
  302. InstallUpdate(updateDialog, updateFile);
  303. }
  304. private static void DoUpdateWithSingleThread(UpdateDialog updateDialog, string downloadUrl, string updateFile)
  305. {
  306. Thread worker = new Thread(() => DoUpdateWithSingleThreadWorker(updateDialog, downloadUrl, updateFile))
  307. {
  308. Name = "Updater.SingleThreadWorker"
  309. };
  310. worker.Start();
  311. }
  312. private static async void InstallUpdate(UpdateDialog updateDialog, string updateFile)
  313. {
  314. // Extract Update
  315. updateDialog.MainText.Text = "Extracting Update...";
  316. updateDialog.ProgressBar.Value = 0;
  317. if (OperatingSystem.IsLinux())
  318. {
  319. using Stream inStream = File.OpenRead(updateFile);
  320. using Stream gzipStream = new GZipInputStream(inStream);
  321. using TarInputStream tarStream = new TarInputStream(gzipStream, Encoding.ASCII);
  322. updateDialog.ProgressBar.MaxValue = inStream.Length;
  323. await Task.Run(() =>
  324. {
  325. TarEntry tarEntry;
  326. if (!OperatingSystem.IsWindows())
  327. {
  328. while ((tarEntry = tarStream.GetNextEntry()) != null)
  329. {
  330. if (tarEntry.IsDirectory) continue;
  331. string outPath = Path.Combine(UpdateDir, tarEntry.Name);
  332. Directory.CreateDirectory(Path.GetDirectoryName(outPath));
  333. using (FileStream outStream = File.OpenWrite(outPath))
  334. {
  335. tarStream.CopyEntryContents(outStream);
  336. }
  337. File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
  338. File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
  339. TarEntry entry = tarEntry;
  340. Application.Invoke(delegate
  341. {
  342. updateDialog.ProgressBar.Value += entry.Size;
  343. });
  344. }
  345. }
  346. });
  347. updateDialog.ProgressBar.Value = inStream.Length;
  348. }
  349. else
  350. {
  351. using Stream inStream = File.OpenRead(updateFile);
  352. using ZipFile zipFile = new ZipFile(inStream);
  353. updateDialog.ProgressBar.MaxValue = zipFile.Count;
  354. await Task.Run(() =>
  355. {
  356. foreach (ZipEntry zipEntry in zipFile)
  357. {
  358. if (zipEntry.IsDirectory) continue;
  359. string outPath = Path.Combine(UpdateDir, zipEntry.Name);
  360. Directory.CreateDirectory(Path.GetDirectoryName(outPath));
  361. using (Stream zipStream = zipFile.GetInputStream(zipEntry))
  362. using (FileStream outStream = File.OpenWrite(outPath))
  363. {
  364. zipStream.CopyTo(outStream);
  365. }
  366. File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
  367. Application.Invoke(delegate
  368. {
  369. updateDialog.ProgressBar.Value++;
  370. });
  371. }
  372. });
  373. }
  374. // Delete downloaded zip
  375. File.Delete(updateFile);
  376. List<string> allFiles = EnumerateFilesToDelete().ToList();
  377. updateDialog.MainText.Text = "Renaming Old Files...";
  378. updateDialog.ProgressBar.Value = 0;
  379. updateDialog.ProgressBar.MaxValue = allFiles.Count;
  380. // Replace old files
  381. await Task.Run(() =>
  382. {
  383. foreach (string file in allFiles)
  384. {
  385. try
  386. {
  387. File.Move(file, file + ".ryuold");
  388. Application.Invoke(delegate
  389. {
  390. updateDialog.ProgressBar.Value++;
  391. });
  392. }
  393. catch
  394. {
  395. Logger.Warning?.Print(LogClass.Application, "Updater was unable to rename file: " + file);
  396. }
  397. }
  398. Application.Invoke(delegate
  399. {
  400. updateDialog.MainText.Text = "Adding New Files...";
  401. updateDialog.ProgressBar.Value = 0;
  402. updateDialog.ProgressBar.MaxValue = Directory.GetFiles(UpdatePublishDir, "*", SearchOption.AllDirectories).Length;
  403. });
  404. MoveAllFilesOver(UpdatePublishDir, HomeDir, updateDialog);
  405. });
  406. Directory.Delete(UpdateDir, true);
  407. updateDialog.MainText.Text = "Update Complete!";
  408. updateDialog.SecondaryText.Text = "Do you want to restart Ryujinx now?";
  409. updateDialog.Modal = true;
  410. updateDialog.ProgressBar.Hide();
  411. updateDialog.YesButton.Show();
  412. updateDialog.NoButton.Show();
  413. }
  414. public static bool CanUpdate(bool showWarnings)
  415. {
  416. #if !DISABLE_UPDATER
  417. if (RuntimeInformation.OSArchitecture != Architecture.X64)
  418. {
  419. if (showWarnings)
  420. {
  421. GtkDialog.CreateWarningDialog("You are not running a supported system architecture!", "(Only x64 systems are supported!)");
  422. }
  423. return false;
  424. }
  425. if (!NetworkInterface.GetIsNetworkAvailable())
  426. {
  427. if (showWarnings)
  428. {
  429. GtkDialog.CreateWarningDialog("You are not connected to the Internet!", "Please verify that you have a working Internet connection!");
  430. }
  431. return false;
  432. }
  433. if (Program.Version.Contains("dirty") || !ReleaseInformation.IsValid())
  434. {
  435. if (showWarnings)
  436. {
  437. 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.");
  438. }
  439. return false;
  440. }
  441. return true;
  442. #else
  443. if (showWarnings)
  444. {
  445. if (ReleaseInformation.IsFlatHubBuild())
  446. {
  447. GtkDialog.CreateWarningDialog("Updater Disabled!", "Please update Ryujinx via FlatHub.");
  448. }
  449. else
  450. {
  451. GtkDialog.CreateWarningDialog("Updater Disabled!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version.");
  452. }
  453. }
  454. return false;
  455. #endif
  456. }
  457. // NOTE: This method should always reflect the latest build layout.
  458. private static IEnumerable<string> EnumerateFilesToDelete()
  459. {
  460. var files = Directory.EnumerateFiles(HomeDir); // All files directly in base dir.
  461. if (OperatingSystem.IsWindows())
  462. {
  463. foreach (string dir in WindowsDependencyDirs)
  464. {
  465. string dirPath = Path.Combine(HomeDir, dir);
  466. if (Directory.Exists(dirPath))
  467. {
  468. files = files.Concat(Directory.EnumerateFiles(dirPath, "*", SearchOption.AllDirectories));
  469. }
  470. }
  471. }
  472. return files;
  473. }
  474. private static void MoveAllFilesOver(string root, string dest, UpdateDialog dialog)
  475. {
  476. foreach (string directory in Directory.GetDirectories(root))
  477. {
  478. string dirName = Path.GetFileName(directory);
  479. if (!Directory.Exists(Path.Combine(dest, dirName)))
  480. {
  481. Directory.CreateDirectory(Path.Combine(dest, dirName));
  482. }
  483. MoveAllFilesOver(directory, Path.Combine(dest, dirName), dialog);
  484. }
  485. foreach (string file in Directory.GetFiles(root))
  486. {
  487. File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
  488. Application.Invoke(delegate
  489. {
  490. dialog.ProgressBar.Value++;
  491. });
  492. }
  493. }
  494. public static void CleanupUpdate()
  495. {
  496. foreach (string file in EnumerateFilesToDelete())
  497. {
  498. if (Path.GetExtension(file).EndsWith(".ryuold"))
  499. {
  500. File.Delete(file);
  501. }
  502. }
  503. }
  504. }
  505. }