diff --git a/src/c#/GeneralUpdate.Core/Download/DownloadManager.cs b/src/c#/GeneralUpdate.Core/Download/DownloadManager.cs deleted file mode 100644 index 09129fe1..00000000 --- a/src/c#/GeneralUpdate.Core/Download/DownloadManager.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; - -namespace GeneralUpdate.Core.Download -{ - [Obsolete("Use IDownloadOrchestrator + DefaultDownloadOrchestrator instead. Will be removed in v11.")] - public class DownloadManager(string path, string format, int timeOut) - { - #region Private Members - - private readonly ImmutableList.Builder _downloadTasksBuilder = ImmutableList.Create().ToBuilder(); - private ImmutableList _downloadTasks; - - #endregion Private Members - - #region Public Properties - - public List<(object, string)> FailedVersions { get; } = new(); - - public string Path => path; - - public string Format => format; - - public int TimeOut => timeOut; - - private ImmutableList DownloadTasks => _downloadTasks ?? _downloadTasksBuilder.ToImmutable(); - - public event EventHandler MultiAllDownloadCompleted; - public event EventHandler MultiDownloadCompleted; - public event EventHandler MultiDownloadError; - public event EventHandler MultiDownloadStatistics; - - #endregion Public Properties - - #region Public Methods - - public async Task LaunchTasksAsync() - { - try - { - var downloadTasks = DownloadTasks.Select(task => task.LaunchAsync()).ToList(); - await Task.WhenAll(downloadTasks); - MultiAllDownloadCompleted?.Invoke(this, new MultiAllDownloadCompletedEventArgs(true, FailedVersions)); - } - catch (Exception ex) - { - MultiAllDownloadCompleted?.Invoke(this, new MultiAllDownloadCompletedEventArgs(false, FailedVersions)); - throw new Exception($"Download manager error: {ex.Message}", ex); - } - } - - public void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs e) - => MultiDownloadStatistics?.Invoke(this, e); - - public void OnMultiAsyncCompleted(object sender, MultiDownloadCompletedEventArgs e) - => MultiDownloadCompleted?.Invoke(this, e); - - public void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs e) - { - MultiDownloadError?.Invoke(this, e); - FailedVersions.Add((e.Version, e.Exception.Message)); - } - - public void Add(DownloadTask task) - { - Debug.Assert(task != null); - if (!_downloadTasksBuilder.Contains(task)) - { - _downloadTasksBuilder.Add(task); - } - } - - #endregion Public Methods - } -} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Core/Download/DownloadTask.cs b/src/c#/GeneralUpdate.Core/Download/DownloadTask.cs deleted file mode 100644 index dd894bc7..00000000 --- a/src/c#/GeneralUpdate.Core/Download/DownloadTask.cs +++ /dev/null @@ -1,305 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; - -namespace GeneralUpdate.Core.Download -{ - public class DownloadTask - { - #region Private Members - - private readonly HttpClient _httpClient; - private readonly DownloadManager _manager; - private readonly VersionInfo? _version; - private Timer? _timer; - private DateTime _startTime; - private long _receivedBytes; - private long _totalBytes; - private long _currentBytes; - - #endregion Private Members - - public DownloadTask(DownloadManager manager, VersionInfo version) - { - _manager = manager; - _version = version; - _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(_manager.TimeOut) }; - - // Set authentication headers if provided - if (!string.IsNullOrEmpty(version?.AuthScheme) && !string.IsNullOrEmpty(version?.AuthToken)) - { - _httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue(version.AuthScheme, version.AuthToken); - } - - _timer = new Timer(_=> Statistics(), null, 0, 1000); - } - - public async Task LaunchAsync() - { - try - { - var path = Path.Combine(_manager.Path, $"{_version?.Name}{_manager.Format}"); - await DownloadFileRangeAsync(_version.Url, path); - } - catch (Exception exception) - { - GeneralTracer.Error("The LaunchAsync method in the DownloadTask class throws an exception." , exception); - _manager.OnMultiDownloadError(this, new MultiDownloadErrorEventArgs(exception, _version)); - } - } - - #region Private Methods - - private async Task DownloadFileRangeAsync(string url, string path) - { - try - { - var tempPath = path + ".temp"; - var startPos = CheckFile(tempPath); - - var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - if (startPos > 0) - { - requestMessage.Headers.Range = new RangeHeaderValue(startPos, null); - } - - using var response = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead); - if (!response.IsSuccessStatusCode) - throw new HttpRequestException($"Failed to download file: {response.ReasonPhrase}"); - - var totalBytes = response.Content.Headers.ContentLength ?? 0; - Interlocked.Exchange(ref _totalBytes, totalBytes); - - if (startPos >= totalBytes) - { - if (File.Exists(path)) - { - File.SetAttributes(path, FileAttributes.Normal); - File.Delete(path); - } - - File.Move(tempPath, path); - OnDownloadCompleted(true); - return; - } - - await foreach (var chunk in DownloadChunksAsync(response)) - { - await WriteFileAsync(tempPath, chunk, totalBytes); - } - } - catch (Exception exception) - { - OnDownloadCompleted(false); - GeneralTracer.Error("The DownloadFileRangeAsync method in the DownloadTask class throws an exception." , exception); - _manager.OnMultiDownloadError(this, new MultiDownloadErrorEventArgs(exception, _version)); - } - } - - private async IAsyncEnumerable DownloadChunksAsync(HttpResponseMessage response) - { - using var responseStream = await response.Content.ReadAsStreamAsync(); - var buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - var chunk = new byte[bytesRead]; - Buffer.BlockCopy(buffer, 0, chunk, 0, bytesRead); - yield return chunk; - } - } - - private async Task WriteFileAsync(string tempPath, byte[] chunk, long totalBytes) - { - try - { - using var fileStream = new FileStream(tempPath, FileMode.Append, FileAccess.Write, FileShare.None); - await fileStream.WriteAsync(chunk, 0, chunk.Length); - Interlocked.Add(ref _receivedBytes, chunk.Length); - if (_receivedBytes >= totalBytes) - { - fileStream.Close(); - var path = tempPath.Replace(".temp", ""); - if (File.Exists(path)) - { - File.SetAttributes(path, FileAttributes.Normal); - File.Delete(path); - } - - File.Move(tempPath, path); - OnDownloadCompleted(true); - } - } - catch (Exception exception) - { - OnDownloadCompleted(false); - GeneralTracer.Error("The WriteFileAsync method in the DownloadTask class throws an exception." , exception); - _manager.OnMultiDownloadError(this, new MultiDownloadErrorEventArgs(exception, _version)); - } - } - - private void Statistics() - { - try - { - var interval = DateTime.Now - _startTime; - var tempTotalBytes = Interlocked.Read(ref _totalBytes); - //Accumulate the downloaded size. - var tempReceivedBytes = Interlocked.Read(ref _receivedBytes); - //Current downloaded size. - var tempCurrentBytes = tempReceivedBytes - Interlocked.Read(ref _currentBytes); - var speed = CalculateDownloadSpeed(tempCurrentBytes, interval); - var formatSpeed = FormatDownloadSpeed(speed); - var remainingTime = CalculateRemainingTime(tempTotalBytes, tempReceivedBytes, speed); - var progress = CalculateDownloadProgress(tempTotalBytes, tempReceivedBytes); - - var args = new MultiDownloadStatisticsEventArgs(_version - , remainingTime - , formatSpeed - , tempTotalBytes - , tempReceivedBytes - , progress); - _manager.OnMultiDownloadStatistics(this, args); - Interlocked.Exchange(ref _currentBytes, tempReceivedBytes); - _startTime = DateTime.Now; - } - catch (Exception exception) - { - _manager.OnMultiDownloadError(this, new MultiDownloadErrorEventArgs(exception, _version)); - } - } - - private void OnDownloadCompleted(bool isComplated) - { - try - { - DisposeTimer(); - var eventArgs = new MultiDownloadCompletedEventArgs(_version, isComplated); - _manager.OnMultiAsyncCompleted(this, eventArgs); - } - catch (Exception exception) - { - GeneralTracer.Error(exception.Message, exception); - _manager.OnMultiDownloadError(this, new MultiDownloadErrorEventArgs(exception, _version)); - } - } - - /// - /// Calculate the remaining download time (in seconds). - /// - /// - /// - /// - /// - private static TimeSpan CalculateRemainingTime(long totalBytes, long bytesReceived, double downloadSpeed) - { - if (downloadSpeed == 0) - { - return new TimeSpan(0, 0, 0, 0); - } - - var bytesRemaining = totalBytes - bytesReceived; - var secondsRemaining = bytesRemaining / downloadSpeed; - return TimeSpan.FromSeconds(secondsRemaining); - } - - /// - /// Calculate the download speed (in bytes per second). - /// - /// - /// - /// - private static double CalculateDownloadSpeed(long bytesReceived, TimeSpan elapsedTime) - { - if (elapsedTime.TotalSeconds == 0) - return 0; - - return bytesReceived / elapsedTime.TotalSeconds; - } - - /// - /// Calculate the download progress (percentage %). - /// - /// - /// - /// - private static double CalculateDownloadProgress(long totalBytes, long bytesReceived) - { - if (totalBytes == 0) - return 0; - - return (double)bytesReceived / totalBytes * 100; - } - - /// - /// Convert the download speed to an appropriate unit (B, KB, MB, GB). - /// - /// - /// - private static string FormatDownloadSpeed(double speedInBytesPerSecond) - { - const double kiloByte = 1024; - const double megaByte = kiloByte * 1024; - const double gigaByte = megaByte * 1024; - - return speedInBytesPerSecond switch - { - >= gigaByte => $"{speedInBytesPerSecond / gigaByte:F2} GB/s", - >= megaByte => $"{speedInBytesPerSecond / megaByte:F2} MB/s", - _ => speedInBytesPerSecond >= kiloByte - ? $"{speedInBytesPerSecond / kiloByte:F2} KB/s" - : $"{speedInBytesPerSecond:F2} B/s" - }; - } - - /// - /// Get the size of the downloaded file for resuming interrupted downloads. - /// - /// - /// - private static long CheckFile(string tempPath) - { - long startPos = 0; - if (!File.Exists(tempPath)) return startPos; - using var fileStream = File.OpenWrite(tempPath); - startPos = fileStream.Length; - fileStream.Seek(startPos, SeekOrigin.Current); - return startPos; - } - - private void DisposeTimer() - { - if (_timer == null) return; - try - { - _timer.Change(Timeout.Infinite, Timeout.Infinite); - using var waitHandle = new ManualResetEvent(false); - _timer.Dispose(waitHandle); - waitHandle.WaitOne(); - } - catch (ObjectDisposedException exception) - { - GeneralTracer.Error("Timer has already been disposed: " + exception.Message); - _manager.OnMultiDownloadError(this, new MultiDownloadErrorEventArgs(exception, _version)); - } - catch (Exception exception) - { - GeneralTracer.Error("An error occurred while disposing the timer.", exception); - _manager.OnMultiDownloadError(this, new MultiDownloadErrorEventArgs(exception, _version)); - } - finally - { - _timer = null; - } - } - - #endregion Private Methods - } -} diff --git a/src/c#/GeneralUpdate.Core/Silent/SilentUpdateMode.cs b/src/c#/GeneralUpdate.Core/Silent/SilentUpdateMode.cs deleted file mode 100644 index e28b4479..00000000 --- a/src/c#/GeneralUpdate.Core/Silent/SilentUpdateMode.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using GeneralUpdate.Core.Strategy; -using GeneralUpdate.Core.Download; -using GeneralUpdate.Core.FileSystem; -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.JsonContext; -using GeneralUpdate.Core.Strategy; -using GeneralUpdate.Core; -using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.Configuration; -using GeneralUpdate.Core.Network; - -namespace GeneralUpdate.Core; - -[Obsolete("Use SilentPollOrchestrator instead. Will be removed in v11.")] -internal sealed class SilentUpdateMode -{ - private const string ProcessInfoEnvironmentKey = "ProcessInfo"; - private static readonly TimeSpan PollingInterval = TimeSpan.FromMinutes(20); - private readonly GlobalConfigInfo _configInfo; - private readonly Encoding _encoding; - private readonly string _format; - private readonly int _downloadTimeOut; - private readonly bool _patchEnabled; - private readonly bool _backupEnabled; - private Task? _pollingTask; - private int _prepared; - private int _updaterStarted; - - public SilentUpdateMode(GlobalConfigInfo configInfo, Encoding encoding, string format, int downloadTimeOut, bool patchEnabled, bool backupEnabled) - { - _configInfo = configInfo; - _encoding = encoding; - _format = format; - _downloadTimeOut = downloadTimeOut; - _patchEnabled = patchEnabled; - _backupEnabled = backupEnabled; - } - - public Task StartAsync() - { - GeneralTracer.Info($"SilentUpdateMode.StartAsync: initializing silent update mode. PollingInterval={PollingInterval.TotalMinutes}min, BackupEnabled={_backupEnabled}, PatchEnabled={_patchEnabled}"); - AppDomain.CurrentDomain.ProcessExit += OnProcessExit; - _pollingTask = Task.Run(PollLoopAsync); - _pollingTask.ContinueWith(task => - { - if (task.Exception != null) - { - GeneralTracer.Error("The StartAsync method in SilentUpdateMode captured a polling exception.", task.Exception); - } - }, TaskContinuationOptions.OnlyOnFaulted); - GeneralTracer.Info("SilentUpdateMode.StartAsync: polling loop started, returning to caller."); - return Task.CompletedTask; - } - - private async Task PollLoopAsync() - { - GeneralTracer.Info("SilentUpdateMode.PollLoopAsync: entering silent update polling loop."); - while (Volatile.Read(ref _prepared) == 0) - { - try - { - GeneralTracer.Info("SilentUpdateMode.PollLoopAsync: polling for available updates."); - await PrepareUpdateIfNeededAsync(); - } - catch (Exception exception) - { - GeneralTracer.Error("The PollLoopAsync method in SilentUpdateMode throws an exception.", exception); - } - - if (Volatile.Read(ref _prepared) == 1) - { - GeneralTracer.Info("SilentUpdateMode.PollLoopAsync: update preparation completed, exiting poll loop."); - break; - } - - GeneralTracer.Info($"SilentUpdateMode.PollLoopAsync: no update prepared this cycle, waiting {PollingInterval.TotalMinutes}min before next poll."); - await Task.Delay(PollingInterval); - } - } - - private async Task PrepareUpdateIfNeededAsync() - { - GeneralTracer.Info($"SilentUpdateMode.PrepareUpdateIfNeededAsync: validating version. UpdateUrl={_configInfo.UpdateUrl}, ClientVersion={_configInfo.ClientVersion}"); - var mainResp = await VersionService.Validate(_configInfo.UpdateUrl - , _configInfo.ClientVersion - , AppType.ClientApp - , _configInfo.AppSecretKey - , GetPlatform() - , _configInfo.ProductId - , _configInfo.Scheme - , _configInfo.Token); - - if (mainResp?.Code != 200 || mainResp.Body == null || mainResp.Body.Count == 0) - { - GeneralTracer.Info($"SilentUpdateMode.PrepareUpdateIfNeededAsync: no update available. ResponseCode={mainResp?.Code}, BodyCount={mainResp?.Body?.Count ?? 0}"); - return; - } - - var versions = mainResp.Body.OrderBy(x => x.ReleaseDate).ToList(); - var latestVersion = versions.Last().Version; - GeneralTracer.Info($"SilentUpdateMode.PrepareUpdateIfNeededAsync: {versions.Count} version(s) available. LatestVersion={latestVersion}"); - - if (CheckFail(latestVersion)) - { - GeneralTracer.Warn($"SilentUpdateMode.PrepareUpdateIfNeededAsync: latest version {latestVersion} matches or precedes a known failed upgrade, skipping."); - return; - } - - BlackListManager.Instance?.AddBlackFiles(_configInfo.BlackFiles); - BlackListManager.Instance?.AddBlackFormats(_configInfo.BlackFormats); - BlackListManager.Instance?.AddSkipDirectorys(_configInfo.SkipDirectorys); - - _configInfo.Encoding = _encoding; - _configInfo.Format = _format; - _configInfo.DownloadTimeOut = _downloadTimeOut; - _configInfo.PatchEnabled = _patchEnabled; - _configInfo.IsMainUpdate = true; - _configInfo.LastVersion = latestVersion; - _configInfo.UpdateVersions = versions; - _configInfo.TempPath = StorageManager.GetTempDirectory("main_temp"); - _configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath, $"{StorageManager.DirectoryName}{_configInfo.ClientVersion}"); - - if (_backupEnabled) - { - GeneralTracer.Info($"SilentUpdateMode.PrepareUpdateIfNeededAsync: backing up from {_configInfo.InstallPath} to {_configInfo.BackupDirectory}"); - StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory, BlackListManager.Instance.SkipDirectorys); - GeneralTracer.Info("SilentUpdateMode.PrepareUpdateIfNeededAsync: backup completed."); - } - - var processInfo = ConfigurationMapper.MapToProcessInfo( - _configInfo, - versions, - BlackListManager.Instance.BlackFormats.ToList(), - BlackListManager.Instance.BlackFiles.ToList(), - BlackListManager.Instance.SkipDirectorys.ToList()); - _configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, ProcessInfoJsonContext.Default.ProcessInfo); - - GeneralTracer.Info($"SilentUpdateMode.PrepareUpdateIfNeededAsync: starting download of {versions.Count} package(s)."); - var manager = new DownloadManager(_configInfo.TempPath, _configInfo.Format, _configInfo.DownloadTimeOut); - foreach (var versionInfo in _configInfo.UpdateVersions) - { - manager.Add(new DownloadTask(manager, versionInfo)); - } - await manager.LaunchTasksAsync(); - GeneralTracer.Info("SilentUpdateMode.PrepareUpdateIfNeededAsync: all packages downloaded."); - - GeneralTracer.Info("SilentUpdateMode.PrepareUpdateIfNeededAsync: executing update strategy."); - var strategy = CreateStrategy(); - strategy.Create(_configInfo); - await strategy.ExecuteAsync(); - GeneralTracer.Info("SilentUpdateMode.PrepareUpdateIfNeededAsync: update strategy executed, marking as prepared."); - - Interlocked.Exchange(ref _prepared, 1); - } - - private void OnProcessExit(object? sender, EventArgs e) - { - GeneralTracer.Info($"SilentUpdateMode.OnProcessExit: process exit detected. Prepared={Volatile.Read(ref _prepared)}, UpdaterStarted={Volatile.Read(ref _updaterStarted)}"); - if (Volatile.Read(ref _prepared) != 1 || Interlocked.Exchange(ref _updaterStarted, 1) == 1) - { - GeneralTracer.Info("SilentUpdateMode.OnProcessExit: silent updater launch skipped (not prepared or already started)."); - return; - } - - try - { - Environments.SetEnvironmentVariable(ProcessInfoEnvironmentKey, _configInfo.ProcessInfo); - var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); - GeneralTracer.Info($"SilentUpdateMode.OnProcessExit: launching silent updater. Path={updaterPath}"); - if (File.Exists(updaterPath)) - { - Process.Start(new ProcessStartInfo - { - UseShellExecute = true, - FileName = updaterPath - }); - GeneralTracer.Info("SilentUpdateMode.OnProcessExit: silent updater launched successfully."); - } - else - { - GeneralTracer.Warn($"SilentUpdateMode.OnProcessExit: updater not found at {updaterPath}, cannot launch."); - } - } - catch (Exception exception) - { - GeneralTracer.Error("The OnProcessExit method in SilentUpdateMode throws an exception.", exception); - } - } - - private IStrategy CreateStrategy() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return new WindowsStrategy(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return new LinuxStrategy(); - throw new PlatformNotSupportedException("The current operating system is not supported!"); - } - - private static int GetPlatform() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return PlatformType.Windows; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return PlatformType.Linux; - return -1; - } - - private static bool CheckFail(string version) - { - var fail = Environments.GetEnvironmentVariable("UpgradeFail"); - if (string.IsNullOrEmpty(fail) || string.IsNullOrEmpty(version)) - return false; - - var failVersion = new Version(fail); - var latestVersion = new Version(version); - return failVersion >= latestVersion; - } -} diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 860be9f8..79636488 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -96,8 +96,6 @@ public ClientUpdateStrategy UseCustomOption(Func func) private async Task ExecuteWorkflowAsync() { - // Silent mode �� delegate to SilentUpdateMode - // (encoding/format/timeout are read from _configInfo) var defaultEncoding = Encoding.UTF8; var defaultTimeout = 60; if (true /* silent check would read from options */) diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs index 5b316fd5..b87c8cbd 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs @@ -117,21 +117,23 @@ public void StartApp() private async Task DownloadVersionsAsync(List versions) { - var manager = new DownloadManager(_appPath, Format.ZIP, TimeOut); - foreach (var versionInfo in versions) + var assets = versions.Select(v => new Download.Models.DownloadAsset( + Name: v.PacketName ?? v.Version ?? "unknown", + Url: v.Url ?? string.Empty, + Size: 0, + SHA256: v.Hash, + Version: v.Version ?? "0.0.0" + )).ToList(); + + var plan = new Download.Models.DownloadPlan(assets, false); + + var httpClient = new System.Net.Http.HttpClient(); + try { - var version = new VersionInfo - { - Name = versionInfo.PacketName, - Version = versionInfo.Version, - Url = versionInfo.Url, - Format = Format.ZIP, - Hash = versionInfo.Hash - }; - manager.Add(new DownloadTask(manager, version)); + var orchestrator = new Download.Orchestrators.DefaultDownloadOrchestrator(httpClient); + await orchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false); } - - await manager.LaunchTasksAsync(); + finally { httpClient.Dispose(); } } private void Decompress(List versions)