LANCommander/LANCommander.Steam/Services/SteamCmdService.cs
2026-05-07 21:01:01 -05:00

853 lines
No EOL
31 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using LANCommander.Steam.Abstractions;
using LANCommander.Steam.Enums;
using LANCommander.Steam.Events;
using LANCommander.Steam.Models;
using LANCommander.Steam.Options;
namespace LANCommander.Steam.Services;
/// <summary>
/// Service for interacting with SteamCMD.
/// Can be constructed without dependency injection using optional parameters.
/// </summary>
public class SteamCmdService(
SteamCmdOptions? options = null,
ISteamCmdProfileStore? profileStore = null,
ILogger<SteamCmdService>? logger = null)
: ISteamCmdService
{
private readonly ILogger<SteamCmdService>? _logger = logger;
private readonly SteamCmdOptions _options = options ?? new SteamCmdOptions();
private readonly CancellationTokenSource _cancellationTokenSource = new();
private string? _executablePath;
/// <summary>
/// Event fired when an install job status changes
/// </summary>
public event EventHandler<SteamCmdInstallStatusEventArgs>? InstallStatusChanged;
/// <summary>
/// Event fired when install progress is updated
/// </summary>
public event EventHandler<SteamCmdInstallProgressEventArgs>? InstallProgress;
/// <summary>
/// Get or set the SteamCMD executable path
/// </summary>
public string? ExecutablePath
{
get => _executablePath ?? _options.ExecutablePath;
set => _executablePath = value;
}
public async Task<SteamCmdConnectionStatus> GetConnectionStatusAsync(string username)
{
try
{
if (!IsValidUsername(username))
throw new ArgumentException("Invalid username", nameof(username));
// Auto-populate SteamCMD path if not configured
if (string.IsNullOrWhiteSpace(ExecutablePath))
{
if (_options.AutoDetectPath)
{
var detectedPath = await AutoDetectSteamCmdPathAsync();
if (!string.IsNullOrWhiteSpace(detectedPath))
{
ExecutablePath = detectedPath;
_logger?.LogInformation("Auto-detected SteamCMD at: {Path}", detectedPath);
}
else
{
_logger?.LogWarning("SteamCMD path is not configured and could not be auto-detected");
return SteamCmdConnectionStatus.NotInstalled;
}
}
else
{
_logger?.LogWarning("SteamCMD path is not configured and auto-detection is disabled");
return SteamCmdConnectionStatus.NotInstalled;
}
}
if (!File.Exists(ExecutablePath))
{
_logger?.LogWarning("SteamCMD executable not found at configured path: {Path}", ExecutablePath);
return SteamCmdConnectionStatus.NotInstalled;
}
// Try to run steamcmd with +quit to check if it's working
var result = await ExecuteSteamCmdCommandAsync("+quit");
if (result.Success)
{
// Check if we're logged in by trying to get user info
var loginResult = await ExecuteSteamCmdCommandAsync($"+login {username} +quit", TimeSpan.FromSeconds(30));
return loginResult.Success ? SteamCmdConnectionStatus.Authenticated : SteamCmdConnectionStatus.Unauthenticated;
}
return SteamCmdConnectionStatus.NotInstalled;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error checking SteamCMD connection status");
return SteamCmdConnectionStatus.NotInstalled;
}
}
public async Task<string> AutoDetectSteamCmdPathAsync()
{
var possiblePaths = new List<string>();
// Windows paths
if (OperatingSystem.IsWindows())
{
// Common Steam installation directories
var steamPaths = new[]
{
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Steam", "steamcmd.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Steam", "steamcmd.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Steam", "steamcmd.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Steam", "steamcmd.exe"),
"C:\\Steam\\steamcmd.exe",
"C:\\Program Files\\Steam\\steamcmd.exe",
"C:\\Program Files (x86)\\Steam\\steamcmd.exe"
};
possiblePaths.AddRange(steamPaths);
// Check PATH environment variable
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [];
foreach (var dir in pathDirs)
{
if (!string.IsNullOrWhiteSpace(dir))
{
possiblePaths.Add(Path.Combine(dir, "steamcmd.exe"));
}
}
}
// Linux paths
else if (OperatingSystem.IsLinux())
{
possiblePaths.AddRange(new[]
{
"/app/Data/Steam/steamcmd.sh",
"/usr/local/bin/steamcmd",
"/usr/bin/steamcmd",
"/home/steam/steamcmd/steamcmd.sh",
"/opt/steamcmd/steamcmd.sh",
"/var/lib/steam/steamcmd/steamcmd.sh"
});
// Check PATH environment variable
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(':') ?? [];
foreach (var dir in pathDirs)
{
if (!string.IsNullOrWhiteSpace(dir))
{
possiblePaths.Add(Path.Combine(dir, "steamcmd"));
}
}
}
// macOS paths
else if (OperatingSystem.IsMacOS())
{
possiblePaths.AddRange(new[]
{
"/usr/local/bin/steamcmd",
"/opt/homebrew/bin/steamcmd",
"/Applications/Steam.app/Contents/MacOS/steamcmd"
});
// Check PATH environment variable
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(':') ?? [];
foreach (var dir in pathDirs)
{
if (!string.IsNullOrWhiteSpace(dir))
{
possiblePaths.Add(Path.Combine(dir, "steamcmd"));
}
}
}
// Check each possible path
foreach (var path in possiblePaths)
{
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
try
{
// Verify it's actually SteamCMD by checking if it responds to --version or +quit
var testResult = await ExecuteSteamCmdCommandAsync("+quit", TimeSpan.FromSeconds(30), path);
if (testResult.Success)
{
_logger?.LogDebug("Found SteamCMD at: {Path}", path);
return path;
}
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "Failed to verify SteamCMD at: {Path}", path);
}
}
}
_logger?.LogWarning("SteamCMD not found in common installation locations");
return string.Empty;
}
public async Task<SteamCmdStatus> LoginToSteamAsync(string username, string? password = null)
{
try
{
if (!IsValidUsername(username))
throw new ArgumentException("Invalid username", nameof(username));
if (string.IsNullOrWhiteSpace(ExecutablePath))
{
_logger?.LogError("SteamCMD path is not configured");
return SteamCmdStatus.PathNotConfigured;
}
if (!File.Exists(ExecutablePath))
{
_logger?.LogError("SteamCMD executable not found at configured path: {Path}", ExecutablePath);
return SteamCmdStatus.ExecutableNotFound;
}
var loginCommand = string.IsNullOrWhiteSpace(password)
? $"+login {username}"
: $"+login {username} {password}";
var result = await ExecuteSteamCmdCommandAsync($"{loginCommand} +quit", TimeSpan.FromSeconds(120));
if (result.Output.Contains("Invalid Password"))
return SteamCmdStatus.InvalidPassword;
if (result.Success)
{
_logger?.LogInformation("Successfully logged into Steam as {Username}", username);
return SteamCmdStatus.Success;
}
_logger?.LogError("Failed to log into Steam: {Error}", result.ErrorOutput);
return SteamCmdStatus.Error;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error logging into Steam");
return SteamCmdStatus.UnknownError;
}
}
public async Task<SteamCmdStatus> LogoutAsync(string username)
{
try
{
if (!IsValidUsername(username))
return SteamCmdStatus.InvalidUsername;
if (string.IsNullOrWhiteSpace(ExecutablePath))
{
_logger?.LogError("SteamCMD path is not configured");
return SteamCmdStatus.PathNotConfigured;
}
if (!File.Exists(ExecutablePath))
{
_logger?.LogError("SteamCMD executable not found at configured path: {Path}", ExecutablePath);
return SteamCmdStatus.ExecutableNotFound;
}
var logoutCommand = $"+logout {username}";
var result = await ExecuteSteamCmdCommandAsync($"{logoutCommand} +quit");
if (result.Success)
{
_logger?.LogInformation("Successfully logged out of the Steam account {Username}", username);
return SteamCmdStatus.Success;
}
_logger?.LogError("Failed to log out of Steam: {Error}", result.ErrorOutput);
return SteamCmdStatus.Error;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error logging out of Steam");
return SteamCmdStatus.UnknownError;
}
}
public async Task InstallContentAsync(uint appId, string installDirectory, string? username = null)
{
if (!string.IsNullOrWhiteSpace(username) && !IsValidUsername(username))
throw new ArgumentException("Invalid username", nameof(username));
if (string.IsNullOrWhiteSpace(ExecutablePath))
{
_logger?.LogError("SteamCMD path is not configured");
throw new InvalidOperationException("SteamCMD path is not configured");
}
if (!File.Exists(ExecutablePath))
{
_logger?.LogError("SteamCMD executable not found at configured path: {Path}", ExecutablePath);
throw new FileNotFoundException("SteamCMD executable not found", ExecutablePath);
}
Directory.CreateDirectory(installDirectory);
var commands = new List<string>();
if (!string.IsNullOrWhiteSpace(username))
commands.Add($"+login {username}");
else
commands.Add("+login anonymous");
commands.Add($"+force_install_dir \"{installDirectory}\"");
commands.Add($"+app_update {appId} validate");
commands.Add("+quit");
// Create install job
var job = new SteamCmdInstallJob
{
AppId = appId,
InstallDirectory = installDirectory,
Username = username,
Status = SteamCmdInstallStatus.InProgress,
StatusMessage = "Starting installation...",
CompletionSource = new TaskCompletionSource<SteamCmdStatus>()
};
OnInstallStatusChanged(job, SteamCmdInstallStatus.InProgress);
try
{
var result =
await ExecuteSteamCmdCommandWithProgressAsync(string.Join(" ", commands), job,
_cancellationTokenSource.Token);
if (result.Success)
{
job.Status = SteamCmdInstallStatus.Completed;
job.StatusMessage = "Installation completed sucessfully";
job.Progress = 100;
job.CompletedAt = DateTime.UtcNow;
job.CompletionSource.TrySetResult(SteamCmdStatus.Success);
OnInstallStatusChanged(job, SteamCmdInstallStatus.Completed);
OnInstallProgress(job, 100, "Installation completed successfully");
_logger?.LogInformation("Successfully installed Steam app {AppId} to {InstallDirectory}", job.AppId,
job.InstallDirectory);
}
else
{
job.Status = SteamCmdInstallStatus.Failed;
job.StatusMessage = $"Installation failed: {result.ErrorOutput}";
job.ErrorMessage = result.ErrorOutput;
job.CompletedAt = DateTime.UtcNow;
job.CompletionSource?.TrySetResult(SteamCmdStatus.Error);
OnInstallStatusChanged(job, SteamCmdInstallStatus.Failed, result.ErrorOutput);
_logger?.LogError("Failed to install Steam app {AppId}: {Error}", job.AppId, result.ErrorOutput);
}
}
catch (Exception ex)
{
job.Status = SteamCmdInstallStatus.Failed;
job.StatusMessage = $"Installation failed: {ex.Message}";
job.ErrorMessage = ex.Message;
job.CompletedAt = DateTime.UtcNow;
job.CompletionSource?.TrySetException(ex);
OnInstallStatusChanged(job, SteamCmdInstallStatus.Failed, ex.Message);
_logger?.LogError(ex, "Error installing Steam content for app {AppId}", job.AppId);
}
}
public async Task<SteamCmdStatus> RemoveContentAsync(string installDirectory)
{
try
{
if (string.IsNullOrWhiteSpace(installDirectory))
{
_logger?.LogError("Install directory is not specified");
return SteamCmdStatus.InstallDirectoryNotFound;
}
if (!Directory.Exists(installDirectory))
{
_logger?.LogWarning("Install directory does not exist: {InstallDirectory}", installDirectory);
return SteamCmdStatus.Success; // Consider it already removed
}
// Remove the directory and all its contents
Directory.Delete(installDirectory, true);
_logger?.LogInformation("Successfully removed content from {InstallDirectory}", installDirectory);
return SteamCmdStatus.Success;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error removing content from {InstallDirectory}", installDirectory);
return SteamCmdStatus.UnknownError;
}
}
public async Task<IEnumerable<SteamCmdProfile>> GetProfilesAsync()
{
if (profileStore == null)
{
throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation.");
}
return await profileStore.GetAllAsync();
}
public async Task<SteamCmdProfile?> GetProfileAsync(string username)
{
if (profileStore == null)
{
throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation.");
}
return await profileStore.GetByUsernameAsync(username);
}
public async Task SaveProfileAsync(SteamCmdProfile profile)
{
if (profileStore == null)
{
throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation.");
}
if (profile == null)
{
throw new ArgumentNullException(nameof(profile));
}
if (string.IsNullOrWhiteSpace(profile.Username))
{
throw new ArgumentException("Profile username cannot be empty", nameof(profile));
}
await profileStore.SaveAsync(profile);
_logger?.LogInformation("Saved profile for username: {Username}", profile.Username);
}
public async Task DeleteProfileAsync(string username)
{
if (profileStore == null)
{
throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation.");
}
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("Username cannot be empty", nameof(username));
}
await profileStore.DeleteAsync(username);
_logger?.LogInformation("Deleted profile for username: {Username}", username);
}
/// <inheritdoc />
public async Task<SteamCmdAppInfo?> GetAppInfoAsync(uint appId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(ExecutablePath))
{
if (_options.AutoDetectPath)
{
var detected = await AutoDetectSteamCmdPathAsync();
if (!string.IsNullOrEmpty(detected))
ExecutablePath = detected;
}
if (string.IsNullOrWhiteSpace(ExecutablePath))
{
_logger?.LogWarning("SteamCMD path is not configured; cannot get app changenumber.");
return null;
}
}
if (!File.Exists(ExecutablePath))
{
_logger?.LogWarning("SteamCMD executable not found at {Path}", ExecutablePath);
return null;
}
// +quit can suppress output on some SteamCMD versions; we parse both stdout and stderr
var arguments = $"+login anonymous +app_info_update 1 +app_info_print {appId} +quit";
var result = await ExecuteSteamCmdCommandAsync(arguments, TimeSpan.FromSeconds(60));
var combined = (result.Output + "\n" + result.ErrorOutput);
var info = ParseAppInfoPrintOutput(appId, combined);
if (info != null)
_logger?.LogDebug("SteamCMD app info for {AppId}: changenumber={Changenumber}", appId, info.Changenumber);
return info;
}
private static SteamCmdAppInfo? ParseAppInfoPrintOutput(uint appId, string output)
{
// VDF-like output: "buildid" "3936003" and "timeupdated" "1560990358" (often under "public" branch)
var buildIdMatch = Regex.Match(output, @"""buildid""\s+""([^""]+)""", RegexOptions.IgnoreCase);
var timeUpdatedMatch = Regex.Match(output, @"""timeupdated""\s+""([^""]+)""", RegexOptions.IgnoreCase);
if (!buildIdMatch.Success)
return null;
var changenumber = buildIdMatch.Groups[1].Value.Trim();
if (string.IsNullOrEmpty(changenumber))
return null;
DateTimeOffset? timeUpdated = null;
if (timeUpdatedMatch.Success && long.TryParse(timeUpdatedMatch.Groups[1].Value.Trim(), out var unixSeconds))
timeUpdated = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
return new SteamCmdAppInfo
{
AppId = appId,
Changenumber = changenumber,
TimeUpdated = timeUpdated
};
}
private async Task<SteamCmdResult> ExecuteSteamCmdCommandWithProgressAsync(
string arguments,
SteamCmdInstallJob? job = null,
CancellationToken cancellationToken = default)
{
try
{
if (string.IsNullOrWhiteSpace(ExecutablePath))
{
throw new InvalidOperationException("SteamCMD executable path is not configured");
}
var processStartInfo = new ProcessStartInfo
{
FileName = ExecutablePath,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using var process = new Process { StartInfo = processStartInfo };
var output = new StringBuilder();
var error = new StringBuilder();
process.Start();
// Read streams with \r and \n as line delimiters so we get progress updates in real time.
// SteamCMD uses \r for progress lines and full buffering when piped; BeginOutputReadLine
// only fires on \n, so output would otherwise appear only at the end.
var readStdOut = ReadStreamByLineAsync(
process.StandardOutput.BaseStream,
Encoding.UTF8,
line =>
{
if (string.IsNullOrEmpty(line))
return;
output.AppendLine(line);
_logger?.LogDebug("SteamCMD Output: {Output}", line);
if (job != null)
ParseProgressFromOutput(line, job);
},
cancellationToken);
var readStdErr = ReadStreamByLineAsync(
process.StandardError.BaseStream,
Encoding.UTF8,
line =>
{
if (string.IsNullOrEmpty(line))
return;
error.AppendLine(line);
_logger?.LogDebug("SteamCMD Error: {Error}", line);
if (job != null)
ParseProgressFromOutput(line, job);
},
cancellationToken);
try
{
await Task.WhenAll(
readStdOut,
readStdErr,
process.WaitForExitAsync(cancellationToken));
}
catch (OperationCanceledException)
{
_logger?.LogWarning("SteamCMD operation was cancelled");
process.Kill();
throw;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error while executing SteamCMD");
}
return new SteamCmdResult
{
Success = process.ExitCode == 0,
ExitCode = process.ExitCode,
Output = output.ToString(),
ErrorOutput = error.ToString()
};
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error executing SteamCMD command: {Arguments}", arguments);
return new SteamCmdResult
{
Success = false,
ExitCode = -1,
ErrorOutput = ex.Message
};
}
}
/// <summary>
/// Reads a stream and invokes the callback for each line. Lines are split on \r and \n
/// so that progress output (often \r-only) is delivered in real time instead of buffered.
/// </summary>
private static async Task ReadStreamByLineAsync(
Stream stream,
Encoding encoding,
Action<string> onLine,
CancellationToken cancellationToken = default)
{
using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var lineBuilder = new StringBuilder();
var charBuffer = new char[256];
int read;
while ((read = await reader.ReadAsync(charBuffer.AsMemory(), cancellationToken).ConfigureAwait(false)) > 0)
{
for (var i = 0; i < read; i++)
{
var ch = charBuffer[i];
if (ch == '\r' || ch == '\n')
{
if (lineBuilder.Length > 0)
{
onLine(lineBuilder.ToString());
lineBuilder.Clear();
}
}
else
{
lineBuilder.Append(ch);
}
}
}
if (lineBuilder.Length > 0)
onLine(lineBuilder.ToString());
}
private void ParseProgressFromOutput(string line, SteamCmdInstallJob job)
{
// SteamCMD progress patterns:
// "Downloading update (X of Y MB)"
// "Update state (X% at Y MB/s) : downloading update"
// "Success. App 'X' fully installed."
// "Installing update..."
var progressMatch = Regex.Match(line, @"Update state \(([\d.]+)%", RegexOptions.IgnoreCase);
if (progressMatch.Success && double.TryParse(progressMatch.Groups[1].Value, out var progress))
{
job.Progress = Math.Min(100, Math.Max(0, progress));
// Try to extract download speed
var speedMatch = Regex.Match(line, @"at ([\d.]+) (MB|KB)/s", RegexOptions.IgnoreCase);
if (speedMatch.Success && double.TryParse(speedMatch.Groups[1].Value, out var speed))
{
var unit = speedMatch.Groups[2].Value.ToUpper();
job.BytesPerSecond = (long)(speed * (unit == "MB" ? 1048576 : 1024));
}
OnInstallProgress(job, job.Progress, line, job.BytesDownloaded, job.BytesTotal, job.BytesPerSecond);
}
// Extract download progress (X of Y MB)
var downloadMatch = Regex.Match(line, @"Downloading update \(([\d.]+) of ([\d.]+) (MB|KB)\)", RegexOptions.IgnoreCase);
if (downloadMatch.Success)
{
if (double.TryParse(downloadMatch.Groups[1].Value, out var downloaded) &&
double.TryParse(downloadMatch.Groups[2].Value, out var total))
{
var unit = downloadMatch.Groups[3].Value.ToUpper();
var multiplier = unit == "MB" ? 1048576 : 1024;
job.BytesDownloaded = (long)(downloaded * multiplier);
job.BytesTotal = (long)(total * multiplier);
if (job.BytesTotal > 0)
{
job.Progress = (job.BytesDownloaded * 100.0) / job.BytesTotal;
}
OnInstallProgress(job, job.Progress, line, job.BytesDownloaded, job.BytesTotal, job.BytesPerSecond);
}
}
// Update status message
if (!string.IsNullOrWhiteSpace(line) && !line.Contains("Update state"))
{
job.StatusMessage = line.Trim();
}
}
private void OnInstallStatusChanged(SteamCmdInstallJob job, SteamCmdInstallStatus status, string? errorMessage = null)
{
try
{
InstallStatusChanged?.Invoke(this, new SteamCmdInstallStatusEventArgs(job, status, errorMessage));
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error invoking InstallStatusChanged event");
}
}
private void OnInstallProgress(SteamCmdInstallJob job, double progress, string statusMessage, long bytesDownloaded = 0, long bytesTotal = 0, long bytesPerSecond = 0)
{
try
{
InstallProgress?.Invoke(this, new SteamCmdInstallProgressEventArgs(job, progress, statusMessage, bytesDownloaded, bytesTotal, bytesPerSecond));
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error invoking InstallProgress event");
}
}
private async Task<SteamCmdResult> ExecuteSteamCmdCommandAsync(string arguments, TimeSpan? timeout = null, string? steamCmdPath = null)
{
try
{
var executablePath = string.IsNullOrWhiteSpace(steamCmdPath) ? ExecutablePath : steamCmdPath;
if (string.IsNullOrWhiteSpace(executablePath))
{
throw new InvalidOperationException("SteamCMD executable path is not configured");
}
var processStartInfo = new ProcessStartInfo
{
FileName = executablePath,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using var process = new Process { StartInfo = processStartInfo };
var output = new StringBuilder();
var error = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
output.AppendLine(e.Data);
_logger?.LogDebug("SteamCMD Output: {Output}", e.Data);
}
};
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
error.AppendLine(e.Data);
_logger?.LogDebug("SteamCMD Error: {Error}", e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
try
{
if (timeout.HasValue)
await process.WaitForExitAsync().WaitAsync(timeout.Value);
else
await process.WaitForExitAsync();
}
catch (TimeoutException)
{
_logger?.LogWarning("SteamCMD timed out");
process.Kill();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error while executing SteamCMD");
}
return new SteamCmdResult
{
Success = process.ExitCode == 0,
ExitCode = process.ExitCode,
Output = output.ToString(),
ErrorOutput = error.ToString()
};
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error executing SteamCMD command: {Arguments}", arguments);
return new SteamCmdResult
{
Success = false,
ExitCode = -1,
ErrorOutput = ex.Message
};
}
}
private static bool IsValidUsername(string value)
{
return !string.IsNullOrEmpty(value) &&
Regex.IsMatch(value, @"^[a-zA-Z0-9_]+$");
}
private class SteamCmdResult
{
public bool Success { get; set; }
public int ExitCode { get; set; }
public string Output { get; set; } = string.Empty;
public string ErrorOutput { get; set; } = string.Empty;
}
}