Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions scripts/lib/install-lib.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -568,14 +568,31 @@ function Get-ProcessOwner {
return $null
}

function Get-ProcessImagePath {
# The process's own main-module path, or $null when it cannot be read. A protected process, or a
# 32-bit steam.exe read from 64-bit PowerShell, makes MainModule throw; then ask WMI, which does
# not need a handle on the process. Never throws.
param([Parameter(Mandatory = $true)]$Process)
try {
$f = $Process.MainModule.FileName
if ($f) { return $f }
} catch { }
try {
$cim = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId = $($Process.Id)" -ErrorAction Stop
if ($cim -and $cim.ExecutablePath) { return $cim.ExecutablePath }
} catch { }
return $null
}

function Get-ProcessesUsingFolder {
<#
.SYNOPSIS
Running processes whose main image or any loaded module lives under Folder.
.DESCRIPTION
Protected processes and processes that exit while we look throw when their modules are
read. Those are skipped, never fatal. Returns objects with Name, Id, SessionId, Owner,
Path (the file that put it on the list) and Role 'holder'.
Path (the file that put it on the list), ImagePath (the process's own exe, or $null when
unreadable) and Role 'holder'. Path may be a DLL; ImagePath is never a module.
#>
param([Parameter(Mandatory = $true)][string]$Folder)
$prefix = [System.IO.Path]::GetFullPath($Folder).TrimEnd('\') + '\'
Expand All @@ -601,7 +618,8 @@ function Get-ProcessesUsingFolder {
if ($hit) {
$found += [pscustomobject]@{
Name = $p.ProcessName; Id = $p.Id; SessionId = $p.SessionId
Owner = (Get-ProcessOwner -ProcessId $p.Id); Path = $hit; Role = 'holder' }
Owner = (Get-ProcessOwner -ProcessId $p.Id); Path = $hit
ImagePath = (Get-ProcessImagePath -Process $p); Role = 'holder' }
}
}
return $found
Expand Down Expand Up @@ -636,11 +654,11 @@ function Test-SteamIsolationBlocksUpgrade {
$known = @($procs | ForEach-Object { $_.Id })
foreach ($p in @(Get-Process -Name 'steam', 'steamwebhelper' -ErrorAction SilentlyContinue)) {
if (($sessions -contains $p.SessionId) -and -not ($known -contains $p.Id)) {
$path = $null
try { $path = $p.MainModule.FileName } catch { }
$path = Get-ProcessImagePath -Process $p
$procs += [pscustomobject]@{
Name = $p.ProcessName; Id = $p.Id; SessionId = $p.SessionId
Owner = (Get-ProcessOwner -ProcessId $p.Id); Path = $path; Role = 'steam' }
Owner = (Get-ProcessOwner -ProcessId $p.Id); Path = $path
ImagePath = $path; Role = 'steam' }
}
}
}
Expand All @@ -659,7 +677,10 @@ function Format-SteamInUseMessage {
$launcher = Join-Path $Check.Folder 'MultiSeat.SteamLauncher.exe'
$steamExe = $null
foreach ($p in @($Check.Processes)) {
if ($p.Name -eq 'steam' -and $p.Path) { $steamExe = $p.Path; break }
# ImagePath, not Path: for a holder, Path is the module that matched (the hook DLL).
if ($p.Name -eq 'steam' -and $p.ImagePath -and $p.ImagePath.EndsWith('steam.exe', [System.StringComparison]::OrdinalIgnoreCase)) {
$steamExe = $p.ImagePath; break
}
}
if (-not $steamExe) { $steamExe = '<path to steam.exe>' }

Expand Down
74 changes: 74 additions & 0 deletions src/MultiSeat.Tests/Scripts/InstallLibTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,80 @@ public void Preflight_names_a_running_process_whose_image_lives_in_steam_isolati
Assert.Empty(after.GetProperty("Processes").EnumerateArray());
}

// -- The close-Steam command must name steam.exe, never the hook DLL (#126) ---------------

private static string MessageFor(string engine, string processesLiteral, string folder)
{
var j = J(Ps(engine,
"$check = [pscustomobject]@{ Blocked = $true; Folder = " + Q(folder) + "; LockedFiles = @(); Processes = @(" + processesLiteral + ") }\n" +
"$r = [pscustomobject]@{ Message = (Format-SteamInUseMessage -Check $check) }\n"));
return j.GetProperty("Message").GetString()!;
}

[Theory, MemberData(nameof(Engines))]
public void The_close_steam_command_names_steam_exe_when_the_holder_path_is_the_hook_dll(string engine)
{
const string steamExe = @"C:\Program Files (x86)\Steam\steam.exe";
const string dir = @"C:\Program Files\MultiSeat\SteamIsolation";
var msg = MessageFor(engine,
"[pscustomobject]@{ Name = 'steam'; Id = 10; SessionId = 2; Owner = 'PC\\gamer'; Role = 'holder'; " +
"Path = " + Q(dir + @"\MultiSeat.SteamHook.dll") + "; ImagePath = " + Q(steamExe) + " }", dir);
Assert.Contains("--steam \"" + steamExe + "\" --forward -- -shutdown", msg);
Assert.DoesNotContain("--steam \"" + dir + @"\MultiSeat.SteamHook.dll", msg);
}

[Theory, MemberData(nameof(Engines))]
public void The_close_steam_command_keeps_the_placeholder_when_no_steam_image_is_known(string engine)
{
const string dir = @"C:\Program Files\MultiSeat\SteamIsolation";
var msg = MessageFor(engine,
"[pscustomobject]@{ Name = 'steam'; Id = 10; SessionId = 2; Owner = $null; Role = 'holder'; " +
"Path = " + Q(dir + @"\MultiSeat.SteamHook.dll") + "; ImagePath = $null }", dir);
Assert.Contains("--steam \"<path to steam.exe>\"", msg);
Assert.DoesNotContain("SteamHook.dll\" --forward", msg);
}

[Theory, MemberData(nameof(Engines))]
public void The_close_steam_command_skips_a_steam_entry_whose_image_is_not_steam_exe(string engine)
{
// A first entry with only a DLL image must not win over a later entry with the real exe.
const string steamExe = @"D:\Games\Steam\steam.exe";
const string dir = @"C:\Program Files\MultiSeat\SteamIsolation";
var msg = MessageFor(engine,
"[pscustomobject]@{ Name = 'steam'; Id = 10; SessionId = 2; Owner = $null; Role = 'holder'; Path = 'x.dll'; ImagePath = " + Q(dir + @"\MultiSeat.SteamHook.dll") + " }, " +
"[pscustomobject]@{ Name = 'steam'; Id = 11; SessionId = 2; Owner = $null; Role = 'steam'; Path = " + Q(steamExe) + "; ImagePath = " + Q(steamExe) + " }", dir);
Assert.Contains("--steam \"" + steamExe + "\"", msg);
}

[Theory, MemberData(nameof(Engines))]
public void Preflight_fills_ImagePath_with_the_real_exe_and_the_message_uses_it(string engine)
{
// A real process whose image is SteamIsolation\steam.exe. Path (the matching file) and
// ImagePath must both be that exe here, and the printed command must carry it.
using var t = new Temp();
PutSteamIsolation(t.Root, "old");
var fakeSteam = Path.Combine(t.Root, "SteamIsolation", "steam.exe");
File.Copy(Path.Combine(Environment.SystemDirectory, "ping.exe"), fakeSteam, overwrite: true);
using var proc = Process.Start(new ProcessStartInfo(fakeSteam, "-n 120 127.0.0.1")
{
UseShellExecute = false, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardError = true,
})!;
try
{
Thread.Sleep(500);
var j = J(Ps(engine, CheckCall(t.Root) +
"$r | Add-Member -NotePropertyName Message -NotePropertyValue (Format-SteamInUseMessage -Check $r)\n"));
var mine = j.GetProperty("Processes").EnumerateArray().Single(x => x.GetProperty("Id").GetInt32() == proc.Id);
Assert.Equal(fakeSteam, mine.GetProperty("ImagePath").GetString(), ignoreCase: true);
Assert.Contains("--steam \"" + fakeSteam + "\"", j.GetProperty("Message").GetString(), StringComparison.OrdinalIgnoreCase);
}
finally
{
try { proc.Kill(); proc.WaitForExit(10_000); } catch (InvalidOperationException) { }
}
}

private static string GateCall(string install, string marker) =>
"$r = Invoke-StopWhenNotBlocked -InstallDir " + Q(install) +
" -Stop { Set-Content -LiteralPath " + Q(marker) + " -Value stopped }\n";
Expand Down
Loading