mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-25 01:33:51 +02:00
Electron support for Linux (#1074)
* init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Fix UI var * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * Test updater * Rebase and handle merge conflicts * Fix Linux updater * Fix Linux app restart * Fix friend order * Handle AppImageInstaller, show an install message on Linux * Updates to the AppImage installer * Fix Linux updater, fix set version, check for .NET, copy wine prefix * Handle random errors * Rotate tall prints * try fix Linux restart bug * Final --------- Co-authored-by: rs189 <35667100+rs189@users.noreply.github.com>
This commit is contained in:
277
Dotnet/Update.cs
277
Dotnet/Update.cs
@@ -5,48 +5,88 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
|
||||
#if !LINUX
|
||||
using System.Windows.Forms;
|
||||
#endif
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
internal class Update
|
||||
public class Update
|
||||
{
|
||||
private static readonly string VRCX_Setup_Executable = Path.Combine(Program.AppDataDirectory, "VRCX_Setup.exe");
|
||||
private static readonly string Update_Executable = Path.Combine(Program.AppDataDirectory, "update.exe");
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly string VrcxSetupExecutable = Path.Combine(Program.AppDataDirectory, "VRCX_Setup.exe");
|
||||
private static readonly string UpdateExecutable = Path.Combine(Program.AppDataDirectory, "update.exe");
|
||||
private static readonly string TempDownload = Path.Combine(Program.AppDataDirectory, "tempDownload");
|
||||
private static readonly string HashLocation = Path.Combine(Program.AppDataDirectory, "sha256sum.txt");
|
||||
private static readonly HttpClient httpClient;
|
||||
private static CancellationToken _cancellationToken;
|
||||
public static int UpdateProgress;
|
||||
private static string AppImagePath = string.Empty;
|
||||
private static string AppImagePathOld = string.Empty;
|
||||
|
||||
static Update()
|
||||
{
|
||||
var httpClientHandler = new HttpClientHandler();
|
||||
if (WebApi.ProxySet)
|
||||
httpClientHandler.Proxy = WebApi.Proxy;
|
||||
|
||||
httpClient = new HttpClient(httpClientHandler);
|
||||
httpClient.DefaultRequestHeaders.Add("User-Agent", Program.Version);
|
||||
}
|
||||
|
||||
public void Init(string appImagePath = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(appImagePath))
|
||||
return;
|
||||
|
||||
AppImagePath = appImagePath;
|
||||
AppImagePathOld = appImagePath + ".old";
|
||||
logger.Info($"AppImagePath: {AppImagePath}");
|
||||
}
|
||||
|
||||
public static void Check()
|
||||
{
|
||||
if (Process.GetProcessesByName("VRCX_Setup").Length > 0)
|
||||
Environment.Exit(0);
|
||||
var setupHash = Path.Combine(Program.AppDataDirectory, "sha256sum.txt");
|
||||
if (File.Exists(setupHash))
|
||||
File.Delete(setupHash);
|
||||
var tempDownload = Path.Combine(Program.AppDataDirectory, "tempDownload.exe");
|
||||
if (File.Exists(tempDownload))
|
||||
File.Delete(tempDownload);
|
||||
if (File.Exists(VRCX_Setup_Executable))
|
||||
File.Delete(VRCX_Setup_Executable);
|
||||
if (File.Exists(Update_Executable))
|
||||
Install();
|
||||
|
||||
if (File.Exists(HashLocation))
|
||||
File.Delete(HashLocation);
|
||||
if (File.Exists(TempDownload))
|
||||
File.Delete(TempDownload);
|
||||
if (File.Exists(VrcxSetupExecutable))
|
||||
File.Delete(VrcxSetupExecutable);
|
||||
|
||||
if (File.Exists(UpdateExecutable))
|
||||
InstallUpdate();
|
||||
}
|
||||
|
||||
private static void Install()
|
||||
private static void InstallUpdate()
|
||||
{
|
||||
var setupArguments = string.Empty;
|
||||
#if !LINUX
|
||||
if (Wine.GetIfWine())
|
||||
setupArguments += "/SKIP_SHORTCUT=true";
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(Update_Executable, VRCX_Setup_Executable);
|
||||
if (File.Exists(VrcxSetupExecutable))
|
||||
File.Delete(VrcxSetupExecutable);
|
||||
File.Move(UpdateExecutable, VrcxSetupExecutable);
|
||||
var vrcxProcess = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = VRCX_Setup_Executable,
|
||||
FileName = VrcxSetupExecutable,
|
||||
Arguments = setupArguments,
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Program.AppDataDirectory
|
||||
@@ -57,15 +97,19 @@ namespace VRCX
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MessageBox.Show(e.ToString(), "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
var message = $"Failed to install the update: {e.Message}";
|
||||
logger.Info(message);
|
||||
#if !LINUX
|
||||
MessageBox.Show(message, "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static void DownloadInstallRedist()
|
||||
public static async Task DownloadInstallRedist()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = DownloadFile("https://aka.ms/vs/17/release/vc_redist.x64.exe");
|
||||
var filePath = await DownloadFile("https://aka.ms/vs/17/release/vc_redist.x64.exe");
|
||||
var installRedist = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
@@ -75,55 +119,31 @@ namespace VRCX
|
||||
}
|
||||
};
|
||||
installRedist.Start();
|
||||
installRedist.WaitForExit();
|
||||
await installRedist.WaitForExitAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MessageBox.Show(e.ToString(), "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
var message = $"Failed to download and install the Visual C++ Redistributable: {e.Message}";
|
||||
logger.Info(message);
|
||||
#if !LINUX
|
||||
MessageBox.Show(message, "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static string DownloadFile(string fileUrl)
|
||||
private static async Task<string> DownloadFile(string fileUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var httpClientHandler = new HttpClientHandler();
|
||||
if (WebApi.ProxySet)
|
||||
httpClientHandler.Proxy = WebApi.Proxy;
|
||||
|
||||
var httpClient = new HttpClient(httpClientHandler);
|
||||
var response = await httpClient.GetAsync(fileUrl, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new Exception($"Failed to download the file. Status code: {response.StatusCode}");
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = httpClient.GetAsync(fileUrl).Result;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string fileName = GetFileNameFromContentDisposition(response);
|
||||
|
||||
string tempPath = Path.Combine(Path.GetTempPath(), "VRCX");
|
||||
Directory.CreateDirectory(tempPath);
|
||||
|
||||
string filePath = Path.Combine(tempPath, fileName);
|
||||
|
||||
using (FileStream fileStream = File.Create(filePath))
|
||||
{
|
||||
response.Content.CopyToAsync(fileStream).Wait();
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Failed to download the file. Status code: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error downloading the file: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.Dispose();
|
||||
}
|
||||
var fileName = GetFileNameFromContentDisposition(response);
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), "VRCX");
|
||||
Directory.CreateDirectory(tempPath);
|
||||
var filePath = Path.Combine(tempPath, fileName);
|
||||
await using var fileStream = File.Create(filePath);
|
||||
await response.Content.CopyToAsync(fileStream, cancellationToken);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private static string GetFileNameFromContentDisposition(HttpResponseMessage response)
|
||||
@@ -135,7 +155,7 @@ namespace VRCX
|
||||
if (startIndex >= 0)
|
||||
{
|
||||
startIndex += "filename=".Length;
|
||||
int endIndex = contentDisposition.IndexOf(';', startIndex);
|
||||
int endIndex = contentDisposition.IndexOf(";", startIndex, StringComparison.Ordinal);
|
||||
if (endIndex == -1)
|
||||
{
|
||||
endIndex = contentDisposition.Length;
|
||||
@@ -148,5 +168,138 @@ namespace VRCX
|
||||
|
||||
throw new Exception("Unable to extract file name from content-disposition header.");
|
||||
}
|
||||
|
||||
public static async Task DownloadUpdate(string fileUrl, string fileName, string hashUrl, int downloadSize)
|
||||
{
|
||||
_cancellationToken = CancellationToken.None;
|
||||
const int chunkSize = 8192;
|
||||
|
||||
if (File.Exists(TempDownload))
|
||||
File.Delete(TempDownload);
|
||||
if (File.Exists(HashLocation))
|
||||
File.Delete(HashLocation);
|
||||
|
||||
var hashesPath = await DownloadFile(hashUrl, _cancellationToken);
|
||||
if (!string.IsNullOrEmpty(hashesPath))
|
||||
File.Move(hashesPath, HashLocation);
|
||||
|
||||
await using var destination = File.OpenWrite(TempDownload);
|
||||
using (var response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, _cancellationToken))
|
||||
await using (var download = await response.Content.ReadAsStreamAsync(_cancellationToken))
|
||||
{
|
||||
var contentLength = response.Content.Headers.ContentLength;
|
||||
var buffer = new byte[chunkSize];
|
||||
long totalBytesRead = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int bytesRead = await download.ReadAsync(buffer, 0, chunkSize, _cancellationToken);
|
||||
if (bytesRead == 0) break;
|
||||
|
||||
if (_cancellationToken.IsCancellationRequested)
|
||||
throw new OperationCanceledException("Download was cancelled.");
|
||||
|
||||
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), _cancellationToken);
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
if (contentLength.HasValue)
|
||||
{
|
||||
double percentage = Math.Round((double)totalBytesRead / contentLength.Value * 100, 2);
|
||||
UpdateProgress = (int)percentage;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentLength.HasValue)
|
||||
{
|
||||
double percentage = Math.Round((double)totalBytesRead / contentLength.Value * 100, 2);
|
||||
UpdateProgress = (int)percentage;
|
||||
}
|
||||
}
|
||||
destination.Close();
|
||||
|
||||
var data = new FileInfo(TempDownload);
|
||||
if (data.Length != downloadSize)
|
||||
{
|
||||
File.Delete(TempDownload);
|
||||
logger.Error("Downloaded file size does not match expected size");
|
||||
throw new Exception("Downloaded file size does not match expected size");
|
||||
}
|
||||
if (File.Exists(HashLocation))
|
||||
{
|
||||
logger.Info("Checking hash");
|
||||
var lines = await File.ReadAllLinesAsync(HashLocation, _cancellationToken);
|
||||
var hashDict = new Dictionary<string, string>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var split = line.Split(' ');
|
||||
if (split.Length == 3)
|
||||
hashDict[split[2]] = split[0];
|
||||
}
|
||||
using (var sha256 = SHA256.Create())
|
||||
await using (var stream = File.OpenRead(TempDownload))
|
||||
{
|
||||
var hashBytes = await sha256.ComputeHashAsync(stream, _cancellationToken);
|
||||
var hashString = BitConverter.ToString(hashBytes).Replace("-", "");
|
||||
if (!hashDict.TryGetValue(fileName, out var expectedHash))
|
||||
{
|
||||
logger.Error("Hash check failed, file not found in hash file");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(expectedHash) &&
|
||||
!hashString.Equals(expectedHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.Error($"Hash check failed file:{hashString} web:{expectedHash}");
|
||||
throw new Exception("Hash check failed");
|
||||
// can't delete file yet because it's in use
|
||||
}
|
||||
}
|
||||
File.Delete(HashLocation);
|
||||
logger.Info("Hash check passed");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(AppImagePath))
|
||||
{
|
||||
// Windows
|
||||
if (File.Exists(UpdateExecutable))
|
||||
File.Delete(UpdateExecutable);
|
||||
File.Move(TempDownload, UpdateExecutable);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Linux
|
||||
if (File.Exists(AppImagePathOld))
|
||||
File.Delete(AppImagePathOld);
|
||||
File.Move(AppImagePath, AppImagePathOld);
|
||||
File.Move(TempDownload, AppImagePath);
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "chmod",
|
||||
Arguments = $"+x {AppImagePath}"
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
await process.WaitForExitAsync();
|
||||
}
|
||||
|
||||
UpdateProgress = 0;
|
||||
_cancellationToken = CancellationToken.None;
|
||||
}
|
||||
|
||||
public static async Task CancelUpdate()
|
||||
{
|
||||
_cancellationToken = new CancellationToken(true);
|
||||
UpdateProgress = 0;
|
||||
await Task.Delay(100);
|
||||
try
|
||||
{
|
||||
if (File.Exists(TempDownload))
|
||||
File.Delete(TempDownload);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user