Files
VRCX/Dotnet/Update.cs
2026-01-12 11:14:33 +13:00

271 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
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
{
public class Update
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly string VrcxSetupExecutable = Path.Join(Program.AppDataDirectory, "VRCX_Setup.exe");
private static readonly string UpdateExecutable = Path.Join(Program.AppDataDirectory, "update.exe");
private static readonly string TempDownload = Path.Join(Program.AppDataDirectory, "tempDownload");
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);
if (File.Exists(TempDownload))
File.Delete(TempDownload);
if (File.Exists(VrcxSetupExecutable))
File.Delete(VrcxSetupExecutable);
if (File.Exists(UpdateExecutable))
InstallUpdate();
}
private static void InstallUpdate()
{
var setupArguments = string.Empty;
try
{
if (File.Exists(VrcxSetupExecutable))
File.Delete(VrcxSetupExecutable);
File.Move(UpdateExecutable, VrcxSetupExecutable);
var vrcxProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = VrcxSetupExecutable,
Arguments = setupArguments,
UseShellExecute = true,
WorkingDirectory = Program.AppDataDirectory
}
};
vrcxProcess.Start();
Environment.Exit(0);
}
catch (Exception e)
{
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 async Task DownloadInstallRedist()
{
try
{
var filePath = await DownloadFile("https://aka.ms/vs/17/release/vc_redist.x64.exe");
var installRedist = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = filePath,
Arguments = "/install /quiet /norestart"
}
};
installRedist.Start();
await installRedist.WaitForExitAsync();
}
catch (Exception e)
{
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 async Task<string> DownloadFile(string fileUrl, CancellationToken cancellationToken = default)
{
var response = await httpClient.GetAsync(fileUrl, cancellationToken);
if (!response.IsSuccessStatusCode)
throw new Exception($"Failed to download the file. Status code: {response.StatusCode}");
var fileName = GetFileNameFromContentDisposition(response);
var tempPath = Path.Join(Path.GetTempPath(), "VRCX");
Directory.CreateDirectory(tempPath);
var filePath = Path.Join(tempPath, fileName);
await using var fileStream = File.Create(filePath);
await response.Content.CopyToAsync(fileStream, cancellationToken);
return filePath;
}
private static string GetFileNameFromContentDisposition(HttpResponseMessage response)
{
string contentDisposition = response.Content.Headers.ContentDisposition?.ToString();
if (contentDisposition != null)
{
int startIndex = contentDisposition.IndexOf("filename=", StringComparison.OrdinalIgnoreCase);
if (startIndex >= 0)
{
startIndex += "filename=".Length;
int endIndex = contentDisposition.IndexOf(";", startIndex, StringComparison.Ordinal);
if (endIndex == -1)
{
endIndex = contentDisposition.Length;
}
string fileName = contentDisposition.Substring(startIndex, endIndex - startIndex).Trim(' ', '"');
return fileName;
}
}
throw new Exception("Unable to extract file name from content-disposition header.");
}
public static async Task DownloadUpdate(string fileUrl, string hashString, int downloadSize)
{
_cancellationToken = CancellationToken.None;
const int chunkSize = 8192;
if (File.Exists(TempDownload))
File.Delete(TempDownload);
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)
{
if (contentLength.HasValue)
{
var percentage = Math.Round((double)totalBytesRead / contentLength.Value * 100, 2);
UpdateProgress = (int)percentage;
}
var bytesRead = await download.ReadAsync(buffer.AsMemory(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;
}
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 (string.IsNullOrEmpty(hashString))
{
logger.Error("Hash string is empty, skipping hash check");
}
else
{
logger.Info("Checking hash");
using (var sha256 = SHA256.Create())
await using (var stream = File.OpenRead(TempDownload))
{
var fileHashBytes = await sha256.ComputeHashAsync(stream, _cancellationToken);
var fileHashString = Convert.ToHexString(fileHashBytes);
if (!hashString.Equals(fileHashString, StringComparison.OrdinalIgnoreCase))
{
logger.Error($"Hash check failed file:{fileHashString} web:{hashString}");
throw new Exception("Hash check failed");
// can't delete file yet because it's in use
}
}
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
}
}
}
}