# Shared helpers for the sequential Nova128 editor/compiler launchers (E07-S02). # Import-Module (Join-Path $PSScriptRoot "lib\Nova128Launcher.psm1") -Force $script:ViceBin = "C:\Program Files\GTK3VICE-3.10-win64\bin" function Get-Nova128ViceTool([string]$Name) { $path = Join-Path $script:ViceBin "$Name.exe" if (-not (Test-Path -LiteralPath $path)) { throw "$Name.exe not found at $path" } return $path } # One writable session per image directory. The lock holds the owning PID and # kind; a lock whose process is gone is stale and is replaced. function Enter-Nova128ImageSession { param( [Parameter(Mandatory)][string]$Image, [Parameter(Mandatory)][ValidateSet("editor", "compiler")][string]$Kind ) $folder = Split-Path ([IO.Path]::GetFullPath($Image)) -Parent New-Item -ItemType Directory -Force -Path $folder | Out-Null $lockPath = Join-Path $folder ".newlang-session.lock" if (Test-Path -LiteralPath $lockPath) { # A live holder keeps the file open without sharing, so even reading it # can fail. That is the same answer as a readable live PID. $text = "" try { $text = Get-Content -Raw -LiteralPath $lockPath } catch { throw "another Nova128 session already holds $Image" } $owner = 0 $ownerKind = "another" if ($text -match '(?m)^(\d+)\s+(\S+)') { $owner = [int]$Matches[1] $ownerKind = $Matches[2] } $alive = $owner -gt 0 -and $null -ne (Get-Process -Id $owner -ErrorAction SilentlyContinue) if ($alive) { throw "the $ownerKind session (pid $owner) still owns $Image; close it before starting another" } Remove-Item -LiteralPath $lockPath -Force } try { $stream = [IO.File]::Open($lockPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) } catch { throw "another Nova128 session already holds $Image" } $writer = [IO.StreamWriter]::new($stream) $writer.WriteLine("$PID $Kind") $writer.Flush() return [pscustomobject]@{ Path = $lockPath; Stream = $stream; Writer = $writer Image = $Image; Kind = $Kind } } function Exit-Nova128ImageSession { param([Parameter(Mandatory)]$Session) try { $Session.Writer.Dispose() $Session.Stream.Dispose() } finally { if (Test-Path -LiteralPath $Session.Path) { Remove-Item -LiteralPath $Session.Path -Force } if ($Session.Image) { Wait-Nova128ImageFlush -Image $Session.Image } } } # The emulator has exited, but DOS writes may still be in flight. Return only # when the image can be opened exclusively. function Wait-Nova128ImageFlush { param( [Parameter(Mandatory)][string]$Image, [int]$TimeoutSeconds = 30 ) if (-not (Test-Path -LiteralPath $Image)) { return } $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) while ($true) { try { $stream = [IO.File]::Open($Image, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None) $stream.Dispose() return } catch { if ([DateTime]::UtcNow -ge $deadline) { throw "the emulator exited but $Image is still locked (image not flushed)" } Start-Sleep -Milliseconds 200 } } } # Start an emulator and wait for it to exit. Autostart uses the host program # path, so the program is deterministic; the disk image stays attached for the # session's own reads and writes. # Write one file into a disk image and prove it appears in the directory. function Add-Nova128DiskFile { param( [Parameter(Mandatory)][string]$Image, [Parameter(Mandatory)][string]$HostPath, [Parameter(Mandatory)][string]$DiskName, [ValidateSet("prg", "seq")][string]$Type = "prg" ) $c1541 = Get-Nova128ViceTool "c1541" if (-not (Test-Path -LiteralPath $HostPath -PathType Leaf)) { throw "file not found: $HostPath" } $destination = if ($Type -eq "seq") { "$DiskName,s" } else { $DiskName } $output = (& $c1541 $Image -write $HostPath $destination 2>&1) -join "`n" if ($LASTEXITCODE -ne 0) { throw "c1541 could not write '$DiskName' to $Image`n$output" } $listing = (& $c1541 $Image -list 2>&1) -join "`n" $pattern = '(?im)"' + [regex]::Escape($DiskName) + '"\s+' + $Type if ($listing -notmatch $pattern) { throw "$DiskName ($Type) is missing from $Image after writing" } } function Invoke-Nova128Emulator { param( [Parameter(Mandatory)][ValidateSet("x64sc", "x128")][string]$Emulator, [Parameter(Mandatory)][string]$Program, [string]$DiskImage, [string]$Screenshot, [int]$LimitCycles = 0, [switch]$Warp, [switch]$EightyColumn, [switch]$ProgramOnDisk ) $tool = Get-Nova128ViceTool $Emulator $workingDirectory = $null $autostart = $Program if ($ProgramOnDisk) { # The editor takes its write device from the disk it was loaded from. # Autostarting the copy on the session disk is what makes its saves # reach the image; a host-injected program has no such device. if (-not $DiskImage) { throw "a disk-resident program needs -DiskImage" } $autostart = [IO.Path]::GetFileName($DiskImage) + ":" + $Program $workingDirectory = Split-Path ([IO.Path]::GetFullPath($DiskImage)) -Parent } elseif (-not (Test-Path -LiteralPath $Program)) { throw "program not found: $Program" } $arguments = @() if ($DiskImage) { if (-not (Test-Path -LiteralPath $DiskImage)) { throw "disk image not found: $DiskImage" } $arguments += @("-drive8type", "1581", "-drive8truedrive", "-8", ('"' + $DiskImage + '"')) } if ($EightyColumn) { $arguments += "-80col" } $arguments += @("-autostart", ('"' + $autostart + '"'), "-autostart-warp", "-sounddev", "dummy") if ($Warp) { $arguments += "-warp" } if ($LimitCycles -gt 0) { $arguments += @("-limitcycles", "$LimitCycles") } if ($Screenshot) { if (Test-Path -LiteralPath $Screenshot) { Remove-Item -LiteralPath $Screenshot -Force } # x64sc's plain -exitscreenshot is the VIC-II screen. On x128 the # 80-column editor draws on the VDC, which only the plain option # captures; -exitscreenshotvicii would save a blank 40-column screen. $shotOption = if ($Emulator -eq "x64sc" -or $EightyColumn) { "-exitscreenshot" } else { "-exitscreenshotvicii" } $arguments += @($shotOption, ('"' + $Screenshot + '"')) } Write-Host ("launching {0} with {1}" -f $Emulator, $autostart) $startArgs = @{ FilePath = $tool; ArgumentList = $arguments; PassThru = $true; Wait = $true } if ($workingDirectory) { $startArgs.WorkingDirectory = $workingDirectory } $process = Start-Process @startArgs if ($Screenshot -and -not (Test-Path -LiteralPath $Screenshot)) { throw "$Emulator did not create the exit screenshot $Screenshot" } if ($DiskImage) { Wait-Nova128ImageFlush -Image $DiskImage } return $process.ExitCode } Export-ModuleMember -Function Get-Nova128ViceTool, Enter-Nova128ImageSession, Exit-Nova128ImageSession, Wait-Nova128ImageFlush, Add-Nova128DiskFile, Invoke-Nova128Emulator