using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using SpotXInstaller.Models; namespace SpotXInstaller.Services; public sealed class InstallerService { private const string BashUrl = "https://api.kirieska.xyz/files/SPOTX/spotx.sh"; private const string PsUrl = "https://api.kirieska.xyz/files/SPOTX/run.ps1"; private const string PsUninstallUrl = "https://api.kirieska.xyz/files/SPOTX/uninstall.ps1"; public Task InstallAsync( InstallOptions options, IProgress<(int Percent, string Status)> progress, IProgress log, CancellationToken ct = default) { return RunAsync( options, isUninstall: false, progress, log, ct); } public Task UninstallAsync( IProgress<(int Percent, string Status)> progress, IProgress log, CancellationToken ct = default) { return RunAsync( new InstallOptions(false, false, false, false), isUninstall: true, progress, log, ct); } private async Task RunAsync( InstallOptions options, bool isUninstall, IProgress<(int Percent, string Status)> progress, IProgress log, CancellationToken ct) { bool isWindows = OperatingSystem.IsWindows(); string url = isUninstall ? (isWindows ? PsUninstallUrl : BashUrl) : (isWindows ? PsUrl : BashUrl); string extension = isWindows ? ".ps1" : ".sh"; string scriptPath = Path.Combine( Path.GetTempPath(), $"spotx_{Guid.NewGuid():N}{extension}"); try { ct.ThrowIfCancellationRequested(); // --------------------------------------------------------- // 1. DOWNLOAD // --------------------------------------------------------- progress.Report((5, "Загрузка скрипта...")); using (var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) }) { http.DefaultRequestHeaders.UserAgent.ParseAdd( "SpotXInstaller/1.0"); byte[] data = await http.GetByteArrayAsync( url, ct); await File.WriteAllBytesAsync( scriptPath, data, ct); } progress.Report((20, "Скрипт загружен")); ct.ThrowIfCancellationRequested(); // --------------------------------------------------------- // 2. UNIX PERMISSIONS // --------------------------------------------------------- if (!isWindows) { progress.Report( (24, "Подготовка скрипта...")); int chmodExit = await ExecAsync( "/bin/chmod", new[] { "+x", scriptPath }, _ => { }, ct); if (chmodExit != 0) { throw new InvalidOperationException( $"Не удалось установить права на скрипт. Код: {chmodExit}"); } } ct.ThrowIfCancellationRequested(); // --------------------------------------------------------- // 3. BUILD COMMAND // --------------------------------------------------------- string file; var arguments = new List(); if (isWindows) { file = "powershell.exe"; arguments.Add("-NoProfile"); arguments.Add("-ExecutionPolicy"); arguments.Add("Bypass"); arguments.Add("-File"); arguments.Add(scriptPath); if (!isUninstall) { if (options.BlockUpdates) arguments.Add("-block_update_on"); if (options.HidePodcasts) arguments.Add("-podcasts_off"); if (options.HidePremium) arguments.Add("-adsections_off"); arguments.Add("-no_pause"); } } else { file = "/bin/bash"; arguments.Add(scriptPath); if (isUninstall) { arguments.Add("--uninstall"); } else { if (options.BlockUpdates) arguments.Add("-B"); if (options.HidePodcasts) arguments.Add("-h"); if (options.HidePremium) arguments.Add("-e"); if (options.ClearCache) arguments.Add("-c"); } } // --------------------------------------------------------- // 4. RUN // --------------------------------------------------------- progress.Report(( 30, isUninstall ? "Запуск восстановления..." : "Запуск установки...")); int exitCode = await ExecAsync( file, arguments.ToArray(), line => { if (string.IsNullOrWhiteSpace(line)) return; log.Report(line); string lower = line.ToLowerInvariant(); // Никакого фальшивого +2%. // Только изменение текста статуса по реальному выводу. if (lower.Contains("download") || lower.Contains("скач")) { progress.Report( (40, "Загрузка Spotify...")); } else if (lower.Contains("install") || lower.Contains("установ")) { progress.Report( (60, "Установка Spotify...")); } else if (lower.Contains("patch") || lower.Contains("патч")) { progress.Report( (80, "Применение SpotX...")); } else if (lower.Contains("complete") || lower.Contains("готов")) { progress.Report( (90, "Завершение...")); } }, ct); ct.ThrowIfCancellationRequested(); // --------------------------------------------------------- // 5. RESULT // --------------------------------------------------------- if (exitCode == 0) { progress.Report( (100, isUninstall ? "Восстановление завершено" : "Установка завершена")); } else { progress.Report( (100, $"Ошибка установки (код {exitCode})")); } return exitCode; } finally { try { if (File.Exists(scriptPath)) File.Delete(scriptPath); } catch { // Игнорируем ошибку очистки временного файла. } } } private static async Task ExecAsync( string file, string[] arguments, Action onLine, CancellationToken ct) { var tcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); var psi = new ProcessStartInfo { FileName = file, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; foreach (string argument in arguments) psi.ArgumentList.Add(argument); var process = new Process { StartInfo = psi, EnableRaisingEvents = true }; int finished = 0; process.OutputDataReceived += (_, e) => { if (e.Data is not null) onLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data is not null) onLine(e.Data); }; process.Exited += (_, _) => { if (Interlocked.Exchange(ref finished, 1) != 0) return; try { tcs.TrySetResult(process.ExitCode); } catch { tcs.TrySetResult(-1); } finally { process.Dispose(); } }; using CancellationTokenRegistration registration = ct.Register(() => { if (Interlocked.Exchange(ref finished, 1) != 0) return; try { if (!process.HasExited) { process.Kill( entireProcessTree: true); } } catch { // Процесс уже мог завершиться. } tcs.TrySetCanceled(ct); }); try { ct.ThrowIfCancellationRequested(); if (!process.Start()) throw new InvalidOperationException( $"Не удалось запустить процесс: {file}"); process.BeginOutputReadLine(); process.BeginErrorReadLine(); return await tcs.Task.ConfigureAwait(false); } catch { try { if (!process.HasExited) process.Kill(entireProcessTree: true); } catch { // Ignore. } throw; } } }