Files
spotx/Services/InstallerService.cs
T
2026-09-14 21:02:45 +02:00

389 lines
12 KiB
C#

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<int> InstallAsync(
InstallOptions options,
IProgress<(int Percent, string Status)> progress,
IProgress<string> log,
CancellationToken ct = default)
{
return RunAsync(
options,
isUninstall: false,
progress,
log,
ct);
}
public Task<int> UninstallAsync(
IProgress<(int Percent, string Status)> progress,
IProgress<string> log,
CancellationToken ct = default)
{
return RunAsync(
new InstallOptions(false, false, false, false),
isUninstall: true,
progress,
log,
ct);
}
private async Task<int> RunAsync(
InstallOptions options,
bool isUninstall,
IProgress<(int Percent, string Status)> progress,
IProgress<string> 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<string>();
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<int> ExecAsync(
string file,
string[] arguments,
Action<string> onLine,
CancellationToken ct)
{
var tcs =
new TaskCompletionSource<int>(
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;
}
}
}