param(
[Parameter(Position = 0, Mandatory = $true)]
[Alias("n")]
[string]$scriptName,
[Alias("t")]
[Parameter(Position = 1, Mandatory = $false)]
[int]$terminate
)
$path = (Split-Path $MyInvocation.MyCommand.Path -Parent)
Set-Location $path
$script:attempt = 0
function OnStreamEndAsJob() {
return Start-Job -Name "$scriptName-OnStreamEnd" -ScriptBlock {
param($path, $scriptName, $arguments)
function Write-Debug($message){
if ($arguments['debug']) {
Write-Host "DEBUG: $message"
}
}
Write-Host $arguments
Write-Debug "Setting location to $path"
Set-Location $path
Write-Debug "Loading Helpers.ps1 with script name $scriptName"
. .\Helpers.ps1 -n $scriptName
Write-Debug "Loading Events.ps1 with script name $scriptName"
. .\Events.ps1 -n $scriptName
Write-Host "Stream has ended, now invoking code"
Write-Debug "Creating pipe with name $scriptName-OnStreamEnd"
$job = Create-Pipe -pipeName "$scriptName-OnStreamEnd"
while ($true) {
$maxTries = 25
$tries = 0
Write-Debug "Checking job state: $($job.State)"
if ($job.State -eq "Completed") {
Write-Host "Another instance of $scriptName has been started again. This current session is now redundant and will terminate without further action."
Write-Debug "Job state is 'Completed'. Exiting loop."
break;
}
Write-Debug "Invoking OnStreamEnd with arguments: $arguments"
if ((OnStreamEnd $arguments)) {
Write-Debug "OnStreamEnd returned true. Exiting loop."
break;
}
if ((IsCurrentlyStreaming)) {
Write-Host "Streaming is active. To prevent potential conflicts, this script will now terminate prematurely."
}
while (($tries -lt $maxTries) -and ($job.State -ne "Completed")) {
Start-Sleep -Milliseconds 200
$tries++
}
}
Write-Debug "Sending 'Terminate' message to pipe $scriptName-OnStreamEnd"
Send-PipeMessage "$scriptName-OnStreamEnd" Terminate
} -ArgumentList $path, $scriptName, $script:arguments
}
function IsCurrentlyStreaming() {
$sunshineProcess = Get-Process sunshine -ErrorAction SilentlyContinue
if ($null -eq $sunshineProcess) {
return $false
}
return $null -ne (Get-NetUDPEndpoint -OwningProcess $sunshineProcess.Id -ErrorAction Ignore)
}
function Stop-Script() {
Send-PipeMessage -pipeName $scriptName Terminate
}
function Send-PipeMessage($pipeName, $message) {
Write-Debug "Attempting to send message to pipe: $pipeName"
$pipeExists = Get-ChildItem -Path "\\.\pipe\" | Where-Object { $_.Name -eq $pipeName }
Write-Debug "Pipe exists check: $($pipeExists.Length -gt 0)"
if ($pipeExists.Length -gt 0) {
$pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", $pipeName, [System.IO.Pipes.PipeDirection]::Out)
Write-Debug "Connecting to pipe: $pipeName"
$pipe.Connect(3000)
$streamWriter = New-Object System.IO.StreamWriter($pipe)
Write-Debug "Sending message: $message"
$streamWriter.WriteLine($message)
try {
$streamWriter.Flush()
$streamWriter.Dispose()
$pipe.Dispose()
Write-Debug "Message sent and resources disposed successfully."
}
catch {
Write-Debug "Error during disposal: $_"
# We don't care if the disposal fails, this is common with async pipes.
# Also, this powershell script will terminate anyway.
}
}
else {
Write-Debug "Pipe not found: $pipeName"
}
}
function Create-Pipe($pipeName) {
return Start-Job -Name "$pipeName-PipeJob" -ScriptBlock {
param($pipeName, $scriptName)
Register-EngineEvent -SourceIdentifier $scriptName -Forward
$pipe = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName, [System.IO.Pipes.PipeDirection]::In, 10, [System.IO.Pipes.PipeTransmissionMode]::Byte, [System.IO.Pipes.PipeOptions]::Asynchronous)
$streamReader = New-Object System.IO.StreamReader($pipe)
Write-Host "Waiting for named pipe to recieve kill command"
$pipe.WaitForConnection()
$message = $streamReader.ReadLine()
if ($message) {
Write-Host "Terminating pipe..."
$pipe.Dispose()
$streamReader.Dispose()
New-Event -SourceIdentifier $scriptName -MessageData $message
}
} -ArgumentList $pipeName, $scriptName
}
function Remove-OldLogs {
# Get all log files in the directory
$logFiles = Get-ChildItem -Path './logs' -Filter "log_*.txt" -ErrorAction SilentlyContinue
# Sort the files by creation time, oldest first
$sortedFiles = $logFiles | Sort-Object -Property CreationTime -ErrorAction SilentlyContinue
if ($sortedFiles) {
# Calculate how many files to delete
$filesToDelete = $sortedFiles.Count - 10
# Check if there are more than 10 files
if ($filesToDelete -gt 0) {
# Delete the oldest files, keeping the latest 10
$sortedFiles[0..($filesToDelete - 1)] | Remove-Item -Force
}
}
}
function Start-Logging {
# Get the current timestamp
$timeStamp = [int][double]::Parse((Get-Date -UFormat "%s"))
$logDirectory = "./logs"
# Define the path and filename for the log file
$logFileName = "log_$timeStamp.txt"
$logFilePath = Join-Path $logDirectory $logFileName
# Check if the log directory exists, and create it if it does not
if (-not (Test-Path $logDirectory)) {
New-Item -Path $logDirectory -ItemType Directory
}
# Start logging to the log file
Start-Transcript -Path $logFilePath
}
function Stop-Logging {
Stop-Transcript
}
function Get-Settings {
# Read the file content
$jsonContent = Get-Content -Path ".\settings.json" -Raw
# Remove single line comments
$jsonContent = $jsonContent -replace '//.*', ''
# Remove multi-line comments
$jsonContent = $jsonContent -replace '/\*[\s\S]*?\*/', ''
# Remove trailing commas from arrays and objects
$jsonContent = $jsonContent -replace ',\s*([\]}])', '$1'
try {
# Convert JSON content to PowerShell object
$jsonObject = $jsonContent | ConvertFrom-Json
return $jsonObject
}
catch {
Write-Error "Failed to parse JSON: $_"
}
}
function Update-JsonProperty {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$FilePath,
[Parameter(Mandatory = $true)]
[string]$Property,
[Parameter(Mandatory = $true)]
[object]$NewValue
)
# Read the file as a single string.
$content = Get-Content -Path $FilePath -Raw
# Remove comments (both // and /* */ style) and trailing commas
$strippedContent = $content -replace '//.*?(\r?\n|$)', '$1' # Remove single line comments
$strippedContent = $strippedContent -replace '/\*[\s\S]*?\*/', '' # Remove multi-line comments
$strippedContent = $strippedContent -replace ',(\s*[\]}])', '$1' # Remove trailing commas
# Format the new value properly
if ($NewValue -is [string]) {
# Convert the string to a JSON-compliant string.
$formattedValue = (ConvertTo-Json $NewValue -Compress)
} else {
$formattedValue = $NewValue.ToString().ToLower()
}
# Build a regex pattern for matching the property.
$escapedProperty = [regex]::Escape($Property)
$pattern = '"' + $escapedProperty + '"\s*:\s*[^,}\r\n]+'
# Check if the property exists in the stripped content
if ($strippedContent -match $pattern) {
# Property exists, just update its value in the original content
$replacement = '"' + $Property + '": ' + $formattedValue
$updatedContent = [regex]::Replace($content, $pattern, $replacement)
} else {
# Property doesn't exist, need to add it
# Find the last property in the JSON object
$lastPropPattern = ',?\s*"([^"]+)"\s*:\s*[^,}\r\n]+'
if ($content -match '}\s*$') {
# Find the last occurrence of a property
$lastMatch = [regex]::Matches($content, $lastPropPattern)[-1]
$lastPropIndex = $lastMatch.Index + $lastMatch.Length
# Check if the last property ends with a comma
$endsWithComma = $content.Substring($lastPropIndex).TrimStart() -match '^\s*,'
# Prepare the new property string
$newPropString = ""
if (!$endsWithComma) {
$newPropString += ","
}
$newPropString += "`n `"$Property`": $formattedValue"
# Insert the new property before the closing brace
$closingBraceIndex = $content.LastIndexOf('}')
$updatedContent = $content.Substring(0, $closingBraceIndex).TrimEnd() +
$newPropString +
"`n}" +
$content.Substring($closingBraceIndex + 1)
} else {
Write-Error "Unable to find a proper location to insert the new property. JSON file may be malformed."
return
}
}
# Write the updated content back.
Set-Content -Path $FilePath -Value $updatedContent
}
function Wait-ForStreamEndJobToComplete() {
$job = OnStreamEndAsJob
while ($job.State -ne "Completed") {
$job | Receive-Job
Start-Sleep -Seconds 1
}
$job | Wait-Job | Receive-Job
}
if ($terminate -eq 1) {
Write-Host "Stopping Script"
Stop-Script | Out-Null
}