VRisingServerManager.psm1
# module parameters $ErrorActionPreference = 'Stop' class VRisingServer { # static variables static hidden [hashtable] $_config static hidden [string] $_configFilePath static hidden [string] $_serverFileDir static hidden [string] $DATA_DIR_NAME = 'Data' static hidden [string] $SAVES_DIR_NAME = 'Saves' static hidden [string] $INSTALL_DIR_NAME = 'Install' static hidden [string] $LOG_DIR_NAME = 'Log' # static constructor static VRisingServer() { Update-TypeData ` -TypeName "VRisingServer" ` -MemberName ShortName ` -MemberType ScriptProperty ` -Value { return $this._properties.ReadProperty('ShortName') } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName SaveName ` -MemberType ScriptProperty ` -Value { return $this._settings.GetHostSetting('SaveName') } ` -SecondValue { param($value) $this.SetHostSetting('SaveName', $value) } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName DisplayName ` -MemberType ScriptProperty ` -Value { return $this._settings.GetHostSetting('Name') } ` -SecondValue { param($value) $this.SetHostSetting('Name', $value) } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName Status ` -MemberType ScriptProperty ` -Value { return $this._processMonitor.GetStatus() } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName Monitor ` -MemberType ScriptProperty ` -Value { return $this._processMonitor.GetMonitorStatus() } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName Command ` -MemberType ScriptProperty ` -Value { return $this._processMonitor.GetActiveCommand().Name } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName LastUpdate ` -MemberType ScriptProperty ` -Value { return $this._processMonitor.GetUpdateStatus() } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName Uptime ` -MemberType ScriptProperty ` -Value { return $this._processMonitor.GetUptime() } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName UpdateOnStartup ` -MemberType ScriptProperty ` -Value { return $this._properties.ReadProperty('UpdateOnStartup') } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName InstallDir ` -MemberType ScriptProperty ` -Value { return $this._properties.ReadProperty('InstallDir') } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName DataDir ` -MemberType ScriptProperty ` -Value { return $this._properties.ReadProperty('DataDir') } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName SettingsDir ` -MemberType ScriptProperty ` -Value { return $this.GetSettingsDirPath() } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName SavesDir ` -MemberType ScriptProperty ` -Value { return $this.GetSavesDirPath() } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName LogDir ` -MemberType ScriptProperty ` -Value { return $this._properties.ReadProperty('LogDir') } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName LogFile ` -MemberType ScriptProperty ` -Value { return $this._properties.GetLogFilePath([VRisingServerLogType]::Server) } ` -Force Update-TypeData ` -TypeName "VRisingServer" ` -MemberName FilePath ` -MemberType ScriptProperty ` -Value { return $this._filePath } ` -Force [VRisingServer]::_configFilePath = Join-Path ` -Path ([Environment]::GetEnvironmentVariable('ProgramData')) ` -ChildPath 'edgetools' | Join-Path -ChildPath 'VRisingServerManager' | Join-Path -ChildPath 'config.json' [VRisingServer]::_serverFileDir = Join-Path ` -Path ([Environment]::GetEnvironmentVariable('ProgramData')) ` -ChildPath 'edgetools' | Join-Path -ChildPath 'VRisingServerManager' | Join-Path -ChildPath 'Servers' [VRisingServer]::_config = @{ SkipNewVersionCheck = $false DefaultServerDir = 'D:\VRisingServers' SteamCmdPath = $null } [VRisingServer]::LoadConfigFile() if (($null -eq [VRisingServer]::_config['SteamCmdPath']) -or ($true -eq ([string]::IsNullOrWhiteSpace([VRisingServer]::_config['SteamCmdPath'])))) { [VRisingServerLog]::Warning("SteamCmdPath is unset -- resolve using: Set-VRisingServerManagerConfigOption SteamCmdPath 'path/to/steamcmd.exe'") } } static hidden [psobject[]] GetConfigValue([string]$configKey) { return [VRisingServer]::_config[$configKey] } static hidden [void] SetConfigValue([string]$configKey, [PSObject]$configValue) { if ($false -eq ($configKey -in [VRisingServer]::_config.Keys)) { throw [VRisingServerException]::New("Config key '$configKey' unrecognized -- valid keys: $([VRisingServer]::_config.Keys)") } [VRisingServer]::_config[$configKey] = $configValue [VRisingServer]::SaveConfigFile() [VRisingServerLog]::Info("Updated $configKey") } static hidden [string[]] GetConfigKeys() { return [VRisingServer]::_config.Keys } static hidden [void] LoadConfigFile() { if ($false -eq (Test-Path -LiteralPath ([VRisingServer]::_configFilePath) -PathType Leaf)) { return } $configFileContents = Get-Content -Raw -LiteralPath ([VRisingServer]::_configFilePath) | ConvertFrom-Json if ($true -eq ($configFileContents.PSObject.Properties.Name -contains 'SkipNewVersionCheck')) { [VRisingServer]::_config['SkipNewVersionCheck'] = $configFileContents.SkipNewVersionCheck } if ($true -eq ($configFileContents.PSObject.Properties.Name -contains 'DefaultServerDir')) { [VRisingServer]::_config['DefaultServerDir'] = $configFileContents.DefaultServerDir } if ($true -eq ($configFileContents.PSObject.Properties.Name -contains 'SteamCmdPath')) { [VRisingServer]::_config['SteamCmdPath'] = $configFileContents.SteamCmdPath } } static hidden [void] SaveConfigFile() { # get dir for path $configFileDir = [VRisingServer]::_configFilePath | Split-Path -Parent # check if dir exists if ($false -eq (Test-Path -LiteralPath $configFileDir -PathType Container)) { # create it New-Item -Path $configFileDir -ItemType Directory | Out-Null } $configFile = [PSCustomObject]@{ SkipNewVersionCheck = [VRisingServer]::_config['SkipNewVersionCheck'] DefaultServerDir = [VRisingServer]::_config['DefaultServerDir'] SteamCmdPath = [VRisingServer]::_config['SteamCmdPath'] } $configFileJson = ConvertTo-Json -InputObject $configFile -Depth 5 $configFileJson | Out-File -LiteralPath ([VRisingServer]::_configFilePath) [VRisingServerLog]::Verbose("Saved main config file") } static hidden [VRisingServer[]] FindServers([string[]]$searchKeys) { $servers = [VRisingServer]::LoadServers() $foundServers = [System.Collections.ArrayList]::New() if (($null -eq $searchKeys) -or ($searchKeys.Count -eq 0)) { $searchKeys = @('*') } foreach ($searchKey in $searchKeys) { $serversForKey = [VRisingServer]::FindServers($searchKey, $servers) if ($null -ne $serversForKey) { $foundServers.AddRange($serversForKey) } } return $foundServers.ToArray([VRisingServer]) } static hidden [VRisingServer] GetServer([string]$shortName) { return [VRisingServer]::LoadServers() | Where-Object { $_._properties.ReadProperty('ShortName') -eq $shortName } } static hidden [VRisingServer[]] FindServers([string]$searchKey, [VRisingServer[]]$servers) { if ([string]::IsNullOrWhiteSpace($searchKey)) { $searchKey = '*' } return $servers | Where-Object { $_._properties.ReadProperty('ShortName') -like $searchKey } } static hidden [VRisingServer[]] FindServers([string]$searchKey) { return [VRisingServer]::FindServers($searchKey, [VRisingServer]::LoadServers()) } static hidden [string[]] GetShortNames() { return [VRisingServer]::LoadServers() | ForEach-Object { $_._properties.ReadProperty('ShortName') } } static hidden [bool] ServerFileDirExists() { return Test-Path -LiteralPath ([VRisingServer]::_serverFileDir) -PathType Container } static hidden [string] GetServerFilePath([string]$ShortName) { return Join-Path -Path ([VRisingServer]::_serverFileDir) -ChildPath "$ShortName.json" } static hidden [System.IO.FileInfo[]] GetServerFiles() { return Get-ChildItem ` -Path $(Join-Path -Path ([VRisingServer]::_serverFileDir) -ChildPath '*.json') ` -File } static hidden [VRisingServer[]] LoadServers() { # check if servers dir exists if ($false -eq ([VRisingServer]::ServerFileDirExists())) { return $null } $servers = [System.Collections.ArrayList]::New() $serverFiles = [VRisingServer]::GetServerFiles() foreach ($serverFile in $serverFiles) { $server = [VRisingServer]::LoadServer($serverFile.FullName) if ($null -ne $server) { $servers.Add($server) } } return $servers.ToArray([VRisingServer]) } static hidden [VRisingServer] LoadServer([string]$filePath) { $serverFileContents = Get-Content -Raw -LiteralPath $filePath | ConvertFrom-Json if (($false -eq ($serverFileContents.PSObject.Properties.Name -contains 'ShortName')) -or ([string]::IsNullOrWhiteSpace($serverFileContents.ShortName))) { throw [VRisingServerException]::New("Failed loading server file at $filePath -- ShortName is missing or empty") } $server = [VRisingServer]::New($filePath, $serverFileContents.ShortName) [VRisingServerLog]::Verbose("[$($serverFileContents.ShortName)] server loaded from $filePath") return $server } static hidden [void] CreateServer([string]$ShortName) { if ($false -eq ($ShortName -match '^[0-9A-Za-z-_]+$')) { throw [VRisingServerException]::New("Server '$ShortName' is not a valid name -- allowed characters: [A-Z] [a-z] [0-9] [-] [_]") } if ([VRisingServer]::GetShortNames() -contains $ShortName) { throw [VRisingServerException]::New("Server '$ShortName' already exists") } $server = [VRisingServer]::New([VRisingServer]::GetServerFilePath($ShortName), $ShortName) $serverProperties = @{ ShortName = $ShortName UpdateOnStartup = $true DataDir = Join-Path -Path ([VRisingServer]::_config['DefaultServerDir']) -ChildPath $ShortName | Join-Path -ChildPath ([VRisingServer]::DATA_DIR_NAME) InstallDir = Join-Path -Path ([VRisingServer]::_config['DefaultServerDir']) -ChildPath $ShortName | Join-Path -ChildPath ([VRisingServer]::INSTALL_DIR_NAME) LogDir = Join-Path -Path ([VRisingServer]::_config['DefaultServerDir']) -ChildPath $ShortName | Join-Path -ChildPath ([VRisingServer]::LOG_DIR_NAME) } $server._properties.WriteProperties($serverProperties) [VRisingServerLog]::Info("[$($ShortName)] Server created") } static hidden [void] DeleteServer([VRisingServer]$server, [bool]$force) { if (($true -eq $server._processMonitor.ServerIsRunning()) -and ($false -eq $force)) { throw [VRisingServerException]::New("[$($server._properties.ReadProperty('ShortName'))] Cannot remove server while it is running -- Stop the server with 'vrstop' first, or use 'Force' to override") } if (($true -eq $server._processMonitor.UpdateIsRunning()) -and ($false -eq $force)) { throw [VRisingServerException]::New("[$($server._properties.ReadProperty('ShortName'))] Cannot remove server while it is updating -- Wait for update to complete, or use 'Force' to override") } if (($true -eq $server._processMonitor.MonitorIsRunning()) -and ($false -eq $force)) { throw [VRisingServerException]::New("[$($server._properties.ReadProperty('ShortName'))] Cannot remove server while the monitor is running -- Stop the monitor with 'vrdisable' first, or use 'Force' to override") } if (($true -eq $server._processMonitor.MonitorIsRunning()) -and ($false -eq $force)) { throw [VRisingServerException]::New("[$($server._properties.ReadProperty('ShortName'))] Cannot remove server while the monitor is enabled -- Disable the monitor with 'vrdisable' first, or use 'Force' to override") } $shortName = $($server._properties.ReadProperty('ShortName')) if ($true -eq (Test-Path -LiteralPath $server._properties.GetFilePath() -PathType Leaf)) { Remove-Item -LiteralPath $server._properties.GetFilePath() } [VRisingServerLog]::Info("[$shortName] Server removed") } # instance variables hidden [VRisingServerProperties] $_properties hidden [VRisingServerSettings] $_settings hidden [VRisingServerProcessMonitor] $_processMonitor # instance constructors VRisingServer([string]$filePath, [string]$shortName) { $this._properties = [VRisingServerProperties]::New($filePath) $this._settings = [VRisingServerSettings]::New($this._properties) $this._processMonitor = [VRisingServerProcessMonitor]::New($this._properties) } # instance methods [void] Start() { $this._processMonitor.Start() } [void] Stop([bool]$force) { $this._processMonitor.Stop($force) } [void] Update() { $this._processMonitor.Update() } [void] Restart([bool]$force) { $this._processMonitor.Restart($force) } [void] Enable() { $this._processMonitor.EnableMonitor() } [void] Disable() { $this._processMonitor.DisableMonitor() } hidden [string] GetCommandStatus() { return 'TODO: REMOVE' # if ($true -eq $this.CommandIsRunning()) { # return 'Executing' # } elseif ($this._properties.ReadProperty('CommandFinished') -eq $true) { # return 'OK' # } elseif ($this._properties.ReadProperty('CommandFinished') -eq $false) { # return 'Error' # } else { # return 'Unknown' # } } hidden [string] GetSavesDirPath() { return Join-Path -Path $this._properties.ReadProperty('DataDir') -ChildPath ([VRisingServer]::SAVES_DIR_NAME) } } class VRisingServerException : Exception { VRisingServerException() : base("an error occurred") {} VRisingServerException([string]$message) : base($message) {} VRisingServerException([string]$message, [Exception]$innerException) : base($message, $innerException) {} } class VRisingServerLog { static [bool] $ShowDateTime = $false static [void] Info([PSCustomObject[]]$toLog) { $toLog | ForEach-Object { Write-Information $([VRisingServerLog]::WithPrefix($_)) -InformationAction 'Continue' } } static [void] Verbose([PSCustomObject[]]$toLog) { $toLog | ForEach-Object { Write-Verbose $([VRisingServerLog]::WithPrefix($_)) } } static [void] Debug([PSCustomObject[]]$toLog) { $toLog | ForEach-Object { Write-Debug $([VRisingServerLog]::WithPrefix($_)) } } static [void] Warning([PSCustomObject[]]$toLog) { $toLog | ForEach-Object { Write-Warning $([VRisingServerLog]::WithPrefix($_)) } } static [void] Error([PSCustomObject[]]$toLog) { $toLog | ForEach-Object { Write-Error $([VRisingServerLog]::WithPrefix($_)) } } static hidden [string] WithPrefix([PSCustomObject]$toLog) { $prefixString = '' if ($true -eq [VRisingServerLog]::ShowDateTime) { $prefixString += Get-Date -Format '[yyyy-MM-dd HH:mm:ss] ' } $prefixString += '(VRisingServer) ' $prefixString += $toLog.ToString() return $prefixString } } enum VRisingServerLogType { Server Run RunError Update UpdateError Monitor MonitorError } class VRisingServerProcessMonitor { static hidden [int] $STEAM_APP_ID = 1829350 hidden [VRisingServerProperties] $_properties hidden [System.Threading.Mutex] $_processMutex hidden [System.Threading.Mutex] $_commandMutex hidden [int] $_defaultPollingRate = 1 VRisingServerProcessMonitor([VRisingServerProperties]$properties) { $this._properties = $properties } [void] Run() { $keepRunning = $true $properties = $null try { [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Monitor starting") while ($true -eq $keepRunning) { $properties = $this._properties.ReadProperties(@( 'ShortName', 'ProcessMonitorEnabled', 'UpdateOnStartup' )) if ($false -eq $properties.ProcessMonitorEnabled) { $keepRunning = $false [VRisingServerLog]::Info("[$($properties.ShortName)] Monitor disabled") continue } $activeCommand = $this.GetActiveCommand() if ($null -ne $activeCommand) { $this._properties.WriteProperty('ProcessMonitorActiveCommand', $activeCommand) [VRisingServerLog]::Info("[$($properties.ShortName)] Processing command: $($activeCommand.Name)") switch ($activeCommand.Name) { 'Start' { if ($true -eq $properties.UpdateOnStartup) { $this.UpdateServer() } $this.LaunchServer() # TODO wait for server to start and stabilize for... 30 seconds? break } 'Stop' { $this.KillServer($activeCommand.Force) break } 'Update' { $this.UpdateServer() break } 'Restart' { if ($true -eq $this.ServerIsRunning()) { $this.KillServer($activeCommand.Force) } if ($true -eq $properties.UpdateOnStartup) { $this.UpdateServer() } $this.LaunchServer() } } $this._properties.WriteProperty('ProcessMonitorActiveCommand', $null) [VRisingServerLog]::Info("[$($properties.ShortName)] Command processed: $($activeCommand.Name)") } Start-Sleep -Seconds $this.GetPollingRate() } } finally { $this._properties.WriteProperty('ProcessMonitorActiveCommand', $null) [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Monitor is exiting") } } [void] Start() { $this.SendCommand( [pscustomobject]@{ Name = 'Start' } ) } [void] Stop([bool]$force) { $this.SendCommand( [pscustomobject]@{ Name = 'Stop' Force = $force } ) } [void] Update() { $this.SendCommand( [pscustomobject]@{ Name = 'Update' } ) } [void] Restart([bool]$force) { $this.SendCommand( [pscustomobject]@{ Name = 'Restart' Force = $force } ) } [bool] IsEnabled() { return $this._properties.ReadProperty('ProcessMonitorEnabled') -eq $true } [void] EnableMonitor() { $this.GetProcessMutex().WaitOne() try { $this._properties.WriteProperty('ProcessMonitorEnabled', $true) [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Monitor enabled") } finally { $this.GetProcessMutex().ReleaseMutex() } $this.LaunchMonitor() } [void] DisableMonitor() { $this.GetProcessMutex().WaitOne() try { $this._properties.WriteProperty('ProcessMonitorEnabled', $false) [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Monitor disabled") } finally { $this.GetProcessMutex().ReleaseMutex() } } [void] KillMonitor([bool]$force) { $this.KillProcess('Monitor', $this.GetMonitorProcess(), $force) } [void] KillServer([bool]$force) { $this.KillProcess('Server', $this.GetServerProcess(), $force) } [void] KillUpdate([bool]$force) { $this.KillProcess('Update', $this.GetUpdateProcess(), $force) } [bool] IsBusy() { return ($null -ne $this.GetActiveCommand()) } [string] GetUptime() { $process = $this.GetServerProcess() if ($null -eq $process) { return $null } elseif ($true -eq $process.HasExited) { return $null } else { $uptime = (Get-Date) - $process.StartTime $uptimeString = $null if ($uptime.Days -gt 0) { $uptimeString += "$(($uptime.TotalDays -split '\.')[0])d" } elseif ($uptime.Hours -gt 0) { $uptimeString += "$(($uptime.TotalHours -split '\.')[0])h" } elseif ($uptime.Minutes -gt 0) { $uptimeString += "$(($uptime.TotalMinutes -split '\.')[0])m" } else { $uptimeString += "$(($uptime.TotalSeconds -split '\.')[0])s" } return $uptimeString } } [string] GetStatus() { if ($true -eq $this.ServerIsRunning()) { return 'Running' } elseif ($true -eq $this.UpdateIsRunning()) { return 'Updating' } elseif ($this._properties.ReadProperty('LastExitCode') -ne 0) { return 'Error' } else { return 'Stopped' } } [string] GetMonitorStatus() { if ($true -eq $this.MonitorIsRunning()) { if ($true -eq $this.IsBusy()) { return 'Busy' } else { return 'Idle' } } elseif ($false -eq $this.IsEnabled()) { return 'Disabled' } else { return 'Stopped' } } [string] GetUpdateStatus() { if ($true -eq $this.UpdateIsRunning()) { return 'InProgress' } elseif ($this._properties.ReadProperty('UpdateSuccess') -eq $false) { return 'Failed' } elseif ($this._properties.ReadProperty('UpdateSuccess') -eq $true) { return 'OK' } else { return 'Unknown' } } hidden [void] KillProcess([string]$friendlyName, [System.Diagnostics.Process]$process, [bool]$force) { if ($false -eq $this.ProcessIsRunning($process)) { throw [VRisingServerException]::New("[$($this._properties.ReadProperty('ShortName'))] $friendlyName already stopped") } if ($true -eq $force) { [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Forcefully killing $friendlyName process") } else { [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Gracefully killing $friendlyName process") } & taskkill.exe '/PID' $process.Id $(if ($true -eq $force) { '/F' }) $process.WaitForExit() [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] $friendlyName process has stopped") } hidden [void] SendCommand([pscustomobject]$command) { if ($true -eq $this.IsBusy()) { throw [VRisingServerException]::New("[$($this._properties.ReadProperty('ShortName'))] Cannot send '$($command.Name)' command -- Monitor is busy") } $this.GetCommandMutex().WaitOne() try { # check again if ($true -eq $this.IsBusy()) { throw [VRisingServerException]::New("[$($this._properties.ReadProperty('ShortName'))] Cannot send '$($command.Name)' command -- Monitor is busy") } $this.SetActiveCommand($command) $this.LaunchMonitor() [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] $($command.Name) command sent") } finally { $this.GetCommandMutex().ReleaseMutex() } } hidden [void] SetPollingRate([int]$pollingRate) { if ($pollingRate -gt 0) { $this._properties.WriteProperty('ProcessMonitorPollingRate', $pollingRate) } } hidden [int] GetPollingRate() { $pollingRate = $this._properties.ReadProperty('ProcessMonitorPollingRate') if ($pollingRate -gt 0) { return $pollingRate } else { return $this._defaultPollingRate } } hidden [void] SetActiveCommand([pscustomobject]$command) { $this._properties.WriteProperty('ProcessMonitorActiveCommand', $command) } hidden [psobject] GetActiveCommand() { return $this._properties.ReadProperty('ProcessMonitorActiveCommand') } hidden [void] EnsureDirPathExists([string]$dirPath) { if ($false -eq (Test-Path -LiteralPath $dirPath -PathType Container)) { New-Item -Path $dirPath -ItemType Directory | Out-Null } } hidden [System.Threading.Mutex] GetProcessMutex() { if ($null -eq $this._processMutex) { $this._processMutex = [System.Threading.Mutex]::New($false, "VRisingServerProcessMonitorProcess-$($this._properties.ReadProperty('ShortName'))") } return $this._processMutex } hidden [System.Threading.Mutex] GetCommandMutex() { if ($null -eq $this._commandMutex) { $this._commandMutex = [System.Threading.Mutex]::New($false, "VRisingServerProcessMonitorCommand-$($this._properties.ReadProperty('ShortName'))") } return $this._commandMutex } # start the update process [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Scope='Function')] # must disable this warning due to https://stackoverflow.com/a/23797762 # $handle is declared but not used to cause $process to cache it # ensures that $process can access $process.ExitCode hidden [void] UpdateServer() { if ($true -eq $this.UpdateIsRunning()) { throw [VRisingServerException]::New("[$($this._properties.ReadProperty('ShortName'))] Update is already running") } if ($true -eq $this.ServerIsRunning()) { throw [VRisingServerException]::New("[$($this._properties.ReadProperty('ShortName'))] Server must be stopped before updating") } $properties = $this._properties.ReadProperties(@( 'ShortName', 'LogDir', 'InstallDir' )) $this.EnsureDirPathExists($properties.LogDir) $stdoutLogFile = Join-Path -Path $properties.LogDir -ChildPath "VRisingServer.LastUpdate.Info.log" $stderrLogFile = Join-Path -Path $properties.LogDir -ChildPath "VRisingServer.LastUpdate.Error.log" $process = $null $updateSucceeded = $false $updateDate = Get-Date -Format 'yyyy-MM-ddTHH:mm:ss' try { [VRisingServerLog]::Info("[$($properties.ShortName)] Starting update") $process = Start-Process ` -FilePath ([VRisingServer]::_config['SteamCmdPath']) ` -ArgumentList @( '+force_install_dir', $properties.InstallDir, '+login', 'anonymous', '+app_update', [VRisingServerProcessMonitor]::STEAM_APP_ID, '+quit' ) ` -WindowStyle Hidden ` -RedirectStandardOutput $stdoutLogFile ` -RedirectStandardError $stderrLogFile ` -PassThru $handle = $process.Handle $this._properties.WriteProperties(@{ UpdateProcessName = $process.Name UpdateProcessId = $process.Id UpdateStdoutLogFile = $stdoutLogFile UpdateStderrLogFile = $stderrLogFile }) $process.WaitForExit() if ($process.ExitCode -ne 0) { throw [VRisingServerException]::New("[$($properties.ShortName)] Update process exited with non-zero code: $($process.ExitCode)") } else { $updateSucceeded = $true [VRisingServerLog]::Info("[$($properties.ShortName)] Update completed successfully") } } catch [InvalidOperationException] { throw [VRisingServerException]::New("[$($properties.ShortName)] Failed starting update: $($_.Exception.Message)") } finally { if ($null -ne $process) { $process.Close() } $properties = @{ UpdateProcessName = $null UpdateProcessId = 0 UpdateSuccess = $updateSucceeded } if ($true -eq $updateSucceeded) { $properties['UpdateSuccessDate'] = $updateDate } $this._properties.WriteProperties($properties) } } # start the server process hidden [void] LaunchServer() { if ($true -eq $this.ServerIsRunning()) { throw [VRisingServerException]::New("[$($this._properties.ReadProperty('ShortName'))] Server is already running") } if ($true -eq $this.UpdateIsRunning()) { throw [VRisingServerException]::New("[$($this._properties.ReadProperty('ShortName'))] Server is currently updating and cannot be Started") } $this._properties.WriteProperties(@{ ServerProcessName = $null ServerProcessId = 0 }) $properties = $this._properties.ReadProperties(@( 'ShortName', 'LogDir', 'InstallDir', 'DataDir' )) $this.EnsureDirPathExists($properties.LogDir) $logFile = $this._properties.GetLogFilePath([VRisingServerLogType]::Server) $stdoutLogFile = Join-Path -Path $properties.LogDir -ChildPath "VRisingServer.LastRun.Info.log" $stderrLogFile = Join-Path -Path $properties.LogDir -ChildPath "VRisingServer.LastRun.Error.log" $serverExePath = Join-Path -Path $properties.InstallDir -ChildPath 'VRisingServer.exe' try { $process = Start-Process ` -WindowStyle Hidden ` -RedirectStandardOutput $stdoutLogFile ` -RedirectStandardError $stderrLogFile ` -FilePath $serverExePath ` -ArgumentList "-persistentDataPath `"$($properties.DataDir)`" -logFile `"$logFile`"" ` -PassThru } catch [System.IO.DirectoryNotFoundException] { throw [VRisingServerException]::New("[$($properties.ShortName)] Server failed to start due to missing directory -- try running update first") } catch [InvalidOperationException] { throw [VRisingServerException]::New("[$($properties.ShortName)] Server failed to start: $($_.Exception.Message)") } $this._properties.WriteProperties(@{ ServerProcessName = $process.Name ServerProcessId = $process.Id ServerStdoutLogFile = $stdoutLogFile ServerStderrLogFile = $stderrLogFile }) [VRisingServerLog]::Info("[$($properties.shortName)] Server launched") } # start the background process hidden [void] LaunchMonitor() { # check before locking the mutex (so we don't unnecessarily block the thread) if ($true -eq $this.MonitorIsRunning()) { return } $this.GetProcessMutex().WaitOne() try { # check again just in case it was launched between checking the last statement and locking the mutex if ($true -eq $this.MonitorIsRunning()) { return } $this._properties.WriteProperties(@{ ProcessMonitorProcessName = $null ProcessMonitorProcessId = 0 ProcessMonitorEnabled = $true }) $properties = $this._properties.ReadProperties(@( 'ShortName', 'LogDir' )) $this.EnsureDirPathExists($properties.LogDir) $stdoutLogFile = Join-Path -Path $properties.LogDir -ChildPath "VRisingServer.ProcessMonitor.Info.log" $stderrLogFile = Join-Path -Path $properties.LogDir -ChildPath "VRisingServer.ProcessMonitor.Error.log" $argumentList = @" -Command "& { `$ErrorActionPreference = 'Stop'; if (`$null -eq `$script:VRisingServerManagerFlags) { `$script:VRisingServerManagerFlags = @{}; } `$script:VRisingServerManagerFlags['SkipNewVersionCheck'] = `$true; `$script:VRisingServerManagerFlags['ShowDateTime'] = `$true; `$server = Get-VRisingServer -ShortName '$($properties.shortName)'; `$server._processMonitor.Run(); }" "@ $process = Start-Process ` -FilePath 'powershell' ` -ArgumentList $argumentList ` -WindowStyle Hidden ` -RedirectStandardOutput $stdoutLogFile ` -RedirectStandardError $stderrLogFile ` -PassThru $this._properties.WriteProperties(@{ ProcessMonitorProcessName = $process.Name ProcessMonitorProcessId = $process.Id ProcessMonitorStdoutLogFile = $stdoutLogFile ProcessMonitorStderrLogFile = $stderrLogFile }) [VRisingServerLog]::Info("[$($properties.shortName)] Monitor launched") } finally { $this.GetProcessMutex().ReleaseMutex() } } hidden [bool] MonitorIsRunning() { return $this.ProcessIsRunning($this.GetMonitorProcess()) } hidden [bool] ServerIsRunning() { return $this.ProcessIsRunning($this.GetServerProcess()) } hidden [bool] UpdateIsRunning() { return $this.ProcessIsRunning($this.GetUpdateProcess()) } hidden [bool] ProcessIsRunning([System.Diagnostics.Process]$process) { if ($null -eq $process) { return $false } elseif ($false -eq $process.HasExited) { return $true } else { return $false } } hidden [System.Diagnostics.Process] GetMonitorProcess() { return $this.GetProcessByProperties('ProcessMonitorProcessId', 'ProcessMonitorProcessName') } hidden [System.Diagnostics.Process] GetServerProcess() { return $this.GetProcessByProperties('ServerProcessId', 'ServerProcessName') } hidden [System.Diagnostics.Process] GetUpdateProcess() { return $this.GetProcessByProperties('UpdateProcessId', 'UpdateProcessName') } hidden [System.Diagnostics.Process] GetProcessByProperties([string]$processIdKey, [string]$processNameKey) { $properties = $this._properties.ReadProperties(@($processIdKey, $processNameKey)) if ($properties.$processIdKey -gt 0) { $process = $this.GetProcessById($properties.$processIdKey) if ($properties.$processNameKey -eq $process.ProcessName) { return $process } else { return $null } } else { return $null } } hidden [System.Diagnostics.Process] GetProcessById([int]$processId) { try { return Get-Process -Id $processId } catch [Microsoft.PowerShell.Commands.ProcessCommandException] { if ('NoProcessFoundForGivenId' -eq ($_.FullyQualifiedErrorid -split ',')[0]) { return $null } else { throw $_ } } } } class VRisingServerProperties { hidden [string] $_filePath hidden [System.Threading.Mutex] $_propertiesFileMutex VRisingServerProperties([string]$filePath) { $this._filePath = $filePath $fileName = ($filePath -split '\\')[-1] $this._propertiesFileMutex = [System.Threading.Mutex]::New($false, "VRisingServerProperties-$fileName") } [bool] IsEnabled() { return $this.ReadProperty('Enabled') -eq $true } [string] GetFilePath() { return $this._filePath } [string] GetLogFilePath([VRisingServerLogType]$logType) { switch ($logType) { ([VRisingServerLogType]::Server) { return Join-Path -Path $this.ReadProperty('LogDir') -ChildPath 'VRisingServer.log' } ([VRisingServerLogType]::Run) { return $this.ReadProperty('StdoutLogFile') } ([VRisingServerLogType]::RunError) { return $this.ReadProperty('StderrLogFile') } ([VRisingServerLogType]::Update) { return $this.ReadProperty('UpdateStdoutLogFile') } ([VRisingServerLogType]::UpdateError) { return $this.ReadProperty('UpdateStderrLogFile') } ([VRisingServerLogType]::Monitor) { return $this.ReadProperty('ProcessMonitorStdoutLogFile') } ([VRisingServerLogType]::MonitorError) { return $this.ReadProperty('ProcessMonitorStderrLogFile') } } return $null } hidden [psobject] ReadProperty([string]$name) { return $this.ReadProperties(@($name)).$name } hidden [psobject] ReadProperties([string[]]$names) { if ($false -eq (Test-Path -LiteralPath $this._filePath -PathType Leaf)) { return $null } $fileContent = Get-Content -Raw -LiteralPath $this._filePath | ConvertFrom-Json $properties = [hashtable]@{} foreach ($name in $names) { if ($fileContent.PSObject.Properties.Name -contains $name) { $properties[$name] = $fileContent.$name } } return [pscustomobject]$properties } hidden [void] WriteProperty([string]$name, [psobject]$value) { # deal with PS5.1 ETS System.Array if (($null -ne $value) -and ('Object[]' -eq ($value.GetType().Name))) { $this.WriteProperties(@{ $name=[psobject[]]$value }) } else { $this.WriteProperties(@{ $name=$value }) } } hidden [void] WriteProperties([hashtable]$nameValues) { # get dir for path $serverFileDir = $this._filePath | Split-Path -Parent # check if server dir exists if ($false -eq (Test-Path -LiteralPath $serverFileDir -PathType Container)) { # create it New-Item -Path $serverFileDir -ItemType Directory | Out-Null } try { $this._propertiesFileMutex.WaitOne() # check if file exists if ($true -eq (Test-Path -LiteralPath $this._filePath -PathType Leaf)) { $fileContent = Get-Content -Raw -LiteralPath $this._filePath | ConvertFrom-Json } else { $fileContent = [PSCustomObject]@{} } foreach ($nameValue in $nameValues.GetEnumerator()) { if ($fileContent.PSObject.Properties.Name -contains $nameValue.Name) { $fileContent.$($nameValue.Name) = $nameValue.Value } else { $fileContent | Add-Member -MemberType NoteProperty -Name $nameValue.Name -Value $nameValue.Value } } $fileContentJson = ConvertTo-Json -InputObject $fileContent -Depth 5 $fileContentJson | Out-File -LiteralPath $this._filePath } finally { $this._propertiesFileMutex.ReleaseMutex() } } } class VRisingServerSettings { static hidden [string] $SETTINGS_DIR_NAME = 'Settings' hidden [System.Threading.Mutex] $_settingsFileMutex hidden [VRisingServerProperties] $_properties VRisingServerSettings([VRisingServerProperties] $properties) { $this._properties = $properties $this._settingsFileMutex = [System.Threading.Mutex]::New($false, "VRisingServerSettings-$($properties.ReadProperty('ShortName'))") } [psobject] GetHostSetting([string]$settingName) { return $this.GetSettingsTypeValue([VRisingServerSettingsType]::Host, $settingName) } [psobject] GetGameSetting([string]$settingName) { return $this.GetSettingsTypeValue([VRisingServerSettingsType]::Game, $settingName) } [psobject] GetVoipSetting([string]$settingName) { return $this.GetSettingsTypeValue([VRisingServerSettingsType]::Voip, $settingName) } [void] SetHostSetting([string]$settingName, [psobject]$settingValue, [bool]$resetToDefault) { $this.SetSettingsTypeValue([VRisingServerSettingsType]::Host, $settingName, $settingValue, $resetToDefault) [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Host Setting '$settingName' modified") } [void] SetGameSetting([string]$settingName, [psobject]$settingValue, [bool]$resetToDefault) { $this.SetSettingsTypeValue([VRisingServerSettingsType]::Game, $settingName, $settingValue, $resetToDefault) [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Game Setting '$settingName' modified") } [void] SetVoipSetting([string]$settingName, [psobject]$settingValue, [bool]$resetToDefault) { $this.SetSettingsTypeValue([VRisingServerSettingsType]::Voip, $settingName, $settingValue, $resetToDefault) [VRisingServerLog]::Info("[$($this._properties.ReadProperty('ShortName'))] Voip Setting '$settingName' modified") } hidden [void] SetSettingsTypeValue([VRisingServerSettingsType]$settingsType, [string]$settingName, [psobject]$settingValue, [bool]$resetToDefault) { if ($true -eq [string]::IsNullOrWhiteSpace($settingName)) { throw [VRisingServerException]::New("settingName cannot be null or empty", [System.ArgumentNullException]::New('settingName')) } try { $this._settingsFileMutex.WaitOne() # skip getting default value if it's being reset $defaultValue = $null if ($false -eq $resetToDefault) { $defaultSettings = $null switch ($settingsType) { ([VRisingServerSettingsType]::Host) { $defaultSettings = $this.GetDefaultHostSettingsFile() break } ([VRisingServerSettingsType]::Game) { $defaultSettings = $this.GetDefaultGameSettingsFile() break } ([VRisingServerSettingsType]::Voip) { $defaultSettings = $this.GetDefaultVoipSettingsFile() break } } # get default value $defaultValue = $this.GetSetting($defaultSettings, $settingName) # if default value matches suggested value, reset = true if ($true -eq $this.ObjectsAreEqual($defaultValue, $settingValue)) { $resetToDefault = $true } } # read the file $explicitSettings = $null $settingsFilePath = $null switch ($settingsType) { ([VRisingServerSettingsType]::Host) { $explicitSettings = $this.GetHostSettingsFile() $settingsFilePath = $this.GetHostSettingsFilePath() break } ([VRisingServerSettingsType]::Game) { $explicitSettings = $this.GetGameSettingsFile() $settingsFilePath = $this.GetGameSettingsFilePath() break } ([VRisingServerSettingsType]::Voip) { $explicitSettings = $this.GetVoipSettingsFile() $settingsFilePath = $this.GetVoipSettingsFilePath() break } } # reset or modify the value if ($true -eq $resetToDefault) { $this.DeleteSetting($explicitSettings, $settingName) } else { $explicitSettings = $this.SetSetting($explicitSettings, $settingName, $settingValue) } # write the file $explicitSettingsJson = ConvertTo-Json -InputObject $explicitSettings -Depth 5 $explicitSettingsJson | Out-File -LiteralPath $settingsFilePath } finally { # unlock mutex $this._settingsFileMutex.ReleaseMutex() } } hidden [psobject] GetSettingsTypeValue([VRisingServerSettingsType]$settingsType, [string]$settingName) { $defaultSettings = $null $explicitSettings = $null switch ($settingsType) { ([VRisingServerSettingsType]::Host) { $defaultSettings = $this.GetDefaultHostSettingsFile() $explicitSettings = $this.GetHostSettingsFile() break } ([VRisingServerSettingsType]::Game) { $defaultSettings = $this.GetDefaultGameSettingsFile() $explicitSettings = $this.GetGameSettingsFile() break } ([VRisingServerSettingsType]::Voip) { $defaultSettings = $this.GetDefaultVoipSettingsFile() $explicitSettings = $this.GetVoipSettingsFile() break } } $this.MergePSObjects($defaultSettings, $explicitSettings) if ([string]::IsNullOrWhiteSpace($settingName)) { return $defaultSettings } elseif ($settingName.Contains('*')) { $matchedSettings = $this.GetSettingsKeys($defaultSettings, $null) -like $settingName $filteredSettings = $this.FilterSettings($defaultSettings, $matchedSettings) return $filteredSettings } else { return $this.GetSetting($defaultSettings, $settingName) } } hidden [string[]] FindSettingsTypeKeys([VRisingServerSettingsType]$settingsType, [string]$searchKey) { $settings = $null switch ($settingsType) { ([VRisingServerSettingsType]::Host) { $settings = $this.GetDefaultHostSettingsFile() break } ([VRisingServerSettingsType]::Game) { $settings = $this.GetDefaultGameSettingsFile() break } ([VRisingServerSettingsType]::Voip) { $settings = $this.GetDefaultVoipSettingsFile() break } } return $this.GetSettingsKeys($settings, $null) -like $searchKey } hidden [string[]] GetSettingsKeys($settings, [string]$prefix) { $keys = [System.Collections.ArrayList]::new() foreach ($property in $settings.PSObject.Properties) { if ($property.TypeNameOfValue -eq 'System.Management.Automation.PSCustomObject') { $keys.AddRange($this.GetSettingsKeys($property.Value, "$($property.Name).")) } else { $keys.Add($property.Name) } } for ($i = 0; $i -lt $keys.Count; $i++) { $keys[$i] = $prefix + $keys[$i] } return $keys.ToArray([string]) } # take a source object and merge an overlay on top of it # does not clone, modifies any objects passed by reference hidden [void] MergePSObjects([psobject]$sourceObject, [psobject]$overlay) { if ($null -eq $overlay) { return } if ($null -eq $sourceObject) { $sourceObject = $overlay } # iterate through the properties on the overlay $overlay.PSObject.Properties | ForEach-Object { $currentProperty = $_ # if the sourceobject does NOT contain that property, just assign it from the overlay if ($sourceObject.PSObject.Properties.Name -notcontains $currentProperty.Name) { $sourceObject | Add-Member ` -MemberType NoteProperty ` -Name $currentProperty.Name ` -Value $currentProperty.Value } # if the sourceobject DOES contain that property, check first if it's a container (psobject) switch ($currentProperty.TypeNameOfValue) { 'System.Management.Automation.PSCustomObject' { # if it's a container, call this function on those subobjects (recursive) $this.MergePSObjects($sourceObject.PSObject.Properties[$currentProperty.Name].Value, $currentProperty.Value) break } Default { # if it's NOT a container, just overlay the value directly on top of it $sourceObject | Add-Member ` -MemberType NoteProperty ` -Name $currentProperty.Name ` -Value $currentProperty.Value ` -Force break } } } } hidden [bool] ObjectsAreEqual([psobject]$a, [psobject]$b) { # simple dumbed down recursive comparison function # specifically for comparing values inside the [pscustomobject] from a loaded settings file # does not handle complex types $a_type = $a.GetType().Name $b_type = $b.GetType().Name if ($a_type -ne $b_type) { return $false } switch ($a_type) { 'PSCustomObject' { foreach ($property in $a.PSObject.Properties.GetEnumerator()) { if ($b.PSObject.Properties.Name -notcontains $property.Name) { return $false } $a_value = $property.Value $b_value = $b.PSObject.Properties[$property.Name].Value $expectedComparables = @( 'System.Boolean', 'System.Int32', 'System.Decimal', 'System.String') switch ($property.TypeNameOfValue) { 'System.Object' { if ($false -eq $this.ObjectsAreEqual($a_value, $b_value)) { return $false } continue } { $_ -in $expectedComparables } { if ($a_value -cne $b_value) { return $false } continue } Default { throw [VRisingServerException]::New("ObjectsAreEqual unexpected type: $_") } } } continue } 'Object[]' { if ($a.Count -ne $b.Count) { return $false } for ($i=0; $i -lt $a.Count; $i++) { if ($false -eq $this.ObjectsAreEqual($a[$i], $b[$i])) { return $false } continue } continue } Default { return $a -eq $b } } return $true } hidden [psobject] FilterSettings([psobject]$settings, [string[]]$settingNameFilter) { if (($null -eq $settings) -or ([string]::IsNullOrWhiteSpace($settingNameFilter))) { return $null } $filteredSettings = $null foreach ($settingName in $settingNameFilter) { $settingValue = $this.GetSetting($settings, $settingName) $filteredSettings = $this.SetSetting($filteredSettings, $settingName, $settingValue) } return $filteredSettings } hidden [psobject] GetSetting([psobject]$settings, [string]$settingName) { if ($null -eq $settings) { return $null } $settingNameSegments = $settingName -split '\.' $settingContainer = $settings # loop into the object # based on the number of segments to the path for ($i = 0; $i -lt $settingNameSegments.Count; $i++) { if ($i -eq ($settingNameSegments.Count - 1)) { # last item if ($settingContainer.PSObject.Properties.Name -notcontains $settingNameSegments[$i]) { return $null } } else { if ($null -eq $settingContainer) { # parent pointed to a null value return $null } if ($settingContainer.PSObject.Properties.Name -notcontains $settingNameSegments[$i]) { # missing sub key (given foo.bar, foo does not exist) return $null } $settingContainer = $settingContainer.PSObject.Properties[$settingNameSegments[$i]].Value } } return $settingContainer.PSObject.Properties[$settingNameSegments[-1]].Value } hidden [void] DeleteSetting([psobject]$settings, [string]$settingName) { if ($null -eq $settings) { return } $settingNameSegments = $settingName -split '\.' $settingContainer = $settings # loop into the object # based on the number of segments to the path for ($i = 0; $i -lt $settingNameSegments.Count; $i++) { if ($i -eq ($settingNameSegments.Count - 1)) { # last item if ($settingContainer.PSObject.Properties.Name -contains $settingNameSegments[$i]) { $settingContainer.PSObject.Properties.Remove($settingNameSegments[$i]) } } else { if ($null -eq $settingContainer) { # parent pointed to a null value return } if ($settingContainer.PSObject.Properties.Name -notcontains $settingNameSegments[$i]) { # missing sub key (given foo.bar, foo does not exist) return } $settingContainer = $settingContainer.PSObject.Properties[$settingNameSegments[$i]].Value } } return } # returns the modified (or new) settings object hidden [psobject] SetSetting([psobject]$settings, [string]$settingName, [psobject]$settingValue) { if ($null -eq $settings) { $settings = [PSCustomObject]@{} } # deal with PS5.1 ETS System.Array if (($null -ne $settingValue) -and ('Object[]' -eq ($settingValue.GetType().Name))) { $preparedValue = [psobject[]]$settingValue } else { $preparedValue = $settingValue } $settingNameSegments = $settingName -split '\.' $settingContainer = $settings # loop into the object # based on the number of segments to the path for ($i = 0; $i -lt $settingNameSegments.Count; $i++) { if ($i -eq ($settingNameSegments.Count - 1)) { # last item if ($settingContainer.PSObject.Properties.Name -notcontains $settingNameSegments[$i]) { $settingContainer | Add-Member ` -MemberType NoteProperty ` -Name $settingNameSegments[$i] ` -Value $preparedValue } else { $settingContainer.PSObject.Properties[$settingNameSegments[$i]].Value = $preparedValue } } else { if ($settingContainer.PSObject.Properties.Name -notcontains $settingNameSegments[$i]) { # missing sub key (given foo.bar, foo does not exist) # add the missing key $settingContainer | Add-Member ` -MemberType NoteProperty ` -Name $settingNameSegments[$i] ` -Value ([PSCustomObject]@{}) } $settingContainer = $settingContainer.PSObject.Properties[$settingNameSegments[$i]].Value } } return $settings } hidden [PSCustomObject] ReadSettingsFile([string]$filePath) { if ($true -eq (Test-Path -LiteralPath $filePath -PathType Leaf)) { return Get-Content $filePath | ConvertFrom-Json } return $null } hidden [PSCustomObject] GetHostSettingsFile() { return $this.ReadSettingsFile($this.GetHostSettingsFilePath()) } hidden [PSCustomObject] GetGameSettingsFile() { return $this.ReadSettingsFile($this.GetGameSettingsFilePath()) } hidden [PSCustomObject] GetVoipSettingsFile() { return $this.ReadSettingsFile($this.GetVoipSettingsFilePath()) } hidden [string] GetDefaultHostSettingsFilePath() { return Join-Path -Path $this.GetDefaultSettingsDirPath() -ChildPath 'ServerHostSettings.json' } hidden [string] GetDefaultGameSettingsFilePath() { return Join-Path -Path $this.GetDefaultSettingsDirPath() -ChildPath 'ServerGameSettings.json' } hidden [string] GetHostSettingsFilePath() { return Join-Path -Path $this.GetSettingsDirPath() -ChildPath 'ServerHostSettings.json' } hidden [string] GetGameSettingsFilePath() { return Join-Path -Path $this.GetSettingsDirPath() -ChildPath 'ServerGameSettings.json' } hidden [string] GetVoipSettingsFilePath() { return Join-Path -Path $this.GetSettingsDirPath() -ChildPath 'ServerVoipSettings.json' } hidden [PSCustomObject] GetDefaultHostSettingsFile() { return $this.ReadSettingsFile($this.GetDefaultHostSettingsFilePath()) } hidden [PSCustomObject] GetDefaultGameSettingsFile() { return $this.ReadSettingsFile($this.GetDefaultGameSettingsFilePath()) } hidden [string] GetDefaultSettingsDirPath() { return Join-Path -Path $this._properties.ReadProperty('InstallDir') -ChildPath 'VRisingServer_Data' | Join-Path -ChildPath 'StreamingAssets' | Join-Path -ChildPath 'Settings' } hidden [string] GetSettingsDirPath() { return Join-Path -Path $this._properties.ReadProperty('DataDir') -ChildPath ([VRisingServerSettings]::SETTINGS_DIR_NAME) } hidden [PSCustomObject] GetDefaultVoipSettingsFile() { return [PSCustomObject]@{ VOIPEnabled = $false VOIPIssuer = $null VOIPSecret = $null VOIPAppUserId = $null VOIPAppUserPwd = $null VOIPVivoxDomain = $null VOIPAPIEndpoint = $null VOIPConversationalDistance = $null VOIPAudibleDistance = $null VOIPFadeIntensity = $null } } } class VRisingServerSettingsMap { static [hashtable] $_map static VRisingServerSettingsMap() { [VRisingServerSettingsMap]::_map = @{ Host = @{ Secure = [bool] ListOnMasterServer = [bool] AdminOnlyDebugEvents = [bool] DisableDebugEvents = [bool] Rcon = @{ Enabled = [bool] } } Game = @{ GameModeType = @('PvP', 'PvE') CastleDamageMode = @('Always', 'Never', 'TimeRestricted') SiegeWeaponHealth = @('VeryLow', 'Low', 'Normal', 'High', 'VeryHigh') PlayerDamageMode = @('Always', 'TimeRestricted') CastleHeartDamageMode = @('CanBeDestroyedOnlyWhenDecaying', 'CanBeDestroyedByPlayers', 'CanBeSeizedOrDestroyedByPlayers') PvPProtectionMode = @('Disabled', 'VeryShort', 'Short', 'Medium', 'Long') DeathContainerPermission = @('Anyone', 'ClanMembers', 'OnlySelf') RelicSpawnType = @('Unique', 'Plentiful') CanLootEnemyContainers = [bool] BloodBoundEquipment = [bool] TeleportBoundItems = [bool] AllowGlobalChat = [bool] AllWaypointsUnlocked = [bool] FreeCastleClaim = [bool] FreeCastleDestroy = [bool] InactivityKillEnabled = [bool] DisableDisconnectedDeadEnabled = [bool] AnnounceSiegeWeaponSpawn = [bool] ShowSiegeWeaponMapIcon = [bool] # "VBloodUnitSettings": [] # TODO UNKNOWN CONTENTS # "UnlockedAchievements": [] # TODO UNKNOWN CONTENTS # "UnlockedResearchs": [] # TODO UNKNOWN CONTENTS PlayerInteractionSettings = @{ TimeZone = @('Local', 'UTC', 'PST', 'CET', 'CST') } } Voip = @{ VOIPEnabled = [bool] } } } static [psobject[]] Get([string]$settingsType, [string]$settingName) { # split on dots $splitSettingName = $settingName -split '\.' $settingLeafName = $splitSettingName[-1] $mappedSettings = $null if ([string]::IsNullOrEmpty($splitSettingName)) { return $null } elseif ($splitSettingName.Count -eq 1) { $mappedSettings = [VRisingServerSettingsMap]::_map[$settingsType] } elseif ($splitSettingName.Count -gt 1) { # loop into the settings object # based on the number of segments to the search key $subSettings = [VRisingServerSettingsMap]::_map[$settingsType] for ($i = 0; $i -lt $splitSettingName.Count; $i++) { if ($i -eq ($splitSettingName.Count - 1)) { # last item } else { if ($false -eq $subSettings.ContainsKey($splitSettingName[$i])) { # invalid sub key (given foo.bar, foo does not exist) return $null } $subSettings = $subSettings[$splitSettingName[$i]] } } $mappedSettings = $subSettings } if ($false -eq $mappedSettings.ContainsKey($settingLeafName)) { return $null } else { if ($mappedSettings[$settingLeafName] -is [System.Reflection.TypeInfo]) { switch ($mappedSettings[$settingLeafName].Name) { 'Boolean' { return @($true,$false) } Default { return $null } } } elseif ($mappedSettings[$settingLeafName] -is [array]) { return $mappedSettings[$settingLeafName] } } return $null } } enum VRisingServerSettingsType { Host Game Voip } function ExportAliases { $commandAliases = @{ vrget = 'Get-VRisingServer' vrcreate = 'New-VRisingServer' vrdelete = 'Remove-VRisingServer' vrmget = 'Get-VRisingServerManagerConfigOption' vrmset = 'Set-VRisingServerManagerConfigOption' vrstart = 'Start-VRisingServer' vrstop = 'Stop-VRisingServer' vrupdate = 'Update-VRisingServer' vrlog = 'Read-VRisingServerLog' vrenable = 'Enable-VRisingServer' vrdisable = 'Disable-VRisingServer' vrrestart = 'Restart-VRisingServer' vrset = 'Set-VRisingServer' } foreach ($commandAlias in $commandAliases.GetEnumerator()) { New-Alias -Value $commandAlias.Value -Name $commandAlias.Key -Scope Script } } function LoadServerManagerFlags { # had to stash this behavior into a function to be able to suppress the global warning from PSScriptAnalyzer [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Scope='Function')] param() if ($null -eq $Global:VRisingServerManagerFlags) { return @{} } else { return $Global:VRisingServerManagerFlags } } function ServerManagerOptionArgumentCompleter { [OutputType([System.Management.Automation.CompletionResult])] param( [string] $CommandName, [string] $ParameterName, [string] $WordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [System.Collections.IDictionary] $FakeBoundParameters ) $serverManagerOptions = [VRisingServer]::GetConfigKeys() -like "$WordToComplete*" foreach ($serverManagerOption in $serverManagerOptions) { [System.Management.Automation.CompletionResult]::New($serverManagerOption) } } function ServerSettingsFileArgumentCompleter { [OutputType([System.Management.Automation.CompletionResult])] param( [string] $CommandName, [string] $ParameterName, [string] $WordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [System.Collections.IDictionary] $FakeBoundParameters ) if ($FakeBoundParameters.ShortName.Count -gt 1) { $shortName = $FakeBoundParameters.ShortName[0] } else { $shortName = $FakeBoundParameters.ShortName } switch ($ParameterName) { 'SettingName' { # Foo -> # <- FooBar # <- FooBaz # Foo. -> # <- Foo.Foo # <- Foo.Bar # Foo.F -> # <- Foo.Foo # Foo.F.Foo -> # <- (null) -- don't fuzzy search on middle if ($false -eq $FakeBoundParameters.ContainsKey('ShortName')) { return } $server = Get-VRisingServer -ShortName $shortName if ($null -eq $server) { return } if ($false -eq $FakeBoundParameters.ContainsKey('SettingsType')) { return } $serverSettingsKeys = $server._settings.FindSettingsTypeKeys($FakeBoundParameters.SettingsType, "*$WordToComplete*") foreach ($settingsKey in $serverSettingsKeys) { [System.Management.Automation.CompletionResult]::New($settingsKey) } return } 'SettingValue' { if ($false -eq $FakeBoundParameters.ContainsKey('SettingsType')) { return } if ($false -eq $FakeBoundParameters.ContainsKey('SettingName')) { return } # take type # lookup name in type map # if value type (from map) is a collection type (has multiple known values): # - return collection matching input, sorted preferring input # - e.g. Host ListOnMasterServer '' -> True / False # Host ListOnMasterServer 'Fa' -> False / True # if value type is not a collection type (has multiple known values): # - reach into settings # - extract the current or default value # - e.g. Host AutoSaveCount '' -> 50 $suggestedValues = $null $mapResults = [VRisingServerSettingsMap]::Get( $FakeBoundParameters.SettingsType, $FakeBoundParameters.SettingName) if (($null -ne $mapResults) -and ($mapResults.Count -gt 0)) { # sort array results by those like result $sortedMapResults = [System.Collections.ArrayList]::New() foreach ($mapResult in $mapResults) { if ($mapResult -like "$WordToComplete*") { $sortedMapResults.Insert(0, $mapResult) } else { $sortedMapResults.Add($mapResult) } } $suggestedValues = $sortedMapResults.ToArray() } # value does not have known values, try to grab current value from server instead if ($false -eq [string]::IsNullOrWhiteSpace($FakeBoundParameters.ShortName)) { $server = [VRisingServer]::GetServer($FakeBoundParameters.ShortName) $suggestedValues = $server._settings.GetSettingsTypeValue( $FakeBoundParameters.SettingsType, $FakeBoundParameters.SettingName) } foreach ($suggestedValue in $suggestedValues) { if ($suggestedValue -is [System.String]) { [System.Management.Automation.CompletionResult]::New("`"$suggestedValue`"") } else { [System.Management.Automation.CompletionResult]::New($suggestedValue) } } return } Default { return } } } function ServerShortNameArgumentCompleter { [OutputType([System.Management.Automation.CompletionResult])] param( [string] $CommandName, [string] $ParameterName, [string] $WordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [System.Collections.IDictionary] $FakeBoundParameters ) $serverShortNames = [VRisingServer]::GetShortNames() -like "$WordToComplete*" foreach ($serverShortName in $serverShortNames) { [System.Management.Automation.CompletionResult]::New($serverShortName) } } function Disable-VRisingServer { [CmdletBinding(DefaultParameterSetName='ByShortName')] param ( [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(ParameterSetName='ByServer', ValueFromPipeline=$true)] [VRisingServer] $Server ) process { if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($serverItem in $servers) { try { $serverItem.Disable() } catch [VRisingServerException] { Write-Error $_.Exception continue } } } } Register-ArgumentCompleter -CommandName Disable-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter function Enable-VRisingServer { [CmdletBinding(DefaultParameterSetName='ByShortName')] param ( [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(ParameterSetName='ByServer', ValueFromPipeline=$true)] [VRisingServer] $Server ) process { if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($serverItem in $servers) { try { $serverItem.Enable() } catch [VRisingServerException] { Write-Error $_.Exception continue } } } } Register-ArgumentCompleter -CommandName Enable-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter function Get-VRisingServer { [CmdletBinding()] param ( [Parameter(Position=0)] [string[]] $ShortName, [Parameter(Position=1)] [VRisingServerSettingsType] $SettingsType, [Parameter(Position=2)] [string] $SettingName ) if ($null -ne $SettingsType) { $servers = [VRisingServer]::FindServers($ShortName) foreach ($server in $servers) { switch ($SettingsType) { ([VRisingServerSettingsType]::Host) { $server._settings.GetHostSetting($SettingName) } ([VRisingServerSettingsType]::Game) { $server._settings.GetGameSetting($SettingName) } ([VRisingServerSettingsType]::Voip) { $server._settings.GetVoipSetting($SettingName) } } } } else { return [VRisingServer]::FindServers($ShortName) } } Register-ArgumentCompleter -CommandName Get-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter Register-ArgumentCompleter -CommandName Get-VRisingServer -ParameterName SettingName -ScriptBlock $function:ServerSettingsFileArgumentCompleter function Get-VRisingServerManagerConfigOption { [CmdletBinding()] param( [Parameter(Mandatory=$true, Position=0)] $Option ) [VRisingServer]::GetConfigValue($Option) } Register-ArgumentCompleter -CommandName Get-VRisingServerManagerConfigOption -ParameterName Option -ScriptBlock $function:ServerManagerOptionArgumentCompleter function New-VRisingServer { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory=$true)] [string] $ShortName ) if ($true -eq $PSCmdlet.ShouldProcess($ShortName)) { [VRisingServer]::CreateServer($ShortName) } } function Read-VRisingServerLog { [CmdletBinding(DefaultParameterSetName='ByShortName')] param( [Parameter(Position=1, ParameterSetName='ByShortName')] [Parameter(Position=1, ParameterSetName='ByServer')] [VRisingServerLogType] $LogType = [VRisingServerLogType]::Server, [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(Position=0, ParameterSetName='ByServer', ValueFromPipeline=$true)] [VRisingServer] $Server, [Parameter()] [Alias('Tail')] [int]$Last, [Parameter()] [switch]$Follow ) process { if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($serverItem in $servers) { $logFile = $serverItem._properties.GetLogFilePath($LogType) if ($false -eq [string]::IsNullOrWhiteSpace($logFile)) { $shortName = $($serverItem._properties.ReadProperty('ShortName')) $getContentParams = @{ LiteralPath = $logFile } if ($Last -gt 0) { $getContentParams['Last'] = $Last } if ($true -eq $Follow) { $getContentParams['Wait'] = $true $keepFollowing = $true while ($true -eq $keepFollowing) { try { Get-Content @getContentParams | ForEach-Object { "[$shortName] $_" } $keepFollowing = $false } catch [System.IO.FileNotFoundException],[System.Management.Automation.ItemNotFoundException] { # allow following a log file that doesn't exist yet # or that gets rotated out from under it while being watched Start-Sleep -Seconds 1 continue } } } else { Get-Content @getContentParams | ForEach-Object { "[$shortName] $_" } } } } } } Register-ArgumentCompleter -CommandName Read-VRisingServerLog -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter function Remove-VRisingServer { [CmdletBinding(DefaultParameterSetName='ByShortName', SupportsShouldProcess, ConfirmImpact='High')] param ( [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(ParameterSetName='ByServer', ValueFromPipeline=$true)] [VRisingServer] $Server, [Parameter()] [switch] $Force ) process { if ($Force){ $ConfirmPreference = 'None' } if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($serverItem in $servers) { try { if ($true -eq $PSCmdlet.ShouldProcess($serverItem.ShortName)) { [VRisingServer]::DeleteServer($serverItem, $Force) } } catch [VRisingServerException] { Write-Error $_.Exception continue } } } } Register-ArgumentCompleter -CommandName Remove-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter function Restart-VRisingServer { [CmdletBinding(DefaultParameterSetName='ByShortName', SupportsShouldProcess)] param ( [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(Position=0, ParameterSetName='ByServer', ValueFromPipeline=$true)] [VRisingServer] $Server, [Parameter()] [switch] $Force ) process { if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($serverItem in $servers) { try { if ($PSCmdlet.ShouldProcess($serverItem.ShortName)) { $serverItem.Restart($Force) } } catch [VRisingServerException] { Write-Error $_.Exception continue } } } } Register-ArgumentCompleter -CommandName Restart-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter function Set-VRisingServer { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(ParameterSetName='ByServer')] [VRisingServer] $Server, [Parameter(Position=1)] [VRisingServerSettingsType] $SettingsType, [Parameter(Position=2)] [string] $SettingName, [Parameter(Position=3)] [psobject] $SettingValue, [Parameter()] [switch] $Default ) if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($server in $servers) { if ($PSCmdlet.ShouldProcess($server.ShortName)) { switch ($SettingsType) { ([VRisingServerSettingsType]::Host) { $server._settings.SetHostSetting($SettingName, $SettingValue, $Default) } ([VRisingServerSettingsType]::Game) { $server._settings.SetGameSetting($SettingName, $SettingValue, $Default) } ([VRisingServerSettingsType]::Voip) { $server._settings.SetVoipSetting($SettingName, $SettingValue, $Default) } } } } } Register-ArgumentCompleter -CommandName Set-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter Register-ArgumentCompleter -CommandName Set-VRisingServer -ParameterName SettingName -ScriptBlock $function:ServerSettingsFileArgumentCompleter Register-ArgumentCompleter -CommandName Set-VRisingServer -ParameterName SettingValue -ScriptBlock $function:ServerSettingsFileArgumentCompleter function Set-VRisingServerManagerConfigOption { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory=$true, Position=0)] $Option, [Parameter(Position=1)] [psobject]$Value ) if ($PSCmdlet.ShouldProcess($Option)) { [VRisingServer]::SetConfigValue($Option, $Value) } } Register-ArgumentCompleter -CommandName Set-VRisingServerManagerConfigOption -ParameterName Option -ScriptBlock $function:ServerManagerOptionArgumentCompleter function Start-VRisingServer { [CmdletBinding(DefaultParameterSetName='ByShortName', SupportsShouldProcess)] param ( [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(ParameterSetName='ByServer', ValueFromPipeline=$true)] [VRisingServer] $Server ) process { if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($serverItem in $servers) { try { if ($PSCmdlet.ShouldProcess($serverItem.ShortName)) { $serverItem.Start() } } catch [VRisingServerException] { Write-Error $_.Exception continue } } } } Register-ArgumentCompleter -CommandName Start-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter function Stop-VRisingServer { [CmdletBinding(DefaultParameterSetName='ByShortName', SupportsShouldProcess)] param ( [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(Position=0, ParameterSetName='ByServer', ValueFromPipeline=$true)] [VRisingServer] $Server, [Parameter()] [switch] $Force ) process { if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($serverItem in $servers) { try { if ($PSCmdlet.ShouldProcess($serverItem.ShortName)) { $serverItem.Stop($Force) } } catch [VRisingServerException] { Write-Error $_.Exception continue } } } } Register-ArgumentCompleter -CommandName Stop-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter function Update-VRisingServer { [CmdletBinding(DefaultParameterSetName='ByShortName', SupportsShouldProcess)] param ( [Parameter(Position=0, ParameterSetName='ByShortName')] [string[]] $ShortName, [Parameter(Position=0, ParameterSetName='ByServer', ValueFromPipeline=$true)] [VRisingServer] $Server ) process { if ($PSCmdlet.ParameterSetName -eq 'ByShortName') { $servers = [VRisingServer]::FindServers($ShortName) } else { $servers = @($Server) } foreach ($serverItem in $servers) { try { if ($PSCmdlet.ShouldProcess($serverItem.ShortName)) { $serverItem.Update() } } catch [VRisingServerException] { Write-Error $_.Exception continue } } } } Register-ArgumentCompleter -CommandName Update-VRisingServer -ParameterName ShortName -ScriptBlock $function:ServerShortNameArgumentCompleter ExportAliases $Global:PSDefaultParameterValues['Start-VRisingServer:ErrorAction'] = 'Continue' $Global:PSDefaultParameterValues['Stop-VRisingServer:ErrorAction'] = 'Continue' $Global:PSDefaultParameterValues['Update-VRisingServer:ErrorAction'] = 'Continue' $Global:PSDefaultParameterValues['Remove-VRisingServer:ErrorAction'] = 'Continue' $Global:PSDefaultParameterValues['Enable-VRisingServer:ErrorAction'] = 'Continue' $Global:PSDefaultParameterValues['Disable-VRisingServer:ErrorAction'] = 'Continue' $Global:PSDefaultParameterValues['Restart-VRisingServer:ErrorAction'] = 'Continue' # custom formatters Update-FormatData -AppendPath "$PSScriptRoot\VRisingServerManager.Format.ps1xml" # Load Flags $script:VRisingServerManagerFlags = LoadServerManagerFlags # Optionally Enable Log Timestamps if ($true -eq $script:VRisingServerManagerFlags.ShowDateTime) { [VRisingServerLog]::ShowDateTime = $true } # Get Module Version $script:ModuleVersion = (Import-PowerShellDataFile -Path (Join-Path -Path $PSScriptRoot -ChildPath 'VRisingServerManager.psd1')).ModuleVersion [VRisingServerLog]::Info("VRisingServerManager v$ModuleVersion") # List Flags $enabledFlags = $script:VRisingServerManagerFlags.GetEnumerator() | Where-Object { $_.Value -eq $true } | Select-Object -ExpandProperty Name if ($enabledFlags.Count -gt 0) { [VRisingServerLog]::Info("Using VRisingServerManagerFlags: $enabledFlags") } # check for new version $skipNewVersionCheck = [VRisingServer]::GetConfigValue('SkipNewVersionCheck') if (($true -ne $script:VRisingServerManagerFlags.SkipNewVersionCheck) -and ($true -ne $skipNewVersionCheck)) { $private:latestModule = Find-Module ` -Name 'VRisingServerManager' ` -Repository PSGallery ` -MinimumVersion $ModuleVersion if ($ModuleVersion -ne $private:latestModule.Version) { $releaseNotesList = $private:latestModule.ReleaseNotes.Split(@("`r`n", "`r", "`n"), [System.StringSplitOptions]::None) [VRisingServerLog]::Warning('-- New Version Available! --') [VRisingServerLog]::Warning('') [VRisingServerLog]::Warning("Current Version: $ModuleVersion") [VRisingServerLog]::Warning(" Latest Version: $($private:latestModule.Version)") [VRisingServerLog]::Warning('') [VRisingServerLog]::Warning('To upgrade:') [VRisingServerLog]::Warning(' 1. Stop all active monitors') [VRisingServerLog]::Warning(' run: vrdisable') [VRisingServerLog]::Warning(" 2. Wait for monitors to stop and become 'Disabled'") [VRisingServerLog]::Warning(' run: vrget') [VRisingServerLog]::Warning(" 3. Update the module") [VRisingServerLog]::Warning(' run: Update-Module -Name VRisingServerManager') [VRisingServerLog]::Warning(" 4. Exit the current PowerShell session") [VRisingServerLog]::Warning(" 5. Start a new PowerShell session") [VRisingServerLog]::Warning(" 6. Import the module") [VRisingServerLog]::Warning(' run: Import-Module VRisingServerManager') [VRisingServerLog]::Warning(" 7. Re-Enable all monitors") [VRisingServerLog]::Warning(' run: vrenable') [VRisingServerLog]::Warning('') [VRisingServerLog]::Warning('To disable checking for new versions, run:') [VRisingServerLog]::Warning(' vrmset SkipNewVersionCheck $true') [VRisingServerLog]::Warning('') [VRisingServerLog]::Warning("-- Release Notes --") [VRisingServerLog]::Warning($releaseNotesList) } else { [VRisingServerLog]::Info("You are using the latest version -- run: `'vrmset SkipNewVersionCheck `$true' to disable checking for new versions") } } else { [VRisingServerLog]::Info("Skipped new version check -- run: `'vrmset SkipNewVersionCheck `$false' to enable checking for new versions") } |