diff --git a/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs b/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs index 5c345585..007bdfaa 100644 --- a/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs +++ b/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs @@ -5,6 +5,9 @@ using LANCommander.SDK.PowerShell; using LANCommander.SDK.Providers; using LANCommander.SDK.Rpc.Client; using LANCommander.SDK.Services; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Extensions; +using LANCommander.Steam.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using RpcSubscriber = LANCommander.SDK.Rpc.Clients.RpcSubscriber; diff --git a/LANCommander.SDK/LANCommander.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj index b6ef3335..144e1235 100644 --- a/LANCommander.SDK/LANCommander.SDK.csproj +++ b/LANCommander.SDK/LANCommander.SDK.csproj @@ -32,6 +32,7 @@ + @@ -50,5 +51,9 @@ \ + + + + diff --git a/LANCommander.SDK/Models/Settings/Settings.cs b/LANCommander.SDK/Models/Settings/Settings.cs index f6381278..2bc9fe84 100644 --- a/LANCommander.SDK/Models/Settings/Settings.cs +++ b/LANCommander.SDK/Models/Settings/Settings.cs @@ -15,6 +15,7 @@ public class Settings public DebugSettings Debug { get; set; } = new(); public UpdateSettings Updates { get; set; } = new(); public IPXRelaySettings IPXRelay { get; set; } = new(); + public SteamSettings Steam { get; set; } = new(); public string Culture { get; set; } = "en-US"; } \ No newline at end of file diff --git a/LANCommander.SDK/Models/Settings/SteamSettings.cs b/LANCommander.SDK/Models/Settings/SteamSettings.cs new file mode 100644 index 00000000..285dd92e --- /dev/null +++ b/LANCommander.SDK/Models/Settings/SteamSettings.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using LANCommander.Steam.Models; + +namespace LANCommander.SDK.Models; + +public class SteamSettings +{ + public string Path { get; set; } = string.Empty; + public string InstallDirectory { get; set; } = ""; + public ICollection Profiles { get; set; } = []; +} \ No newline at end of file diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Connect-SteamCmd.cs b/LANCommander.SDK/PowerShell/Cmdlets/Connect-SteamCmd.cs new file mode 100644 index 00000000..8bedd856 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Connect-SteamCmd.cs @@ -0,0 +1,63 @@ +using System; +using System.Management.Automation; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Enums; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommunications.Connect, "SteamCmd")] +[OutputType(typeof(SteamCmdStatus))] +[GenerateBindings] +public partial class ConnectSteamCmdCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public string Username { get; set; } = string.Empty; + + [Parameter(Mandatory = false)] + public SecureString? Password { get; set; } + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken token) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + string? password = null; + if (Password != null) + { + var ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(Password); + try + { + password = System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr); + } + finally + { + System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr); + } + } + + var status = await _steamCmdService.LoginToSteamAsync(Username, password); + + WriteObject(status); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "LoginError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Convert-AspectRatio.cs b/LANCommander.SDK/PowerShell/Cmdlets/Convert-AspectRatio.cs index f4651e2d..19284047 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Convert-AspectRatio.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Convert-AspectRatio.cs @@ -11,7 +11,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets [Cmdlet(VerbsData.Convert, "AspectRatio")] [OutputType(typeof(string))] - public class ConvertAspectRatioCmdlet : BaseCmdlet + public class ConvertAspectRatioCmdlet : Cmdlet { [Parameter(Mandatory = true, Position = 0)] public int Width { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs b/LANCommander.SDK/PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs index b40172e2..2d6f1b76 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs @@ -11,7 +11,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsData.ConvertFrom, "SerializedBase64")] [OutputType(typeof(object))] - public class ConvertFromSerializedBase64Cmdlet : BaseCmdlet + public class ConvertFromSerializedBase64Cmdlet : Cmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public string Input { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs b/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs index b4a94e25..20540080 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs @@ -12,7 +12,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsData.ConvertTo, "SerializedBase64")] [OutputType(typeof(object))] - public class ConvertToSerializedBase64Cmdlet : BaseCmdlet + public class ConvertToSerializedBase64Cmdlet : Cmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public object Input { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-StringBytes.cs b/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-StringBytes.cs index cccf4e47..f5425fca 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-StringBytes.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-StringBytes.cs @@ -6,7 +6,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsData.ConvertTo, "StringBytes")] [OutputType(typeof(byte[]))] - public class ConvertToStringBytesCmdlet : BaseCmdlet + public class ConvertToStringBytesCmdlet : Cmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public string Input { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Disconnect-SteamCmd.cs b/LANCommander.SDK/PowerShell/Cmdlets/Disconnect-SteamCmd.cs new file mode 100644 index 00000000..a64a6deb --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Disconnect-SteamCmd.cs @@ -0,0 +1,45 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Enums; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommunications.Disconnect, "SteamCmd")] +[OutputType(typeof(SteamCmdStatus))] +[GenerateBindings] +public partial class DisconnectSteamCmdCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public string Username { get; set; } = string.Empty; + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var status = await _steamCmdService.LogoutAsync(Username); + + WriteObject(status); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "LogoutError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchBinary.cs b/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchBinary.cs index d1a40028..e716dc39 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchBinary.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchBinary.cs @@ -5,7 +5,7 @@ using System.Management.Automation; namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsData.Edit, "PatchBinary")] - public class EditPatchBinaryCmdlet : BaseCmdlet + public class EditPatchBinaryCmdlet : Cmdlet { [Parameter(Mandatory = true, Position = 0)] public long Offset { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs b/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs index df2f8f57..17ea61d2 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.FileSystemGlobbing.Abstractions; namespace LANCommander.SDK.PowerShell.Cmdlets; [Cmdlet(VerbsData.Edit, "PatchGameSpy")] -public class EditPatchGameSpy : BaseCmdlet +public class EditPatchGameSpy : Cmdlet { private const string GAMESPY_HOSTNAME = "gamespy.com"; private const string GAMESPY_PUBLICKEY = "BF05D63E93751AD4A59A4A7389CF0BE8A22CCDEEA1E7F12C062D6E194472EFDA5184CCECEB4FBADF5EB1D7ABFE91181453972AA971F624AF9BA8F0F82E2869FB7D44BDE8D56EE50977898F3FEE75869622C4981F07506248BD3D092E8EA05C12B2FA37881176084C8F8B8756C4722CDC57D2AD28ACD3AD85934FB48D6B2D2027"; diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-GameManifest.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-GameManifest.cs index 4c31bb2f..bdd9fcb6 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Get-GameManifest.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-GameManifest.cs @@ -7,7 +7,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsCommon.Get, "GameManifest")] [OutputType(typeof(SDK.Models.Manifest.Game))] - public class GetGameManifestCmdlet : BaseCmdlet + public class GetGameManifestCmdlet : Cmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public string Path { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-HorizontalFov.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-HorizontalFov.cs index bea55755..1dfe0420 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Get-HorizontalFov.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-HorizontalFov.cs @@ -9,7 +9,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsCommon.Get, "VerticalFov")] [OutputType(typeof(string))] - public class GetHorizontalFovCmdlet : BaseCmdlet + public class GetHorizontalFovCmdlet : Cmdlet { [Parameter] public int Width { get; set; } = 0; [Parameter] public int Height { get; set; } = 0; diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-PrimaryDisplay.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-PrimaryDisplay.cs index 2af029e9..ffa2abcf 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Get-PrimaryDisplay.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-PrimaryDisplay.cs @@ -8,7 +8,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsCommon.Get, "PrimaryDisplay")] [OutputType(typeof(string))] - public class GetPrimaryDisplayCmdlet : BaseCmdlet + public class GetPrimaryDisplayCmdlet : Cmdlet { protected override void ProcessRecord() { diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdConnectionStatus.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdConnectionStatus.cs new file mode 100644 index 00000000..86bd6791 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdConnectionStatus.cs @@ -0,0 +1,44 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Enums; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamCmdConnectionStatus")] +[OutputType(typeof(SteamCmdConnectionStatus))] +[GenerateBindings] +public partial class GetSteamCmdConnectionStatusCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public string Username { get; set; } = string.Empty; + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var status = await _steamCmdService.GetConnectionStatusAsync(Username); + WriteObject(status); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetConnectionStatusError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdPath.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdPath.cs new file mode 100644 index 00000000..dda05aa2 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdPath.cs @@ -0,0 +1,43 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamCmdPath")] +[OutputType(typeof(string))] +[GenerateBindings] +public partial class GetSteamCmdPathCmdlet : DependencyCmdlet +{ + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var path = await _steamCmdService.AutoDetectSteamCmdPathAsync(); + if (!string.IsNullOrEmpty(path)) + { + WriteObject(path); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "AutoDetectPathError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfile.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfile.cs new file mode 100644 index 00000000..d2486772 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfile.cs @@ -0,0 +1,47 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Models; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamCmdProfile")] +[OutputType(typeof(SteamCmdProfile))] +[GenerateBindings] +public partial class GetSteamCmdProfileCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public string Username { get; set; } = string.Empty; + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var profile = await _steamCmdService.GetProfileAsync(Username); + if (profile != null) + { + WriteObject(profile); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetProfileError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfiles.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfiles.cs new file mode 100644 index 00000000..dfbe7c55 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfiles.cs @@ -0,0 +1,44 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Models; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamCmdProfiles")] +[OutputType(typeof(SteamCmdProfile))] +[GenerateBindings] +public partial class GetSteamCmdProfilesCmdlet : DependencyCmdlet +{ + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var profiles = await _steamCmdService.GetProfilesAsync(); + foreach (var profile in profiles) + { + WriteObject(profile); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetProfilesError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJob.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJob.cs new file mode 100644 index 00000000..316a80c8 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJob.cs @@ -0,0 +1,47 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Models; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamInstallJob")] +[OutputType(typeof(SteamCmdInstallJob))] +[GenerateBindings] +public partial class GetSteamInstallJobCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public Guid JobId { get; set; } + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var job = _steamCmdService.GetInstallJob(JobId); + if (job != null) + { + WriteObject(job); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetInstallJobError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJobs.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJobs.cs new file mode 100644 index 00000000..b51d2832 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJobs.cs @@ -0,0 +1,44 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Models; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamInstallJobs")] +[OutputType(typeof(SteamCmdInstallJob))] +[GenerateBindings] +public partial class GetSteamInstallJobsCmdlet : DependencyCmdlet +{ + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var jobs = _steamCmdService.GetInstallJobs(); + foreach (var job in jobs) + { + WriteObject(job); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetInstallJobsError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManual.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManual.cs new file mode 100644 index 00000000..ac5ce244 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManual.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Services; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamManual")] +[OutputType(typeof(byte[]))] +[GenerateBindings] +public partial class GetSteamManualCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public int AppId { get; set; } + + [Parameter(Mandatory = false)] + public string? OutputPath { get; set; } + + [ServiceDependency] + private SteamStoreService _steamStoreService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamStoreService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamStoreService is not available in the PowerShell session"), + "SteamStoreServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var manualData = await _steamStoreService.DownloadManualAsync(AppId); + + if (manualData == null || manualData.Length == 0) + { + WriteWarning($"No manual found for App ID {AppId}"); + return; + } + + if (!string.IsNullOrEmpty(OutputPath)) + { + await File.WriteAllBytesAsync(OutputPath, manualData, cancellationToken); + WriteObject(OutputPath); + } + else + { + WriteObject(manualData); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "DownloadManualError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManualUri.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManualUri.cs new file mode 100644 index 00000000..db93106e --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManualUri.cs @@ -0,0 +1,30 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Services; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamManualUri")] +[OutputType(typeof(Uri))] +[GenerateBindings] +public partial class GetSteamManualUriCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public int AppId { get; set; } + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + try + { + var uri = SteamStoreService.GetManualUri(AppId); + WriteObject(uri); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetManualUriError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamWebAssetUri.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamWebAssetUri.cs new file mode 100644 index 00000000..3154abfc --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamWebAssetUri.cs @@ -0,0 +1,34 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam; +using LANCommander.Steam.Services; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Get, "SteamWebAssetUri")] +[OutputType(typeof(Uri))] +[GenerateBindings] +public partial class GetSteamWebAssetUriCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public int AppId { get; set; } + + [Parameter(Mandatory = true, Position = 1)] + public WebAssetType WebAssetType { get; set; } + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + try + { + var uri = SteamStoreService.GetWebAssetUri(AppId, WebAssetType); + WriteObject(uri); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetWebAssetUriError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs index 1a971089..da014b65 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs @@ -8,7 +8,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsCommon.Get, "UserCustomField")] [OutputType(typeof(string))] - public class GetUserCustomFieldCmdlet(ProfileClient profileClient) : BaseCmdlet + public class GetUserCustomFieldCmdlet(ProfileClient profileClient) : Cmdlet { [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-VerticalFov.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-VerticalFov.cs index 8f029817..1bb26049 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Get-VerticalFov.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-VerticalFov.cs @@ -9,7 +9,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsCommon.Get, "VerticalFov")] [OutputType(typeof(string))] - public class GetVerticalFovCmdlet : BaseCmdlet + public class GetVerticalFovCmdlet : Cmdlet { [Parameter] public int Width { get; set; } = 0; [Parameter] public int Height { get; set; } = 0; diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Install-SteamContent.cs b/LANCommander.SDK/PowerShell/Cmdlets/Install-SteamContent.cs new file mode 100644 index 00000000..c54ed785 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Install-SteamContent.cs @@ -0,0 +1,50 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Models; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsLifecycle.Install, "SteamContent")] +[OutputType(typeof(SteamCmdInstallJob))] +[GenerateBindings] +public partial class InstallSteamContentCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public uint AppId { get; set; } + + [Parameter(Mandatory = true, Position = 1)] + public string InstallDirectory { get; set; } = string.Empty; + + [Parameter(Mandatory = false)] + public string? Username { get; set; } + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var job = await _steamCmdService.InstallContentAsync(AppId, InstallDirectory, Username); + WriteObject(job); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "InstallContentError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs b/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs index 91348d3a..cfe7cad4 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs @@ -5,7 +5,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsData.Out, "PlayerAvatar")] [OutputType(typeof(string))] - public class OutPlayerAvatarCmdlet(ProfileClient profileClient) : BaseCmdlet + public class OutPlayerAvatarCmdlet(ProfileClient profileClient) : Cmdlet { protected override void ProcessRecord() { diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamCmdProfile.cs b/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamCmdProfile.cs new file mode 100644 index 00000000..00c615d9 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamCmdProfile.cs @@ -0,0 +1,41 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Remove, "SteamCmdProfile")] +[GenerateBindings] +public partial class RemoveSteamCmdProfileCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public string Username { get; set; } = string.Empty; + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + await _steamCmdService.DeleteProfileAsync(Username); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "DeleteProfileError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamContent.cs b/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamContent.cs new file mode 100644 index 00000000..cc8995d0 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamContent.cs @@ -0,0 +1,44 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Enums; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Remove, "SteamContent")] +[OutputType(typeof(SteamCmdStatus))] +[GenerateBindings] +public partial class RemoveSteamContentCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public string InstallDirectory { get; set; } = string.Empty; + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var status = await _steamCmdService.RemoveContentAsync(InstallDirectory); + WriteObject(status); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "RemoveContentError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Search-SteamGames.cs b/LANCommander.SDK/PowerShell/Cmdlets/Search-SteamGames.cs new file mode 100644 index 00000000..8690d29c --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Search-SteamGames.cs @@ -0,0 +1,47 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam; +using LANCommander.Steam.Services; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Search, "SteamGames")] +[OutputType(typeof(GameSearchResult))] +[GenerateBindings] +public partial class SearchSteamGamesCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public string Keyword { get; set; } = string.Empty; + + [ServiceDependency] + private SteamStoreService _steamStoreService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamStoreService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamStoreService is not available in the PowerShell session"), + "SteamStoreServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var results = await _steamStoreService.SearchGamesAsync(Keyword); + foreach (var result in results) + { + WriteObject(result); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "SearchGamesError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Set-SteamCmdProfile.cs b/LANCommander.SDK/PowerShell/Cmdlets/Set-SteamCmdProfile.cs new file mode 100644 index 00000000..52cf33e9 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Set-SteamCmdProfile.cs @@ -0,0 +1,51 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Models; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Set, "SteamCmdProfile")] +[GenerateBindings] +public partial class SetSteamCmdProfileCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public string Username { get; set; } = string.Empty; + + [Parameter(Mandatory = true, Position = 1)] + public string InstallDirectory { get; set; } = string.Empty; + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var profile = new SteamCmdProfile + { + Username = Username, + InstallDirectory = InstallDirectory + }; + + await _steamCmdService.SaveProfileAsync(profile); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "SaveProfileError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Stop-SteamInstallJob.cs b/LANCommander.SDK/PowerShell/Cmdlets/Stop-SteamInstallJob.cs new file mode 100644 index 00000000..840f8d21 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Stop-SteamInstallJob.cs @@ -0,0 +1,43 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsLifecycle.Stop, "SteamInstallJob")] +[OutputType(typeof(bool))] +[GenerateBindings] +public partial class StopSteamInstallJobCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public Guid JobId { get; set; } + + [ServiceDependency] + private ISteamCmdService _steamCmdService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamCmdService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamCmdService is not available in the PowerShell session"), + "SteamCmdServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var cancelled = await _steamCmdService.CancelInstallJobAsync(JobId); + WriteObject(cancelled); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "CancelInstallJobError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamManual.cs b/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamManual.cs new file mode 100644 index 00000000..4e830406 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamManual.cs @@ -0,0 +1,43 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam.Services; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsDiagnostic.Test, "SteamManual")] +[OutputType(typeof(bool))] +[GenerateBindings] +public partial class TestSteamManualCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public int AppId { get; set; } + + [ServiceDependency] + private SteamStoreService _steamStoreService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamStoreService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamStoreService is not available in the PowerShell session"), + "SteamStoreServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var exists = await _steamStoreService.HasManualAsync(AppId); + WriteObject(exists); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "HasManualError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamWebAsset.cs b/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamWebAsset.cs new file mode 100644 index 00000000..ab6211b7 --- /dev/null +++ b/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamWebAsset.cs @@ -0,0 +1,47 @@ +using System; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.Steam; +using LANCommander.Steam.Services; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell.Cmdlets; + +[Cmdlet(VerbsDiagnostic.Test, "SteamWebAsset")] +[OutputType(typeof(bool))] +[GenerateBindings] +public partial class TestSteamWebAssetCmdlet : DependencyCmdlet +{ + [Parameter(Mandatory = true, Position = 0)] + public int AppId { get; set; } + + [Parameter(Mandatory = true, Position = 1)] + public WebAssetType WebAssetType { get; set; } + + [ServiceDependency] + private SteamStoreService _steamStoreService; + + public override async Task ProcessRecordAsync(CancellationToken cancellationToken) + { + if (_steamStoreService == null) + { + WriteError(new ErrorRecord( + new InvalidOperationException("SteamStoreService is not available in the PowerShell session"), + "SteamStoreServiceNotAvailable", + ErrorCategory.InvalidOperation, + null)); + return; + } + + try + { + var (exists, _) = await _steamStoreService.HasWebAssetAsync(AppId, WebAssetType); + WriteObject(exists); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "HasWebAssetError", ErrorCategory.OperationStopped, null)); + } + } +} diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Update-IniValue.cs b/LANCommander.SDK/PowerShell/Cmdlets/Update-IniValue.cs index 734ccdd1..8f9ea1ea 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Update-IniValue.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Update-IniValue.cs @@ -13,7 +13,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets /// [Cmdlet(VerbsData.Update, "IniValue")] [OutputType(typeof(string))] - public class UpdateIniValueCmdlet : BaseCmdlet + public class UpdateIniValueCmdlet : Cmdlet { /// /// Gets or sets the section in the INI file that contains the key to be updated. diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs b/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs index bd93f5f9..5a0eca4c 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs @@ -5,7 +5,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsData.Update, "UserCustomField")] [OutputType(typeof(string))] - public class UpdateUserCustomFieldCmdlet(ProfileClient profileClient) : BaseCmdlet + public class UpdateUserCustomFieldCmdlet(ProfileClient profileClient) : Cmdlet { [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Write-GameManifest.cs b/LANCommander.SDK/PowerShell/Cmdlets/Write-GameManifest.cs index 0d0b884c..c4e773b9 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Write-GameManifest.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Write-GameManifest.cs @@ -6,7 +6,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsCommunications.Write, "GameManifest")] [OutputType(typeof(string))] - public class WriteGameManifestCmdlet : BaseCmdlet + public class WriteGameManifestCmdlet : Cmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public string Path { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Write-ReplaceContentInFile.cs b/LANCommander.SDK/PowerShell/Cmdlets/Write-ReplaceContentInFile.cs index bda0904b..9c7565fc 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Write-ReplaceContentInFile.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Write-ReplaceContentInFile.cs @@ -6,7 +6,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets [Cmdlet(VerbsCommunications.Write, "ReplaceContentInFile")] [OutputType(typeof(string))] - public class ReplaceContentInFileCmdlet : BaseCmdlet + public class ReplaceContentInFileCmdlet : Cmdlet { [Parameter(Mandatory = true, Position = 0)] public string Pattern { get; set; } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs b/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs deleted file mode 100644 index fc3684ff..00000000 --- a/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Management.Automation; - -namespace LANCommander.SDK.PowerShell.Cmdlets -{ - public abstract class BaseCmdlet : Cmdlet - { - } -} diff --git a/LANCommander.SDK/PowerShell/Extensions/InitialSessionStateExtensions.cs b/LANCommander.SDK/PowerShell/Extensions/InitialSessionStateExtensions.cs index 10fe1a56..6de22c6f 100644 --- a/LANCommander.SDK/PowerShell/Extensions/InitialSessionStateExtensions.cs +++ b/LANCommander.SDK/PowerShell/Extensions/InitialSessionStateExtensions.cs @@ -27,5 +27,28 @@ public static class InitialSessionStateExtensions initialSessionState.Commands.Add(new SessionStateCmdletEntry("Update-UserCustomField", typeof(UpdateUserCustomFieldCmdlet), null)); initialSessionState.Commands.Add(new SessionStateCmdletEntry("Write-GameManifest", typeof(WriteGameManifestCmdlet), null)); initialSessionState.Commands.Add(new SessionStateCmdletEntry("Write-ReplaceContentInFile", typeof(ReplaceContentInFileCmdlet), null)); + + // SteamCMD cmdlets + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Connect-SteamCmd", typeof(ConnectSteamCmdCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Disconnect-SteamCmd", typeof(DisconnectSteamCmdCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamCmdConnectionStatus", typeof(GetSteamCmdConnectionStatusCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamCmdPath", typeof(GetSteamCmdPathCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamCmdProfile", typeof(GetSteamCmdProfileCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamCmdProfiles", typeof(GetSteamCmdProfilesCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamInstallJob", typeof(GetSteamInstallJobCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamInstallJobs", typeof(GetSteamInstallJobsCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Install-SteamContent", typeof(InstallSteamContentCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Remove-SteamContent", typeof(RemoveSteamContentCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Remove-SteamCmdProfile", typeof(RemoveSteamCmdProfileCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Set-SteamCmdProfile", typeof(SetSteamCmdProfileCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Stop-SteamInstallJob", typeof(StopSteamInstallJobCmdlet), null)); + + // Steam Store cmdlets + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamManual", typeof(GetSteamManualCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamManualUri", typeof(GetSteamManualUriCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamWebAssetUri", typeof(GetSteamWebAssetUriCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Search-SteamGames", typeof(SearchSteamGamesCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Test-SteamManual", typeof(TestSteamManualCmdlet), null)); + initialSessionState.Commands.Add(new SessionStateCmdletEntry("Test-SteamWebAsset", typeof(TestSteamWebAssetCmdlet), null)); } } \ No newline at end of file diff --git a/LANCommander.SDK/PowerShell/PowerShellScript.cs b/LANCommander.SDK/PowerShell/PowerShellScript.cs index 2f643cf2..f03d50ac 100644 --- a/LANCommander.SDK/PowerShell/PowerShellScript.cs +++ b/LANCommander.SDK/PowerShell/PowerShellScript.cs @@ -1,4 +1,4 @@ -using LANCommander.SDK.Enums; +using LANCommander.SDK.Enums; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; diff --git a/LANCommander.SDK/PowerShell/PowerShellStartup.cs b/LANCommander.SDK/PowerShell/PowerShellStartup.cs new file mode 100644 index 00000000..556374db --- /dev/null +++ b/LANCommander.SDK/PowerShell/PowerShellStartup.cs @@ -0,0 +1,18 @@ +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Models; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Services; +using Microsoft.Extensions.DependencyInjection; +using Svrooij.PowerShell.DI; + +namespace LANCommander.SDK.PowerShell; + +public class PowerShellStartup : PsStartup +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddLANCommanderClient(); + services.AddScoped(); + services.AddScoped(); + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Providers/SteamCmdProfileStore.cs b/LANCommander.SDK/Providers/SteamCmdProfileStore.cs new file mode 100644 index 00000000..530d2333 --- /dev/null +++ b/LANCommander.SDK/Providers/SteamCmdProfileStore.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Models; + +namespace LANCommander.SDK.Providers; + +public class SteamCmdProfileStore(ISettingsProvider settingsProvider) : ISteamCmdProfileStore +{ + public async Task> GetAllAsync() + => settingsProvider.CurrentValue.Steam.Profiles; + + public async Task GetByUsernameAsync(string username) + => settingsProvider.CurrentValue.Steam.Profiles.FirstOrDefault(p => p.Username == username); + + public async Task SaveAsync(SteamCmdProfile profile) + { + settingsProvider.Update(s => + { + var existing = s.Steam.Profiles.FirstOrDefault(p => p.Username == profile.Username); + + if (existing != null) + { + // Update existing profile + existing.InstallDirectory = profile.InstallDirectory; + } + else + { + // Add new profile + s.Steam.Profiles.Add(profile); + } + }); + } + + public async Task DeleteAsync(string username) + { + settingsProvider.Update(s => + { + var existing = s.Steam.Profiles.FirstOrDefault(p => p.Username == username); + + if (existing != null) + { + s.Steam.Profiles.Remove(existing); + } + }); + } +} \ No newline at end of file diff --git a/LANCommander.Server.Services/Extensions/IServiceCollectionExtensions.cs b/LANCommander.Server.Services/Extensions/IServiceCollectionExtensions.cs index 4c15f5bc..1352d503 100644 --- a/LANCommander.Server.Services/Extensions/IServiceCollectionExtensions.cs +++ b/LANCommander.Server.Services/Extensions/IServiceCollectionExtensions.cs @@ -55,7 +55,6 @@ public static class IServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/LANCommander.Server.Services/SteamCMDService.cs b/LANCommander.Server.Services/SteamCMDService.cs deleted file mode 100644 index e5b224ed..00000000 --- a/LANCommander.Server.Services/SteamCMDService.cs +++ /dev/null @@ -1,441 +0,0 @@ -using LANCommander.Server.Services.Enums; -using Microsoft.Extensions.Logging; -using System.Diagnostics; -using System.Text; -using System.Text.RegularExpressions; - -namespace LANCommander.Server.Services; - -public class SteamCMDService( - ILogger logger, - SettingsProvider settingsProvider) : BaseService(logger, settingsProvider) -{ - public async Task GetConnectionStatusAsync(string username) - { - try - { - if (!IsValidUsername(username)) - throw new ArgumentException("Invalid username", nameof(username)); - - // Auto-populate SteamCMD path if not configured - if (string.IsNullOrWhiteSpace(_settingsProvider.CurrentValue.Server.SteamCMD.Path)) - { - var detectedPath = await AutoDetectSteamCmdPathAsync(); - - if (!string.IsNullOrWhiteSpace(detectedPath)) - { - _settingsProvider.Update(s => - { - s.Server.SteamCMD.Path = 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; - } - } - - if (!File.Exists(_settingsProvider.CurrentValue.Server.SteamCMD.Path)) - { - _logger.LogWarning("SteamCMD executable not found at configured path: {Path}", _settingsProvider.CurrentValue.Server.SteamCMD.Path); - 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 AutoDetectSteamCmdPathAsync() - { - var possiblePaths = new List(); - - // 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 LoginToSteamAsync(string username, string? password = null) - { - try - { - if (!IsValidUsername(username)) - throw new ArgumentException("Invalid username", nameof(username)); - - if (string.IsNullOrWhiteSpace(_settingsProvider.CurrentValue.Server.SteamCMD.Path)) - { - _logger.LogError("SteamCMD path is not configured"); - return SteamCmdStatus.PathNotConfigured; - } - - if (!File.Exists(_settingsProvider.CurrentValue.Server.SteamCMD.Path)) - { - _logger.LogError("SteamCMD executable not found at configured path: {Path}", _settingsProvider.CurrentValue.Server.SteamCMD.Path); - return SteamCmdStatus.ExecutableNotFound; - } - - var loginCommand = string.IsNullOrWhiteSpace(password) - ? $"+login {username}" - : $"+login {username} {password}"; - - var result = await ExecuteSteamCmdCommandAsync($"{loginCommand} +quit", TimeSpan.FromSeconds(30)); - - 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 LogoutAsync(string username) - { - try - { - if (!IsValidUsername(username)) - return SteamCmdStatus.InvalidUsername; - - if (string.IsNullOrWhiteSpace(_settingsProvider.CurrentValue.Server.SteamCMD.Path)) - { - _logger.LogError("SteamCMD path is not configured"); - return SteamCmdStatus.PathNotConfigured; - } - - if (!File.Exists(_settingsProvider.CurrentValue.Server.SteamCMD.Path)) - { - _logger.LogError("SteamCMD executable not found at configured path: {Path}", _settingsProvider.CurrentValue.Server.SteamCMD.Path); - 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) - { - try - { - if (!IsValidUsername(username)) - throw new ArgumentException("Invalid username", nameof(username)); - - if (string.IsNullOrWhiteSpace(_settingsProvider.CurrentValue.Server.SteamCMD.Path)) - { - _logger.LogError("SteamCMD path is not configured"); - return SteamCmdStatus.PathNotConfigured; - } - - if (!File.Exists(_settingsProvider.CurrentValue.Server.SteamCMD.Path)) - { - _logger.LogError("SteamCMD executable not found at configured path: {Path}", _settingsProvider.CurrentValue.Server.SteamCMD.Path); - return SteamCmdStatus.ExecutableNotFound; - } - - // Ensure install directory exists - Directory.CreateDirectory(installDirectory); - - var commands = new List(); - - // Add login command if credentials provided - if (!string.IsNullOrWhiteSpace(username)) - { - var loginCommand = $"+login {username}"; - - commands.Add(loginCommand); - } - else - { - commands.Add("+login anonymous"); - } - - // Add install commands - commands.Add($"+force_install_dir \"{installDirectory}\""); - commands.Add($"+app_update {appId} validate"); - commands.Add("+quit"); - - var commandString = string.Join(" ", commands); - var result = await ExecuteSteamCmdCommandAsync(commandString); - - if (result.Success) - { - _logger.LogInformation("Successfully installed Steam app {AppId} to {InstallDirectory}", appId, installDirectory); - return SteamCmdStatus.Success; - } - - _logger.LogError("Failed to install Steam app {AppId}: {Error}", appId, result.ErrorOutput); - return SteamCmdStatus.Error; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error installing Steam content for app {AppId}", appId); - return SteamCmdStatus.UnknownError; - } - } - - public async Task 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; - } - } - - private async Task ExecuteSteamCmdCommandAsync(string arguments, TimeSpan? timeout = null, string steamCmdPath = "") - { - try - { - var executablePath = String.IsNullOrWhiteSpace(steamCmdPath) ? _settingsProvider.CurrentValue.Server.SteamCMD.Path : steamCmdPath; - - 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.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; - } -} \ No newline at end of file diff --git a/LANCommander.Server.Settings/Models/ServerSettings.cs b/LANCommander.Server.Settings/Models/ServerSettings.cs index d9438616..806c4f25 100644 --- a/LANCommander.Server.Settings/Models/ServerSettings.cs +++ b/LANCommander.Server.Settings/Models/ServerSettings.cs @@ -17,7 +17,6 @@ public class ServerSettings public RoleSettings Roles { get; set; } = new(); public ScriptSettings Scripts { get; set; } = new(); public GameServerSettings GameServers { get; set; } = new(); - public SteamCmdSettings SteamCMD { get; set; } = new(); public UpdateSettings Update { get; set; } = new(); public UserSaveSettings UserSaves { get; set; } = new(); } \ No newline at end of file diff --git a/LANCommander.Server.Settings/Models/SteamCmdProfile.cs b/LANCommander.Server.Settings/Models/SteamCmdProfile.cs deleted file mode 100644 index bad12b96..00000000 --- a/LANCommander.Server.Settings/Models/SteamCmdProfile.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LANCommander.Server.Settings.Models; - -public class SteamCmdProfile -{ - public string Username { get; set; } = String.Empty; - public string InstallDirectory { get; set; } = String.Empty; -} \ No newline at end of file diff --git a/LANCommander.Server.Settings/Models/SteamCmdSettings.cs b/LANCommander.Server.Settings/Models/SteamCmdSettings.cs deleted file mode 100644 index 67b00369..00000000 --- a/LANCommander.Server.Settings/Models/SteamCmdSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LANCommander.Server.Settings.Models; - -public class SteamCmdSettings -{ - public string Path { get; set; } = String.Empty; - public string InstallDirectory { get; set; } = ""; - public ICollection Profiles { get; set; } = []; -} \ No newline at end of file diff --git a/LANCommander.Server/Program.cs b/LANCommander.Server/Program.cs index d721d839..a1a69e4a 100644 --- a/LANCommander.Server/Program.cs +++ b/LANCommander.Server/Program.cs @@ -23,6 +23,7 @@ builder.AddServerProcessStatusMonitor(); builder.AddLANCommanderServices(); builder.AddMigrations(); builder.AddDatabase(args); +builder.UseSteam(); builder.Services.AddHealthChecks(); diff --git a/LANCommander.Server/Startup/Steam.cs b/LANCommander.Server/Startup/Steam.cs new file mode 100644 index 00000000..d409dd45 --- /dev/null +++ b/LANCommander.Server/Startup/Steam.cs @@ -0,0 +1,18 @@ +using LANCommander.Steam.Extensions; + +namespace LANCommander.Server.Startup; + +public static class Steam +{ + public static WebApplicationBuilder UseSteam(this WebApplicationBuilder builder) + { + var settings = builder.Configuration.Get(); + + builder.Services.AddSteamCmd(o => + { + o.ExecutablePath = settings?.Steam.Path; + }); + + return builder; + } +} \ No newline at end of file diff --git a/LANCommander.Server/UI/Pages/Settings/Integrations/Steam.razor b/LANCommander.Server/UI/Pages/Settings/Integrations/Steam.razor index 3d43f867..f0def24e 100644 --- a/LANCommander.Server/UI/Pages/Settings/Integrations/Steam.razor +++ b/LANCommander.Server/UI/Pages/Settings/Integrations/Steam.razor @@ -1,10 +1,12 @@ @page "/Settings/Integrations/Steam" -@using LANCommander.Server.Services.Enums @using LANCommander.Server.Settings -@using LANCommander.Server.Settings.Models @using LANCommander.Server.UI.Pages.Settings.Components +@using LANCommander.Steam.Abstractions +@using LANCommander.Steam.Enums +@using LANCommander.Steam.Models @using Microsoft.Extensions.Options -@inject SteamCMDService SteamCmdService +@inject ISteamCmdService SteamCmdService +@inject ISteamCmdProfileStore SteamCmdProfileStore @inject IOptions Settings @inject SettingsProvider SettingsProvider @inject IMessageService MessageService @@ -20,7 +22,7 @@
- +
@@ -28,7 +30,7 @@ - @foreach (var profile in Settings.Value.Server.SteamCMD.Profiles) + @foreach (var profile in _profiles) { @@ -57,6 +59,8 @@ SteamCmdConnectionStatus _connectionStatus; + List _profiles = []; + string _username; string _password; @@ -64,35 +68,20 @@ protected override async Task OnInitializedAsync() { - if (!Settings.Value.Server.SteamCMD.Profiles.Any()) + _profiles = (await SteamCmdService.GetProfilesAsync()).ToList(); + + if (!_profiles.Any()) AddProfile(); - - if (string.IsNullOrWhiteSpace(Settings.Value.Server.SteamCMD.Path)) - { - var detectedPath = await SteamCmdService.AutoDetectSteamCmdPathAsync(); - - if (!string.IsNullOrWhiteSpace(detectedPath)) - { - Settings.Value.Server.SteamCMD.Path = detectedPath; - - SettingsProvider.Update(s => - { - s.Server.SteamCMD.Path = detectedPath; - }); - } - } } void AddProfile() { - Settings.Value.Server.SteamCMD.Profiles.Add(new SteamCmdProfile()); + _profiles.Add(new SteamCmdProfile()); } async Task RemoveProfile(SteamCmdProfile profile) { - Settings.Value.Server.SteamCMD.Profiles.Remove(profile); - - if (!String.IsNullOrWhiteSpace(profile.Username)) + if (!string.IsNullOrWhiteSpace(profile.Username)) { var logoutMessage = new MessageConfig { @@ -104,14 +93,17 @@ MessageService.Loading(logoutMessage); await SteamCmdService.LogoutAsync(profile.Username); + await SteamCmdService.DeleteProfileAsync(profile.Username); logoutMessage.Content = "Successfully logged out from Steam!"; logoutMessage.Duration = 3; + _profiles = (await SteamCmdService.GetProfilesAsync()).ToList(); + MessageService.Success(logoutMessage); } - if (!Settings.Value.Server.SteamCMD.Profiles.Any()) + if (!_profiles.Any()) AddProfile(); } @@ -119,12 +111,10 @@ { try { - Settings.Value.Server.SteamCMD.Profiles = Settings.Value.Server.SteamCMD.Profiles.Where(p => !String.IsNullOrWhiteSpace(p.Username)).ToList(); - - SettingsProvider.Update(s => + foreach (var profile in _profiles) { - s.Server.SteamCMD = Settings.Value.Server.SteamCMD; - }); + await SteamCmdService.SaveProfileAsync(profile); + } MessageService.Success("Settings saved!"); } diff --git a/LANCommander.Steam/Abstractions/ISteamCmdProfileStore.cs b/LANCommander.Steam/Abstractions/ISteamCmdProfileStore.cs new file mode 100644 index 00000000..dcf8733f --- /dev/null +++ b/LANCommander.Steam/Abstractions/ISteamCmdProfileStore.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using LANCommander.Steam.Models; + +namespace LANCommander.Steam.Abstractions; + +/// +/// Interface for storing and retrieving SteamCMD profiles +/// Allows consumers to implement their own storage mechanism +/// +public interface ISteamCmdProfileStore +{ + /// + /// Get all profiles + /// + Task> GetAllAsync(); + + /// + /// Get a profile by username + /// + Task GetByUsernameAsync(string username); + + /// + /// Save or update a profile + /// + Task SaveAsync(SteamCmdProfile profile); + + /// + /// Delete a profile by username + /// + Task DeleteAsync(string username); +} diff --git a/LANCommander.Steam/Abstractions/ISteamCmdService.cs b/LANCommander.Steam/Abstractions/ISteamCmdService.cs new file mode 100644 index 00000000..39bf9fb1 --- /dev/null +++ b/LANCommander.Steam/Abstractions/ISteamCmdService.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LANCommander.Steam.Enums; +using LANCommander.Steam.Events; +using LANCommander.Steam.Models; + +namespace LANCommander.Steam.Abstractions; + +/// +/// Interface for SteamCMD service operations +/// +public interface ISteamCmdService +{ + /// + /// Get or set the SteamCMD executable path + /// + string? ExecutablePath { get; set; } + + /// + /// Event fired when an install job status changes (started, completed, failed) + /// + event EventHandler? InstallStatusChanged; + + /// + /// Event fired when install progress is updated + /// + event EventHandler? InstallProgress; + + /// + /// Check the connection status for a username + /// + Task GetConnectionStatusAsync(string username); + + /// + /// Auto-detect the SteamCMD executable path + /// + Task AutoDetectSteamCmdPathAsync(); + + /// + /// Login to Steam with username and optional password + /// + Task LoginToSteamAsync(string username, string? password = null); + + /// + /// Logout from Steam + /// + Task LogoutAsync(string username); + + /// + /// Queue an installation job for Steam content + /// Returns a job ID that can be used to track progress + /// + Task InstallContentAsync(uint appId, string installDirectory, string? username = null); + + /// + /// Get an install job by ID + /// + SteamCmdInstallJob? GetInstallJob(Guid jobId); + + /// + /// Get all install jobs + /// + IEnumerable GetInstallJobs(); + + /// + /// Cancel an install job + /// + Task CancelInstallJobAsync(Guid jobId); + + /// + /// Remove installed content from a directory + /// + Task RemoveContentAsync(string installDirectory); + + /// + /// Get all profiles (requires profile store to be configured) + /// + Task> GetProfilesAsync(); + + /// + /// Get a profile by username (requires profile store to be configured) + /// + Task GetProfileAsync(string username); + + /// + /// Save a profile (requires profile store to be configured) + /// + Task SaveProfileAsync(SteamCmdProfile profile); + + /// + /// Delete a profile by username (requires profile store to be configured) + /// + Task DeleteProfileAsync(string username); +} diff --git a/LANCommander.Server.Services/Enums/SteamCMDConnectionStatus.cs b/LANCommander.Steam/Enums/SteamCmdConnectionStatus.cs similarity index 70% rename from LANCommander.Server.Services/Enums/SteamCMDConnectionStatus.cs rename to LANCommander.Steam/Enums/SteamCmdConnectionStatus.cs index bf122348..4ca549a7 100644 --- a/LANCommander.Server.Services/Enums/SteamCMDConnectionStatus.cs +++ b/LANCommander.Steam/Enums/SteamCmdConnectionStatus.cs @@ -1,4 +1,4 @@ -namespace LANCommander.Server.Services.Enums; +namespace LANCommander.Steam.Enums; public enum SteamCmdConnectionStatus { diff --git a/LANCommander.Steam/Enums/SteamCmdInstallStatus.cs b/LANCommander.Steam/Enums/SteamCmdInstallStatus.cs new file mode 100644 index 00000000..6c7a9799 --- /dev/null +++ b/LANCommander.Steam/Enums/SteamCmdInstallStatus.cs @@ -0,0 +1,32 @@ +namespace LANCommander.Steam.Enums; + +/// +/// Status of an installation job +/// +public enum SteamCmdInstallStatus +{ + /// + /// Job is queued and waiting to start + /// + Queued, + + /// + /// Job is currently being processed + /// + InProgress, + + /// + /// Job completed successfully + /// + Completed, + + /// + /// Job failed + /// + Failed, + + /// + /// Job was cancelled + /// + Cancelled +} diff --git a/LANCommander.Server.Services/Enums/SteamCmdResult.cs b/LANCommander.Steam/Enums/SteamCmdResult.cs similarity index 80% rename from LANCommander.Server.Services/Enums/SteamCmdResult.cs rename to LANCommander.Steam/Enums/SteamCmdResult.cs index db338b61..813ef040 100644 --- a/LANCommander.Server.Services/Enums/SteamCmdResult.cs +++ b/LANCommander.Steam/Enums/SteamCmdResult.cs @@ -1,4 +1,4 @@ -namespace LANCommander.Server.Services.Enums; +namespace LANCommander.Steam.Enums; public enum SteamCmdStatus { diff --git a/LANCommander.Steam/Events/SteamCmdInstallProgressEventArgs.cs b/LANCommander.Steam/Events/SteamCmdInstallProgressEventArgs.cs new file mode 100644 index 00000000..568acff0 --- /dev/null +++ b/LANCommander.Steam/Events/SteamCmdInstallProgressEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using LANCommander.Steam.Models; + +namespace LANCommander.Steam.Events; + +/// +/// Event arguments for install progress updates +/// +public class SteamCmdInstallProgressEventArgs : EventArgs +{ + /// + /// The install job this progress update is for + /// + public SteamCmdInstallJob Job { get; } + + /// + /// Progress percentage (0-100) + /// + public double Progress { get; } + + /// + /// Status message + /// + public string StatusMessage { get; } + + /// + /// Bytes downloaded + /// + public long BytesDownloaded { get; } + + /// + /// Total bytes to download + /// + public long BytesTotal { get; } + + /// + /// Download speed in bytes per second + /// + public long BytesPerSecond { get; } + + public SteamCmdInstallProgressEventArgs( + SteamCmdInstallJob job, + double progress, + string statusMessage, + long bytesDownloaded = 0, + long bytesTotal = 0, + long bytesPerSecond = 0) + { + Job = job; + Progress = progress; + StatusMessage = statusMessage; + BytesDownloaded = bytesDownloaded; + BytesTotal = bytesTotal; + BytesPerSecond = bytesPerSecond; + } +} diff --git a/LANCommander.Steam/Events/SteamCmdInstallStatusEventArgs.cs b/LANCommander.Steam/Events/SteamCmdInstallStatusEventArgs.cs new file mode 100644 index 00000000..93465c31 --- /dev/null +++ b/LANCommander.Steam/Events/SteamCmdInstallStatusEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using LANCommander.Steam.Enums; +using LANCommander.Steam.Models; + +namespace LANCommander.Steam.Events; + +/// +/// Event arguments for install status changes (started, completed, failed) +/// +public class SteamCmdInstallStatusEventArgs : EventArgs +{ + /// + /// The install job this status change is for + /// + public SteamCmdInstallJob Job { get; } + + /// + /// The new status + /// + public SteamCmdInstallStatus Status { get; } + + /// + /// Error message if the status is Failed + /// + public string? ErrorMessage { get; } + + public SteamCmdInstallStatusEventArgs(SteamCmdInstallJob job, SteamCmdInstallStatus status, string? errorMessage = null) + { + Job = job; + Status = status; + ErrorMessage = errorMessage; + } +} diff --git a/LANCommander.Steam/Extensions/ServiceCollectionExtensions.cs b/LANCommander.Steam/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..9f64573d --- /dev/null +++ b/LANCommander.Steam/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,81 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Implementations; +using LANCommander.Steam.Options; +using LANCommander.Steam.Services; + +namespace LANCommander.Steam.Extensions; + +/// +/// Extension methods for registering SteamCMD services +/// +public static class ServiceCollectionExtensions +{ + /// + /// Add SteamCMD service with default in-memory profile store + /// + public static IServiceCollection AddSteamCmd(this IServiceCollection services, Action? configure = null) + { + if (configure != null) + { + services.Configure(configure); + } + else + { + services.Configure(_ => { }); + } + + services.AddSingleton(); + services.AddScoped(); + + return services; + } + + /// + /// Add SteamCMD service with custom profile store implementation + /// + public static IServiceCollection AddSteamCmd( + this IServiceCollection services, + Action? configure = null) + where TProfileStore : class, ISteamCmdProfileStore + { + if (configure != null) + { + services.Configure(configure); + } + else + { + services.Configure(_ => { }); + } + + services.AddSingleton(); + services.AddScoped(); + + return services; + } + + /// + /// Add SteamCMD service with existing profile store instance + /// + public static IServiceCollection AddSteamCmd( + this IServiceCollection services, + ISteamCmdProfileStore profileStore, + Action? configure = null) + { + if (configure != null) + { + services.Configure(configure); + } + else + { + services.Configure(_ => { }); + } + + services.AddSingleton(profileStore); + services.AddScoped(); + + return services; + } +} diff --git a/LANCommander.Steam/Implementations/InMemorySteamCmdProfileStore.cs b/LANCommander.Steam/Implementations/InMemorySteamCmdProfileStore.cs new file mode 100644 index 00000000..f87283e6 --- /dev/null +++ b/LANCommander.Steam/Implementations/InMemorySteamCmdProfileStore.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LANCommander.Steam.Abstractions; +using LANCommander.Steam.Models; + +namespace LANCommander.Steam.Implementations; + +/// +/// In-memory implementation of ISteamCmdProfileStore +/// Useful for testing or simple scenarios +/// +public class InMemorySteamCmdProfileStore : ISteamCmdProfileStore +{ + private readonly Dictionary _profiles = new(); + + public Task> GetAllAsync() + { + return Task.FromResult>(_profiles.Values.ToList()); + } + + public Task GetByUsernameAsync(string username) + { + _profiles.TryGetValue(username, out var profile); + return Task.FromResult(profile); + } + + public Task SaveAsync(SteamCmdProfile profile) + { + _profiles[profile.Username] = profile; + return Task.CompletedTask; + } + + public Task DeleteAsync(string username) + { + _profiles.Remove(username); + return Task.CompletedTask; + } +} diff --git a/LANCommander.Steam/LANCommander.Steam.csproj b/LANCommander.Steam/LANCommander.Steam.csproj index 7f24897d..83631051 100644 --- a/LANCommander.Steam/LANCommander.Steam.csproj +++ b/LANCommander.Steam/LANCommander.Steam.csproj @@ -1,13 +1,21 @@  - netstandard2.0 + net9.0 + latestmajor + + + + + + + diff --git a/LANCommander.Steam/Models/SteamCmdInstallJob.cs b/LANCommander.Steam/Models/SteamCmdInstallJob.cs new file mode 100644 index 00000000..2226c917 --- /dev/null +++ b/LANCommander.Steam/Models/SteamCmdInstallJob.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using LANCommander.Steam.Enums; + +namespace LANCommander.Steam.Models; + +/// +/// Represents an installation job in the queue +/// +public class SteamCmdInstallJob +{ + /// + /// Unique identifier for this install job + /// + public Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// Steam App ID to install + /// + public uint AppId { get; set; } + + /// + /// Installation directory + /// + public string InstallDirectory { get; set; } = string.Empty; + + /// + /// Username for Steam login (null for anonymous) + /// + public string? Username { get; set; } + + /// + /// Current status of the installation + /// + public SteamCmdInstallStatus Status { get; set; } = SteamCmdInstallStatus.Queued; + + /// + /// Progress percentage (0-100) + /// + public double Progress { get; set; } + + /// + /// Current status message + /// + public string StatusMessage { get; set; } = string.Empty; + + /// + /// Bytes downloaded + /// + public long BytesDownloaded { get; set; } + + /// + /// Total bytes to download + /// + public long BytesTotal { get; set; } + + /// + /// Download speed in bytes per second + /// + public long BytesPerSecond { get; set; } + + /// + /// When the job was created + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// When the job started processing + /// + public DateTime? StartedAt { get; set; } + + /// + /// When the job completed + /// + public DateTime? CompletedAt { get; set; } + + /// + /// Error message if the job failed + /// + public string? ErrorMessage { get; set; } + + /// + /// Task completion source for awaiting the job + /// + internal TaskCompletionSource? CompletionSource { get; set; } +} diff --git a/LANCommander.Steam/Models/SteamCmdProfile.cs b/LANCommander.Steam/Models/SteamCmdProfile.cs new file mode 100644 index 00000000..e10aa810 --- /dev/null +++ b/LANCommander.Steam/Models/SteamCmdProfile.cs @@ -0,0 +1,17 @@ +namespace LANCommander.Steam.Models; + +/// +/// Represents a SteamCMD profile with username and install directory +/// +public class SteamCmdProfile +{ + /// + /// Steam username for this profile + /// + public string Username { get; set; } = string.Empty; + + /// + /// Default install directory for this profile + /// + public string InstallDirectory { get; set; } = string.Empty; +} diff --git a/LANCommander.Steam/Options/SteamCmdOptions.cs b/LANCommander.Steam/Options/SteamCmdOptions.cs new file mode 100644 index 00000000..efeb2db3 --- /dev/null +++ b/LANCommander.Steam/Options/SteamCmdOptions.cs @@ -0,0 +1,22 @@ +namespace LANCommander.Steam.Options; + +/// +/// Configuration options for SteamCMD service +/// +public class SteamCmdOptions +{ + /// + /// Path to the SteamCMD executable + /// + public string? ExecutablePath { get; set; } + + /// + /// Default install directory for Steam content + /// + public string? DefaultInstallDirectory { get; set; } + + /// + /// Whether to auto-detect the SteamCMD path if not configured + /// + public bool AutoDetectPath { get; set; } = true; +} diff --git a/LANCommander.Steam/Services/SteamCmdService.cs b/LANCommander.Steam/Services/SteamCmdService.cs new file mode 100644 index 00000000..cda82d77 --- /dev/null +++ b/LANCommander.Steam/Services/SteamCmdService.cs @@ -0,0 +1,860 @@ +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 Microsoft.Extensions.Options; +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; + +/// +/// Service for interacting with SteamCMD +/// +public class SteamCmdService : ISteamCmdService +{ + private readonly ILogger _logger; + private readonly ISteamCmdProfileStore? _profileStore; + private readonly SteamCmdOptions _options; + private readonly ConcurrentDictionary _installJobs = new(); + private readonly SemaphoreSlim _queueSemaphore = new(1, 1); + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private Task? _queueProcessorTask; + + private string? _executablePath; + + /// + /// Event fired when an install job status changes + /// + public event EventHandler? InstallStatusChanged; + + /// + /// Event fired when install progress is updated + /// + public event EventHandler? InstallProgress; + + /// + /// Get or set the SteamCMD executable path + /// + public string? ExecutablePath + { + get => _executablePath ?? _options.ExecutablePath; + set => _executablePath = value; + } + + public SteamCmdService( + ILogger logger, + IOptions? options = null, + ISteamCmdProfileStore? profileStore = null) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options?.Value ?? new SteamCmdOptions(); + _profileStore = profileStore; + + // Start the queue processor + _queueProcessorTask = Task.Run(ProcessInstallQueueAsync, _cancellationTokenSource.Token); + } + public async Task 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 AutoDetectSteamCmdPathAsync() + { + var possiblePaths = new List(); + + // 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 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(30)); + + 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 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); + } + + // Create install job + var job = new SteamCmdInstallJob + { + AppId = appId, + InstallDirectory = installDirectory, + Username = username, + Status = SteamCmdInstallStatus.Queued, + StatusMessage = "Queued for installation", + CompletionSource = new TaskCompletionSource() + }; + + _installJobs[job.Id] = job; + + _logger.LogInformation("Queued installation job {JobId} for app {AppId} to {InstallDirectory}", job.Id, appId, installDirectory); + + // Queue processor will pick it up automatically + return job; + } + + public SteamCmdInstallJob? GetInstallJob(Guid jobId) + { + _installJobs.TryGetValue(jobId, out var job); + return job; + } + + public IEnumerable GetInstallJobs() + { + return _installJobs.Values.ToList(); + } + + public async Task CancelInstallJobAsync(Guid jobId) + { + if (!_installJobs.TryGetValue(jobId, out var job)) + return false; + + if (job.Status == SteamCmdInstallStatus.Completed || job.Status == SteamCmdInstallStatus.Failed) + return false; + + job.Status = SteamCmdInstallStatus.Cancelled; + job.StatusMessage = "Cancelled by user"; + job.CompletedAt = DateTime.UtcNow; + job.CompletionSource?.TrySetCanceled(); + + OnInstallStatusChanged(job, SteamCmdInstallStatus.Cancelled); + + _logger.LogInformation("Cancelled install job {JobId}", jobId); + + return true; + } + + public async Task 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> GetProfilesAsync() + { + if (_profileStore == null) + { + throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation."); + } + + return await _profileStore.GetAllAsync(); + } + + public async Task 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); + } + + private async Task ProcessInstallQueueAsync() + { + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + var queuedJob = _installJobs.Values + .FirstOrDefault(j => j.Status == SteamCmdInstallStatus.Queued); + + if (queuedJob == null) + { + await Task.Delay(1000, _cancellationTokenSource.Token); + continue; + } + + // Process the job (semaphore is acquired inside ProcessInstallJobAsync) + await ProcessInstallJobAsync(queuedJob); + } + catch (OperationCanceledException) + { + // Service is being disposed + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing install queue"); + await Task.Delay(5000, _cancellationTokenSource.Token); + } + } + } + + private async Task ProcessInstallJobAsync(SteamCmdInstallJob job) + { + await _queueSemaphore.WaitAsync(_cancellationTokenSource.Token); + + try + { + job.Status = SteamCmdInstallStatus.InProgress; + job.StartedAt = DateTime.UtcNow; + job.StatusMessage = "Starting installation..."; + OnInstallStatusChanged(job, SteamCmdInstallStatus.InProgress); + + // Ensure install directory exists + Directory.CreateDirectory(job.InstallDirectory); + + var commands = new List(); + + // Add login command if credentials provided + if (!string.IsNullOrWhiteSpace(job.Username)) + { + commands.Add($"+login {job.Username}"); + } + else + { + commands.Add("+login anonymous"); + } + + // Add install commands + commands.Add($"+force_install_dir \"{job.InstallDirectory}\""); + commands.Add($"+app_update {job.AppId} validate"); + commands.Add("+quit"); + + var commandString = string.Join(" ", commands); + + var result = await ExecuteSteamCmdCommandWithProgressAsync( + commandString, + job, + _cancellationTokenSource.Token); + + if (result.Success) + { + job.Status = SteamCmdInstallStatus.Completed; + job.StatusMessage = "Installation completed successfully"; + 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 (OperationCanceledException) + { + job.Status = SteamCmdInstallStatus.Cancelled; + job.StatusMessage = "Installation cancelled"; + job.CompletedAt = DateTime.UtcNow; + job.CompletionSource?.TrySetCanceled(); + + OnInstallStatusChanged(job, SteamCmdInstallStatus.Cancelled); + } + catch (Exception ex) + { + job.Status = SteamCmdInstallStatus.Failed; + job.StatusMessage = $"Installation error: {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); + } + finally + { + _queueSemaphore.Release(); + } + } + + private async Task 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.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + output.AppendLine(e.Data); + _logger.LogDebug("SteamCMD Output: {Output}", e.Data); + + // Parse progress if job is provided + if (job != null) + { + ParseProgressFromOutput(e.Data, job); + } + } + }; + + 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 + { + await 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 + }; + } + } + + 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 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, + 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; + } +} \ No newline at end of file diff --git a/LANCommander.Steam/Services/SteamStoreService.cs b/LANCommander.Steam/Services/SteamStoreService.cs new file mode 100644 index 00000000..5d9e0118 --- /dev/null +++ b/LANCommander.Steam/Services/SteamStoreService.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Mime; +using System.Threading.Tasks; +using HtmlAgilityPack; + +namespace LANCommander.Steam.Services; + +public class SteamStoreService +{ + private readonly HttpClient HttpClient; + + public SteamStoreService() + { + HttpClient = new HttpClient(); + HttpClient.BaseAddress = new Uri("https://store.steampowered.com"); + } + + public async Task> SearchGamesAsync(string keyword) + { + HtmlWeb web = new HtmlWeb(); + HtmlDocument dom = await web.LoadFromWebAsync($"https://store.steampowered.com/search/suggest?term={keyword}&f=games&cc=US"); + + var results = new List(); + var matches = dom.DocumentNode.SelectNodes("//a[@data-ds-appid]"); + + if (matches == null || matches.Count == 0) + return []; + + foreach (var match in matches) + { + try + { + var appId = match.Attributes["data-ds-appid"].Value; + var matchNameElement = match.SelectSingleNode(".//div[@class = 'match_name']"); + + appId = appId.Split(',').First(); + + if (matchNameElement != null) + { + results.Add(new GameSearchResult + { + Name = matchNameElement.InnerText, + AppId = Convert.ToInt32(appId) + }); + } + } + catch (Exception ex) { } + } + + return results; + } + + public async Task<(bool Exists, string MimeType)> HasWebAssetAsync(int appId, WebAssetType webAssetType) + { + var webAssetUri = GetWebAssetUri(appId, webAssetType); + var response = await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, webAssetUri)); + + var exists = response.Content.Headers.ContentType.MediaType == MediaTypeNames.Image.Jpeg || response.Content.Headers.ContentType.MediaType == "image/png"; + + return (exists, response.Content.Headers.ContentType.MediaType); + } + + public async Task HasManualAsync(int appId) + { + var manualUri = GetManualUri(appId); + var response = await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, manualUri)); + + return response.Content.Headers.ContentType.MediaType == MediaTypeNames.Application.Pdf; + } + + public async Task DownloadManualAsync(int appId) + { + var manualUri = GetManualUri(appId); + var response = await HttpClient.GetAsync(manualUri); + + if (!response.IsSuccessStatusCode) + return null; + + using (var ms = new MemoryStream()) + { + await response.Content.CopyToAsync(ms); + + return ms.ToArray(); + } + } + + public static Uri GetManualUri(int appId) + { + return new Uri($"https://store.steampowered.com/manual/{appId}"); + } + + public static Uri GetWebAssetUri(int appId, WebAssetType type) + { + Dictionary webAssetTypeMap = new Dictionary() + { + { WebAssetType.Capsule, "capsule_231x87.jpg" }, + { WebAssetType.CapsuleLarge, "capsule_616x353.jpg" }, + { WebAssetType.Header, "header.jpg" }, + { WebAssetType.HeroCapsule, "hero_capsule.jpg" }, + { WebAssetType.LibraryCover, "library_600x900.jpg" }, + { WebAssetType.LibraryHeader, "library_header.jpg" }, + { WebAssetType.LibraryHero, "library_hero.jpg" }, + { WebAssetType.Logo, "logo.png" } + }; + + return new Uri($"https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/{appId}/{webAssetTypeMap[type]}"); + } +} \ No newline at end of file