368 lines
12 KiB
C#
368 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)
|
|
{
|
|
// IMPORTANT:
|
|
// SpotX asks interactively about these options when
|
|
// neither switch is supplied. Since our GUI has no
|
|
// interactive PowerShell input, always provide an
|
|
// explicit answer.
|
|
|
|
if (options.BlockUpdates)
|
|
arguments.Add("-block_update_on");
|
|
else
|
|
arguments.Add("-block_update_off");
|
|
|
|
if (options.HidePodcasts)
|
|
arguments.Add("-podcasts_off");
|
|
else
|
|
arguments.Add("-podcasts_on");
|
|
|
|
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();
|
|
|
|
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 psi = new ProcessStartInfo
|
|
{
|
|
FileName = file,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
foreach (string argument in arguments)
|
|
psi.ArgumentList.Add(argument);
|
|
|
|
using var process = new Process
|
|
{
|
|
StartInfo = psi
|
|
};
|
|
|
|
process.OutputDataReceived += (_, e) =>
|
|
{
|
|
if (e.Data is not null)
|
|
onLine(e.Data);
|
|
};
|
|
|
|
process.ErrorDataReceived += (_, e) =>
|
|
{
|
|
if (e.Data is not null)
|
|
onLine(e.Data);
|
|
};
|
|
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
if (!process.Start())
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Не удалось запустить процесс: {file}");
|
|
}
|
|
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
|
|
try
|
|
{
|
|
await process.WaitForExitAsync(ct)
|
|
.ConfigureAwait(false);
|
|
|
|
return process.ExitCode;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
try
|
|
{
|
|
if (!process.HasExited)
|
|
{
|
|
process.Kill(
|
|
entireProcessTree: true);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Процесс уже мог завершиться.
|
|
}
|
|
|
|
try
|
|
{
|
|
await process.WaitForExitAsync()
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore.
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
}
|