Skip to content

Commit 334031f

Browse files
committed
fix: stop installation when MSI enumeration fails
Keep the Windows integration fixture on the user volume so plugin staging can finish before installer validation. Fixes version-fox/vfox#685
1 parent 879529c commit 334031f

3 files changed

Lines changed: 64 additions & 17 deletions

File tree

‎.github/workflows/hooks-test.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ jobs:
5353
$ErrorActionPreference = 'Stop'
5454
$PSNativeCommandUseErrorActionPreference = $true
5555
# Literal percent/exclamation marks also catch cmd expansion bugs.
56-
$env:VFOX_HOME = Join-Path $env:RUNNER_TEMP 'Mechael Jackson''s & %USERNAME% ! SDKs'
56+
# Keep plugin staging and installation on the user's volume.
57+
$env:VFOX_HOME = Join-Path $env:USERPROFILE 'Mechael Jackson''s & %USERNAME% ! SDKs'
5758
$env:VFOX_PYTHON_USE_UV_BUILD = '0'
5859
$archive = Join-Path $env:RUNNER_TEMP 'python-plugin.zip'
5960
Compress-Archive -Path metadata.lua,hooks,lib,bin -DestinationPath $archive

‎lib/util.lua‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,18 +155,34 @@ function windowsInstallExe(path, url, version, filename)
155155
if not listing then
156156
error('Failed to list installer packages: ' .. tostring(listErr))
157157
end
158-
local files = listing:lines()
159-
for file in files do
160-
if file:match("%.msi$") then
161-
local command = windowsCommand.msi(msiPath .. '\\' .. file, qInstallPath)
162-
local exitCode = os.execute(command)
163-
if exitCode ~= 0 then
164-
error("Install msi failed: " .. file)
158+
local files = {}
159+
local readOk, readErr = pcall(function()
160+
for file in listing:lines() do
161+
if file:match("%.msi$") then
162+
files[#files + 1] = file
165163
end
166-
os.remove(qInstallPath .. '\\' .. file)
167164
end
165+
end)
166+
-- GopherLua returns the process's numeric exit status from pipe close.
167+
-- Close even after a read error, and reject partial results before installing.
168+
local closeOk, listExitCode = pcall(function() return listing:close() end)
169+
if not readOk then
170+
error('Failed to list installer packages: ' .. tostring(readErr))
171+
end
172+
if not closeOk or listExitCode ~= 0 then
173+
error('Failed to list installer packages: ' .. tostring(listExitCode))
174+
end
175+
if #files == 0 then
176+
error('No installer MSI packages found in: ' .. msiPath)
177+
end
178+
for _, file in ipairs(files) do
179+
local command = windowsCommand.msi(msiPath .. '\\' .. file, qInstallPath)
180+
local exitCode = os.execute(command)
181+
if exitCode ~= 0 then
182+
error("Install msi failed: " .. file)
183+
end
184+
os.remove(qInstallPath .. '\\' .. file)
168185
end
169-
listing:close()
170186

171187
-- Install pip
172188
print("Installing pip...")

‎tests/windows_install_test.lua‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package.path = './lib/?.lua;' .. package.path
22
RUNTIME = {osType = 'windows', archType = 'amd64', pluginDirPath = "C:\\Plugin's & %USERNAME% ! store"}
33
OS_TYPE = 'windows'
4-
local state = {commands = {}}
4+
local state = {commands = {}, closeCount = 0}
5+
local function reset()
6+
for key in pairs(state) do state[key] = nil end
7+
state.commands = {}
8+
state.closeCount = 0
9+
end
510
package.preload.http = function() return {download_file = function() return nil end} end
611
package.preload.html = function() return {} end
712
package.preload.json = function() return {} end
@@ -14,13 +19,22 @@ os.execute = function(command)
1419
return #state.commands == state.failOn and 1 or 0
1520
end
1621
io.popen = function(command)
17-
os.execute(command)
22+
local exitCode = os.execute(command)
1823
return {
1924
lines = function()
20-
local files, i = {'core.msi', 'stdlib.msi'}, 0
21-
return function() i = i + 1; return files[i] end
25+
local files, i = state.files or {'core.msi', 'stdlib.msi'}, 0
26+
return function()
27+
i = i + 1
28+
if state.readFailure and i == 2 then error('read failed') end
29+
return files[i]
30+
end
31+
end,
32+
close = function()
33+
state.closeCount = state.closeCount + 1
34+
if state.closeFailure then error('close failed') end
35+
-- GopherLua process pipes return a numeric exit status, including 0.
36+
return state.listExitCode or exitCode
2237
end,
23-
close = function() return true end,
2438
}
2539
end
2640
io.open = function()
@@ -32,17 +46,33 @@ require('util')
3246
local path = "C:\\Users\\Mechael Jackson's & %USERNAME% !\\python-3.14.5"
3347
windowsInstallExe(path, 'https://example.invalid/python.exe', '3.14.5', 'python.exe')
3448
assert(#state.commands >= 7, 'installer, enumeration, MSI, pip and aliases must be exercised')
35-
state.commands = {}
49+
reset()
3650
windowsInstallMsi(path, 'https://example.invalid/python.msi', '3.4.4', 'python.msi')
3751
assert(#state.commands == 2, 'MSI and ensurepip must both run')
3852
for _, failure in ipairs({{1, 'Extract failed'}, {3, 'Install msi failed'}, {6, 'executable alias'}}) do
39-
state.commands = {}
53+
reset()
4054
state.failOn = failure[1]
4155
local ok, err = pcall(function()
4256
windowsInstallExe(path, 'https://example.invalid/python.exe', '3.14.5', 'python.exe')
4357
end)
4458
assert(not ok and tostring(err):find(failure[2], 1, true), tostring(err))
4559
end
60+
for _, failure in ipairs({
61+
{name = 'partial listing exits nonzero', files = {'core.msi'}, exitCode = 1, message = 'Failed to list installer packages'},
62+
{name = 'listing read failure', readFailure = true, message = 'read failed'},
63+
{name = 'listing close failure', closeFailure = true, message = 'close failed'},
64+
{name = 'no MSI packages', files = {'readme.txt'}, message = 'No installer MSI packages'},
65+
}) do
66+
reset()
67+
state.files = failure.files
68+
state.listExitCode = failure.exitCode
69+
state.readFailure = failure.readFailure
70+
state.closeFailure = failure.closeFailure
71+
local ok, err = pcall(windowsInstallExe, path, 'https://example.invalid/python.exe', '3.14.5', 'python.exe')
72+
assert(not ok and tostring(err):find(failure.message, 1, true), failure.name .. ': ' .. tostring(err))
73+
assert(state.closeCount == 1, failure.name .. ': listing handle must be closed')
74+
assert(#state.commands == 2, failure.name .. ': no MSI or pip may run before enumeration succeeds')
75+
end
4676
local windows = require('windows_command')
4777
assert(not pcall(windows.msi, 'C:\\bad"path\\python.msi', path), 'invalid Windows quote must fail')
4878
assert(not pcall(windows.native, 'bad\nprogram', {}), 'control characters must fail')

0 commit comments

Comments
 (0)