Skip to content

Commit

Permalink
Replaced some utility methods with library methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaoticz committed Dec 31, 2024
1 parent 5653eb2 commit 1228f58
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 225 deletions.
156 changes: 0 additions & 156 deletions NadekoHub/Common/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ namespace NadekoHub.Common;
/// </summary>
internal static class Utilities
{
private static readonly string _programVerifier = (Environment.OSVersion.Platform is PlatformID.Win32NT) ? "where" : "which";
private static readonly string _envPathSeparator = (Environment.OSVersion.Platform is PlatformID.Win32NT) ? ";" : ":";
private static readonly EnvironmentVariableTarget _envTarget = (Environment.OSVersion.Platform is PlatformID.Win32NT)
? EnvironmentVariableTarget.User
: EnvironmentVariableTarget.Process;

/// <summary>
/// Loads an image embeded with this application.
/// </summary>
Expand All @@ -42,154 +36,4 @@ public static SKBitmap LoadLocalImage(string? uri = default)
? SKBitmap.Decode(uri)
: LoadEmbededImage(uri);
}

/// <summary>
/// Starts the specified program in the background.
/// </summary>
/// <param name="program">
/// The name of the program in the PATH environment variable,
/// or the absolute path to its executable.
/// </param>
/// <param name="arguments">The arguments to the program.</param>
/// <returns>The process of the specified program.</returns>
/// <exception cref="ArgumentException" />
/// <exception cref="ArgumentNullException" />
/// <exception cref="Win32Exception">Occurs when <paramref name="program"/> does not exist.</exception>
/// <exception cref="InvalidOperationException">Occurs when the process fails to execute.</exception>
public static Process StartProcess(string program, string arguments = "")
{
ArgumentException.ThrowIfNullOrEmpty(program, nameof(program));
ArgumentNullException.ThrowIfNull(arguments, nameof(arguments));

return Process.Start(new ProcessStartInfo()
{
FileName = program,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
}) ?? throw new InvalidOperationException($"Failed spawing process for: {program} {arguments}");
}

/// <summary>
/// Checks if a program exists.
/// </summary>
/// <param name="programName">The name of the program.</param>
/// <param name="cToken">The cancellation token.</param>
/// <returns><see langword="true"/> if the program exists, <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentException" />
/// <exception cref="ArgumentNullException" />
public static async ValueTask<bool> ProgramExistsAsync(string programName, CancellationToken cToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(programName, nameof(programName));

using var process = StartProcess(_programVerifier, programName);
return !string.IsNullOrWhiteSpace(await process.StandardOutput.ReadToEndAsync(cToken));
}

/// <summary>
/// Safely deletes a file.
/// </summary>
/// <param name="fileUri">The absolute path to the file.</param>
/// <returns><see langword="true"/> if the file was deleted, <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentException" />
/// <exception cref="ArgumentNullException" />
/// <exception cref="IOException" />
/// <exception cref="NotSupportedException" />
/// <exception cref="PathTooLongException" />
/// <exception cref="UnauthorizedAccessException" />
public static bool TryDeleteFile(string fileUri)
{
ArgumentException.ThrowIfNullOrEmpty(fileUri, nameof(fileUri));

if (!File.Exists(fileUri))
return false;

File.Delete(fileUri);
return true;
}

/// <summary>
/// Safely deletes a directory.
/// </summary>
/// <param name="directoryUri">The absolute path to the directory.</param>
/// <returns><see langword="true"/> if the directory was deleted, <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentException" />
/// <exception cref="ArgumentNullException" />
/// <exception cref="IOException" />
/// <exception cref="DirectoryNotFoundException" />
/// <exception cref="PathTooLongException" />
/// <exception cref="UnauthorizedAccessException" />
public static bool TryDeleteDirectory(string directoryUri)
{
ArgumentException.ThrowIfNullOrEmpty(directoryUri, nameof(directoryUri));

if (!Directory.Exists(directoryUri))
return false;

Directory.Delete(directoryUri, true);
return true;
}

/// <summary>
/// Checks if this application can write to <paramref name="directoryUri"/>.
/// </summary>
/// <param name="directoryUri">The absolute path to a directory.</param>
/// <returns><see langword="true"/> if writing is allowed, <see langword="false"/> otherwise.</returns>
/// <exception cref="PathTooLongException" />
/// <exception cref="DirectoryNotFoundException" />
public static bool CanWriteTo(string directoryUri)
{
var tempFileUri = Path.Combine(directoryUri, $"{Guid.NewGuid()}.tmp");

try
{
using var fileStream = File.Create(tempFileUri);
return true;
}
catch (UnauthorizedAccessException)
{
return false;
}
finally
{
TryDeleteFile(tempFileUri);
}
}

/// <summary>
/// Adds a directory path to the PATH environment variable.
/// </summary>
/// <param name="directoryUri">The absolute path to a directory.</param>
/// <remarks>
/// On Windows, this needs to be called once and the dependencies will be available for the user forever. <br />
/// On Unix systems, we can only add to the PATH on a process basis, so this needs to be called at least once everytime the application is opened.
/// </remarks>
/// <returns><see langword="true"/> if <paramref name="directoryUri"/> got successfully added to the PATH envar, <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentException" />
/// <exception cref="ArgumentNullException" />
public static bool AddPathToPathEnvar(string directoryUri)
{
ArgumentException.ThrowIfNullOrEmpty(directoryUri, nameof(directoryUri));

if (File.Exists(directoryUri))
throw new ArgumentException("Parameter must point to a directory, not a file.", nameof(directoryUri));

var envPathValue = Environment.GetEnvironmentVariable("PATH", _envTarget) ?? string.Empty;

// If directoryPath is already in the PATH envar, don't add it again.
if (envPathValue.Contains(directoryUri, StringComparison.Ordinal))
return false;

var newPathEnvValue = envPathValue + _envPathSeparator + directoryUri;

// Add path to Windows' user envar, so it persists across reboots.
if (Environment.OSVersion.Platform is PlatformID.Win32NT)
Environment.SetEnvironmentVariable("PATH", newPathEnvValue, EnvironmentVariableTarget.User);

// Add path to the current process' envar, so the updater can see the dependencies.
Environment.SetEnvironmentVariable("PATH", newPathEnvValue, EnvironmentVariableTarget.Process);

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Kotz.Utilities;

namespace NadekoHub.Features.AppConfig.Services.Abstractions;

/// <summary>
Expand All @@ -22,7 +24,7 @@ public abstract class FfmpegResolver : IFfmpegResolver
public virtual async ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)
{
// Check where ffmpeg is referenced.
using var whereProcess = Utilities.StartProcess(_programVerifier, FfmpegProcessName);
using var whereProcess = KotzUtilities.StartProcess(_programVerifier, FfmpegProcessName, true);
var installationPath = await whereProcess.StandardOutput.ReadToEndAsync(cToken);

// If ffmpeg is present but not managed by us, just report it is installed.
Expand All @@ -32,7 +34,7 @@ public abstract class FfmpegResolver : IFfmpegResolver
var currentVer = await GetCurrentVersionAsync(cToken);

// If ffmpeg or ffprobe are absent, a reinstall needs to be performed.
if (currentVer is null || !await Utilities.ProgramExistsAsync("ffprobe", cToken))
if (currentVer is null || !KotzUtilities.ProgramExists("ffprobe"))
return null;

var latestVer = await GetLatestVersionAsync(cToken);
Expand All @@ -44,20 +46,20 @@ public abstract class FfmpegResolver : IFfmpegResolver
public virtual async ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
{
// If ffmpeg is not accessible from the shell...
if (!await Utilities.ProgramExistsAsync(FfmpegProcessName, cToken))
if (!KotzUtilities.ProgramExists(FfmpegProcessName))
{
// And doesn't exist in the dependencies folder,
// report that ffmpeg is not installed.
if (!File.Exists(Path.Combine(AppStatics.AppDepsUri, FileName)))
if (!File.Exists(Path.Join(AppStatics.AppDepsUri, FileName)))
return null;

// Else, add the dependencies directory to the PATH envar,
// then try again.
Utilities.AddPathToPathEnvar(AppStatics.AppDepsUri);
KotzUtilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
return await GetCurrentVersionAsync(cToken);
}

using var ffmpeg = Utilities.StartProcess(FfmpegProcessName, "-version");
using var ffmpeg = KotzUtilities.StartProcess(FfmpegProcessName, "-version", true);
var match = AppStatics.FfmpegVersionRegex.Match(await ffmpeg.StandardOutput.ReadLineAsync(cToken) ?? string.Empty);

return match.Groups[1].Value;
Expand Down
5 changes: 3 additions & 2 deletions NadekoHub/Features/AppConfig/Services/AppConfigManager.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Kotz.Utilities;
using NadekoHub.Features.AppConfig.Models;
using NadekoHub.Features.AppConfig.Services.Abstractions;
using NadekoHub.Features.AppWindow.Models;
Expand Down Expand Up @@ -33,7 +34,7 @@ public async ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken =
var newId = CreateNewId();
var newPosition = (_appConfig.BotEntries.IsEmpty) ? 0 : _appConfig.BotEntries.Values.Max(x => x.Position) + 1;
var newBotName = "NewBot_" + newPosition;
var newEntry = new BotInstanceInfo(newBotName, Path.Combine(_appConfig.BotsDirectoryUri, newBotName), newPosition);
var newEntry = new BotInstanceInfo(newBotName, Path.Join(_appConfig.BotsDirectoryUri, newBotName), newPosition);

if (!_appConfig.BotEntries.TryAdd(newId, newEntry))
throw new InvalidOperationException($"Could not create a new bot entry with Id {newId}.");
Expand All @@ -49,7 +50,7 @@ public async ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken =
if (!_appConfig.BotEntries.TryRemove(id, out var removedEntry))
return null;

Utilities.TryDeleteDirectory(removedEntry.InstanceDirectoryUri);
KotzUtilities.TryDeleteDirectory(removedEntry.InstanceDirectoryUri);

await SaveAsync(cToken);

Expand Down
23 changes: 12 additions & 11 deletions NadekoHub/Features/AppConfig/Services/FfmpegLinuxResolver.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Kotz.Utilities;
using NadekoHub.Features.AppConfig.Services.Abstractions;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
Expand Down Expand Up @@ -64,8 +65,8 @@ public override async ValueTask<string> GetLatestVersionAsync(CancellationToken
if (currentVersion == newVersion)
return (currentVersion, null);

Utilities.TryDeleteFile(Path.Combine(installationUri, FileName));
Utilities.TryDeleteFile(Path.Combine(installationUri, "ffprobe"));
KotzUtilities.TryDeleteFile(Path.Join(installationUri, FileName));
KotzUtilities.TryDeleteFile(Path.Join(installationUri, "ffprobe"));
}

// Install
Expand All @@ -77,29 +78,29 @@ public override async ValueTask<string> GetLatestVersionAsync(CancellationToken
await using var downloadStream = await http.GetStreamAsync($"https://johnvansickle.com/ffmpeg/releases/{tarFileName}", cToken);

// Save tar file to the temporary directory.
var tarFilePath = Path.Combine(_tempDirectory, tarFileName);
var tarExtractDir = Path.Combine(_tempDirectory, $"ffmpeg-{newVersion}-{architecture}64-static");
var tarFilePath = Path.Join(_tempDirectory, tarFileName);
var tarExtractDir = Path.Join(_tempDirectory, $"ffmpeg-{newVersion}-{architecture}64-static");
await using (var fileStream = new FileStream(tarFilePath, FileMode.Create))
await downloadStream.CopyToAsync(fileStream, cToken);

// Extract the tar file.
using var extractProcess = Utilities.StartProcess("tar", $"xf \"{tarFilePath}\" --directory=\"{_tempDirectory}\"");
using var extractProcess = KotzUtilities.StartProcess("tar", ["xf", tarFilePath, $"--directory=\"{_tempDirectory}\""]);
await extractProcess.WaitForExitAsync(cToken);

// Move ffmpeg to the dependencies directory.
File.Move(Path.Combine(tarExtractDir, FileName), Path.Combine(installationUri, FileName), true);
File.Move(Path.Combine(tarExtractDir, "ffprobe"), Path.Combine(installationUri, "ffprobe"), true);
KotzUtilities.TryMoveFile(Path.Join(tarExtractDir, FileName), Path.Join(installationUri, FileName), true);
KotzUtilities.TryMoveFile(Path.Join(tarExtractDir, "ffprobe"), Path.Join(installationUri, "ffprobe"), true);

// Mark the files as executable.
using var chmod = Utilities.StartProcess("chmod", $"+x \"{Path.Combine(installationUri, FileName)}\" \"{Path.Combine(installationUri, "ffprobe")}\"");
using var chmod = KotzUtilities.StartProcess("chmod", ["+x", Path.Join(installationUri, FileName), Path.Join(installationUri, "ffprobe")]);
await chmod.WaitForExitAsync(cToken);

// Cleanup
File.Delete(tarFilePath);
Directory.Delete(tarExtractDir, true);
KotzUtilities.TryDeleteFile(tarFilePath);
KotzUtilities.TryDeleteDirectory(tarExtractDir);

// Update environment variable
Utilities.AddPathToPathEnvar(installationUri);
KotzUtilities.AddPathToPATHEnvar(installationUri);

_isUpdating = false;
return (currentVersion, newVersion);
Expand Down
23 changes: 12 additions & 11 deletions NadekoHub/Features/AppConfig/Services/FfmpegMacResolver.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Kotz.Utilities;
using NadekoHub.Features.AppConfig.Models.Api.Evermeet;
using NadekoHub.Features.AppConfig.Services.Abstractions;
using System.IO.Compression;
Expand All @@ -15,7 +16,7 @@ public sealed class FfmpegMacResolver : FfmpegResolver
private const string _apiFfmpegInfoEndpoint = "https://evermeet.cx/ffmpeg/info/ffmpeg/release";
private const string _apiFfprobeInfoEndpoint = "https://evermeet.cx/ffmpeg/info/ffprobe/release";
private readonly string _tempDirectory = Path.GetTempPath();
private bool _isUpdating = false;
private bool _isUpdating;
private readonly IHttpClientFactory _httpClientFactory;

/// <inheritdoc/>
Expand Down Expand Up @@ -57,8 +58,8 @@ public override async ValueTask<string> GetLatestVersionAsync(CancellationToken
return (currentVersion, null);
}

Utilities.TryDeleteFile(Path.Combine(installationUri, FileName));
Utilities.TryDeleteFile(Path.Combine(installationUri, "ffprobe"));
KotzUtilities.TryDeleteFile(Path.Join(installationUri, FileName));
KotzUtilities.TryDeleteFile(Path.Join(installationUri, "ffprobe"));
}

// Install
Expand All @@ -74,7 +75,7 @@ await Task.WhenAll(
);

// Update environment variable
Utilities.AddPathToPathEnvar(installationUri);
KotzUtilities.AddPathToPATHEnvar(installationUri);

_isUpdating = false;
return (currentVersion, newVersion);
Expand All @@ -91,26 +92,26 @@ private async Task InstallDependencyAsync(EvermeetInfo downloadInfo, string depe
var http = _httpClientFactory.CreateClient();
var downloadUrl = downloadInfo.Download["zip"].Url;
var zipFileName = downloadUrl[(downloadUrl.LastIndexOf('/') + 1)..];
var zipFilePath = Path.Combine(_tempDirectory, zipFileName);
var zipFilePath = Path.Join(_tempDirectory, zipFileName);

// Download the zip file and save it to the temporary directory.
using var zipStream = await http.GetStreamAsync(downloadUrl, cToken);
await using var zipStream = await http.GetStreamAsync(downloadUrl, cToken);

using (var fileStream = new FileStream(zipFilePath, FileMode.Create))
await using (var fileStream = new FileStream(zipFilePath, FileMode.Create))
await zipStream.CopyToAsync(fileStream, cToken);

// Extract the zip file.
ZipFile.ExtractToDirectory(zipFileName, _tempDirectory);

// Move the dependency binary.
var finalFileUri = Path.Combine(dependenciesUri, downloadInfo.Name);
File.Move(Path.Combine(_tempDirectory, downloadInfo.Name), finalFileUri, true);
var finalFileUri = Path.Join(dependenciesUri, downloadInfo.Name);
KotzUtilities.TryMoveFile(Path.Join(_tempDirectory, downloadInfo.Name), finalFileUri, true);

// Mark binary as executable.
using var chmod = Utilities.StartProcess("chmod", $"+x \"{finalFileUri}\"");
using var chmod = KotzUtilities.StartProcess("chmod", ["+x", finalFileUri]);
await chmod.WaitForExitAsync(cToken);

// Cleanup.
File.Delete(zipFilePath);
KotzUtilities.TryDeleteFile(zipFilePath);
}
}
Loading

0 comments on commit 1228f58

Please sign in to comment.