доработаный
This commit is contained in:
+363
-134
@@ -11,149 +11,378 @@ 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";
|
||||
private const string BashUrl =
|
||||
"https://api.kirieska.xyz/files/SPOTX/spotx.sh";
|
||||
|
||||
public Task<int> InstallAsync(InstallOptions o,
|
||||
IProgress<(int Percent, string Status)> progress,
|
||||
IProgress<string> log, CancellationToken ct = default)
|
||||
=> RunAsync(o, isUninstall: false, progress, log, ct);
|
||||
private const string PsUrl =
|
||||
"https://api.kirieska.xyz/files/SPOTX/run.ps1";
|
||||
|
||||
public Task<int> UninstallAsync(
|
||||
IProgress<(int Percent, string Status)> progress,
|
||||
IProgress<string> log, CancellationToken ct = default)
|
||||
=> RunAsync(new InstallOptions(false, false, false, false),
|
||||
isUninstall: true, progress, log, ct);
|
||||
private const string PsUninstallUrl =
|
||||
"https://api.kirieska.xyz/files/SPOTX/uninstall.ps1";
|
||||
|
||||
private async Task<int> RunAsync(InstallOptions o, bool isUninstall,
|
||||
IProgress<(int, string)> progress, IProgress<string> log, CancellationToken ct)
|
||||
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)
|
||||
{
|
||||
bool isWin = OperatingSystem.IsWindows();
|
||||
string url = isUninstall
|
||||
? (isWin ? PsUninstallUrl : BashUrl)
|
||||
: (isWin ? PsUrl : BashUrl);
|
||||
string ext = isWin ? ".ps1" : ".sh";
|
||||
string scriptPath = Path.Combine(Path.GetTempPath(), $"spotx_{Guid.NewGuid():N}{ext}");
|
||||
|
||||
try
|
||||
{
|
||||
// === ШАГ 1 (0–20%): скачивание ===
|
||||
progress.Report((3, "Загрузка скрипта..."));
|
||||
using (var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) })
|
||||
{
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd("SpotXInstaller/1.0");
|
||||
byte[] data = await http.GetByteArrayAsync(url, ct).ConfigureAwait(false);
|
||||
await File.WriteAllBytesAsync(scriptPath, data, ct).ConfigureAwait(false);
|
||||
}
|
||||
progress.Report((20, "Скрипт загружен"));
|
||||
|
||||
if (!isWin)
|
||||
{
|
||||
progress.Report((25, "Установка прав на исполнение..."));
|
||||
await ExecAsync("/bin/chmod", new[] { "+x", scriptPath }, _ => { }, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
progress.Report((30, isUninstall ? "Восстановление..." : "Запуск установки..."));
|
||||
|
||||
// === ШАГ 2: маппинг флагов ===
|
||||
string file;
|
||||
var argv = new List<string>();
|
||||
|
||||
if (isWin)
|
||||
{
|
||||
file = "powershell.exe";
|
||||
argv.AddRange(new[]
|
||||
{
|
||||
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||
"-File", scriptPath
|
||||
});
|
||||
|
||||
if (isUninstall)
|
||||
{
|
||||
// uninstall.ps1 не принимает флагов — просто отрабатывает
|
||||
}
|
||||
else
|
||||
{
|
||||
if (o.BlockUpdates) argv.Add("-block_update_on");
|
||||
if (o.HidePodcasts) argv.Add("-podcasts_off");
|
||||
if (o.HidePremium) argv.Add("-adsections_off");
|
||||
argv.Add("-no_pause"); // не ждать Enter в конце
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
file = "/bin/bash";
|
||||
argv.Add(scriptPath);
|
||||
|
||||
if (isUninstall)
|
||||
{
|
||||
argv.Add("--uninstall");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (o.BlockUpdates) argv.Add("-B");
|
||||
if (o.HidePodcasts) argv.Add("-h");
|
||||
if (o.HidePremium) argv.Add("-e");
|
||||
if (o.ClearCache) argv.Add("-c");
|
||||
}
|
||||
}
|
||||
|
||||
// === ШАГ 3 (30–95%): запуск + построчный стрим ===
|
||||
int cur = 30;
|
||||
var gate = new object();
|
||||
|
||||
int exit = await ExecAsync(file, argv.ToArray(), line =>
|
||||
{
|
||||
log.Report(line);
|
||||
lock (gate)
|
||||
{
|
||||
cur = Math.Min(cur + 2, 95);
|
||||
progress.Report((cur, isUninstall ? "Восстановление..." : "Установка..."));
|
||||
}
|
||||
}, ct).ConfigureAwait(false);
|
||||
|
||||
progress.Report((100, "Готово"));
|
||||
return exit;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (File.Exists(scriptPath)) File.Delete(scriptPath); } catch { }
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
$"Не удалось установить права на скрипт. Код: {chmodExit}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<int> ExecAsync(string file, string[] argv,
|
||||
Action<string> onLine, CancellationToken ct)
|
||||
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)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = file,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
foreach (var a in argv) psi.ArgumentList.Add(a);
|
||||
if (options.BlockUpdates)
|
||||
arguments.Add("-block_update_on");
|
||||
|
||||
var proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
||||
proc.OutputDataReceived += (_, e) => { if (e.Data is { } s) onLine(s); };
|
||||
proc.ErrorDataReceived += (_, e) => { if (e.Data is { } s) onLine(s); };
|
||||
proc.Exited += (_, _) =>
|
||||
{
|
||||
try { tcs.TrySetResult(proc.ExitCode); }
|
||||
finally { proc.Dispose(); }
|
||||
};
|
||||
if (options.HidePodcasts)
|
||||
arguments.Add("-podcasts_off");
|
||||
|
||||
using var reg = ct.Register(() =>
|
||||
{
|
||||
try { if (!proc.HasExited) proc.Kill(entireProcessTree: true); }
|
||||
catch { }
|
||||
});
|
||||
if (options.HidePremium)
|
||||
arguments.Add("-adsections_off");
|
||||
|
||||
proc.Start();
|
||||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
return tcs.Task;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user