From b2652055c5ad8e03661343c81f2f1ddafadbd1b5 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 24 Oct 2025 17:37:56 +1100 Subject: [PATCH 1/6] Add protection when updating runtime files Adds a safeguard when updating runtime files that all other instances of PoB are closed so that the updater doesn't run into issues For linux systems it instead shows a popup as we can't reliably detect the instance count --- src/Launch.lua | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/Launch.lua b/src/Launch.lua index 505a42b9e5..f87077f183 100644 --- a/src/Launch.lua +++ b/src/Launch.lua @@ -8,6 +8,37 @@ local startTime = GetTime() APP_NAME = "Path of Building (PoE2)" +local pathSeparator = package.config:sub(1,1) +local isWindows = pathSeparator == "\\" +local updateProcessCandidates = { + "Path{space}of{space}Building-PoE2.exe", + "PathOfBuilding-PoE2.exe", +} + +-- Returns the total number of running processes that match any known PoB executable names. +local function getMatchingProcessCount() + if not isWindows then + -- Non-Windows platforms rely on manual confirmation instead of auto-detection. + return 1 + end + + local total = 0 + for _, name in ipairs(updateProcessCandidates) do + local handle = io.popen(string.format('tasklist /FO CSV /FI "IMAGENAME eq %s" /NH', name)) + if not handle then + return nil, "tasklist unavailable" + end + for line in handle:lines() do + if line ~= "" and not line:match("^INFO:") then + total = total + 1 + end + end + handle:close() + end + + return total +end + SetWindowTitle(APP_NAME) ConExecute("set vid_mode 8") ConExecute("set vid_resizable 3") @@ -314,6 +345,17 @@ end function launch:ApplyUpdate(mode) if mode == "basic" then + local touchesRuntime, err = self:PendingRuntimeUpdateTouchesRuntime() + if touchesRuntime == nil then + ConPrintf("Warning: Unable to inspect runtime update operations: %s", err or "unknown error") + touchesRuntime = true + end + if touchesRuntime and not self:EnsureUpdateExclusiveAccess() then + return + end + if not isWindows then + self.runtimeUpdateManualConfirmPromptShown = nil + end -- Need to revert to the basic environment to fully apply the update LoadModule("UpdateApply", "Update/opFile.txt") SpawnProcess(GetRuntimePath()..'/Update', 'UpdateApply.lua Update/opFileRuntime.txt') @@ -326,6 +368,61 @@ function launch:ApplyUpdate(mode) end end +function launch:GetAdditionalInstanceCount() + -- Subtract one for the current process; returns 0 if no other instance matches. + local count, err = getMatchingProcessCount() + if not count then + return nil, err + end + if count <= 1 then + return 0 + end + return count - 1 +end + +function launch:PendingRuntimeUpdateTouchesRuntime() + local file, err = io.open("Update/opFileRuntime.txt", "r") + if not file then + return nil, err or "missing operations file" + end + for line in file:lines() do + if line:match("^move%s") then + file:close() + return true + end + end + file:close() + return false +end + +function launch:EnsureUpdateExclusiveAccess() + if not isWindows then + -- On Unix-like platforms the updater cannot reliably detect sibling processes, so require manual confirmation. + if not self.runtimeUpdateManualConfirmPromptShown then + self.runtimeUpdateManualConfirmPromptShown = true + local prompt = "^3Linux/Unix confirmation required:^0\n\nClose every other Path of Building window before updating core runtime files.\nOnce all other instances are closed, press Update again to continue.\n\nPress Enter or Escape to dismiss this message." + self:ShowPrompt(1, 0.5, 0, prompt) + return false + end + return true + end + -- Block runtime updates until all other PoB processes exit to avoid DLL write failures. + local otherCount, err = self:GetAdditionalInstanceCount() + if otherCount == nil then + ConPrintf("Warning: Update could not verify other running instances: %s", err) + return true + end + if otherCount > 0 then + local plural = otherCount > 1 + local instanceLabel = plural and "instances" or "instance" + local verb = plural and "are" or "is" + local prompt = string.format("^1Update paused:\n\n^0%d other Path of Building %s %s still running.\nClose every other copy before applying this update, then press Update again.\n\nPress Enter or Escape to dismiss this message.", otherCount, instanceLabel, verb) + self:ShowPrompt(1, 0.5, 0, prompt) + return false + end + return true +end + function launch:CheckForUpdate(inBackground) if self.updateCheckRunning then return From 399b5c0e17cfc864c8e5d8b79c90dedb0d14e619 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 24 Oct 2025 17:54:12 +1100 Subject: [PATCH 2/6] Simplify runtime check mode == "basic" already means we are touching runtime files so no need for extra work --- src/Launch.lua | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/Launch.lua b/src/Launch.lua index f87077f183..d4c8351525 100644 --- a/src/Launch.lua +++ b/src/Launch.lua @@ -345,12 +345,7 @@ end function launch:ApplyUpdate(mode) if mode == "basic" then - local touchesRuntime, err = self:PendingRuntimeUpdateTouchesRuntime() - if touchesRuntime == nil then - ConPrintf("Warning: Unable to inspect runtime update operations: %s", err or "unknown error") - touchesRuntime = true - end - if touchesRuntime and not self:EnsureUpdateExclusiveAccess() then + if not self:EnsureUpdateExclusiveAccess() then return end if not isWindows then @@ -380,21 +375,6 @@ function launch:GetAdditionalInstanceCount() return count - 1 end -function launch:PendingRuntimeUpdateTouchesRuntime() - local file, err = io.open("Update/opFileRuntime.txt", "r") - if not file then - return nil, err or "missing operations file" - end - for line in file:lines() do - if line:match("^move%s") then - file:close() - return true - end - end - file:close() - return false -end - function launch:EnsureUpdateExclusiveAccess() if not isWindows then -- On Unix-like platforms the updater cannot reliably detect sibling processes, so require manual confirmation. From e1a7c129743d78c986d3cb2c098c498a4defd81e Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 24 Oct 2025 18:12:37 +1100 Subject: [PATCH 3/6] Fix check --- src/Launch.lua | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Launch.lua b/src/Launch.lua index d4c8351525..efb33fb0ae 100644 --- a/src/Launch.lua +++ b/src/Launch.lua @@ -12,7 +12,9 @@ local pathSeparator = package.config:sub(1,1) local isWindows = pathSeparator == "\\" local updateProcessCandidates = { "Path{space}of{space}Building-PoE2.exe", + "Path{space}of{space}Building.exe", "PathOfBuilding-PoE2.exe", + "Path of Building (PoE2).exe", } -- Returns the total number of running processes that match any known PoB executable names. @@ -22,19 +24,27 @@ local function getMatchingProcessCount() return 1 end - local total = 0 + local candidateSet = { } for _, name in ipairs(updateProcessCandidates) do - local handle = io.popen(string.format('tasklist /FO CSV /FI "IMAGENAME eq %s" /NH', name)) - if not handle then - return nil, "tasklist unavailable" - end - for line in handle:lines() do - if line ~= "" and not line:match("^INFO:") then + candidateSet[name:lower()] = true + local withSpaces = name:gsub("{space}", " ") + candidateSet[withSpaces:lower()] = true + end + + local total = 0 + local handle = io.popen('tasklist /FO CSV /NH') + if not handle then + return nil, "tasklist unavailable" + end + for line in handle:lines() do + if line ~= "" and not line:match("^INFO:") then + local image = line:match('^"([^"]+)"') + if image and candidateSet[image:lower()] then total = total + 1 end end - handle:close() end + handle:close() return total end From 1a0652ac69dc93aabc92d57a57e9417faca3b6c7 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 24 Oct 2025 19:36:52 +1100 Subject: [PATCH 4/6] Remove PoB 1 check --- src/Launch.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Launch.lua b/src/Launch.lua index efb33fb0ae..49842e41df 100644 --- a/src/Launch.lua +++ b/src/Launch.lua @@ -12,7 +12,6 @@ local pathSeparator = package.config:sub(1,1) local isWindows = pathSeparator == "\\" local updateProcessCandidates = { "Path{space}of{space}Building-PoE2.exe", - "Path{space}of{space}Building.exe", "PathOfBuilding-PoE2.exe", "Path of Building (PoE2).exe", } From 78a47ee694073abc7037af7615f1cb6bda41eca7 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Tue, 28 Oct 2025 05:54:46 +1100 Subject: [PATCH 5/6] Use SimpleGraphic to get process count now --- src/HeadlessWrapper.lua | 3 +++ src/Launch.lua | 37 +++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/HeadlessWrapper.lua b/src/HeadlessWrapper.lua index 6f7ae1e28a..9b339266ab 100644 --- a/src/HeadlessWrapper.lua +++ b/src/HeadlessWrapper.lua @@ -152,6 +152,9 @@ function ConPrintTable(tbl, noRecurse) end function ConExecute(cmd) end function ConClear() end function SpawnProcess(cmdName, args) end +function GetProcessCount(names) + return 1 +end function OpenURL(url) end function SetProfiling(isEnabled) end function Restart() end diff --git a/src/Launch.lua b/src/Launch.lua index 49842e41df..d6820832e6 100644 --- a/src/Launch.lua +++ b/src/Launch.lua @@ -23,29 +23,30 @@ local function getMatchingProcessCount() return 1 end - local candidateSet = { } - for _, name in ipairs(updateProcessCandidates) do - candidateSet[name:lower()] = true - local withSpaces = name:gsub("{space}", " ") - candidateSet[withSpaces:lower()] = true + if not GetProcessCount then + return nil, "GetProcessCount helper unavailable" end - local total = 0 - local handle = io.popen('tasklist /FO CSV /NH') - if not handle then - return nil, "tasklist unavailable" - end - for line in handle:lines() do - if line ~= "" and not line:match("^INFO:") then - local image = line:match('^"([^"]+)"') - if image and candidateSet[image:lower()] then - total = total + 1 - end + local names = { } + local seen = { } + for _, name in ipairs(updateProcessCandidates) do + local actual = name:gsub("{space}", " ") + local key = actual:lower() + if not seen[key] then + seen[key] = true + table.insert(names, actual) end end - handle:close() - return total + local ok, count = pcall(GetProcessCount, names) + if not ok then + return nil, tostring(count) + end + if type(count) ~= "number" or count < 0 then + return nil, "GetProcessCount returned invalid value" + end + + return count end SetWindowTitle(APP_NAME) From 9ee7ae3a5276bcd3e3b2e53d55988f28f89c0b06 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Tue, 28 Oct 2025 07:10:42 +1100 Subject: [PATCH 6/6] Remove unix stuff --- src/HeadlessWrapper.lua | 3 --- src/Launch.lua | 16 +--------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/HeadlessWrapper.lua b/src/HeadlessWrapper.lua index 9b339266ab..6f7ae1e28a 100644 --- a/src/HeadlessWrapper.lua +++ b/src/HeadlessWrapper.lua @@ -152,9 +152,6 @@ function ConPrintTable(tbl, noRecurse) end function ConExecute(cmd) end function ConClear() end function SpawnProcess(cmdName, args) end -function GetProcessCount(names) - return 1 -end function OpenURL(url) end function SetProfiling(isEnabled) end function Restart() end diff --git a/src/Launch.lua b/src/Launch.lua index d6820832e6..046cf81e46 100644 --- a/src/Launch.lua +++ b/src/Launch.lua @@ -19,8 +19,7 @@ local updateProcessCandidates = { -- Returns the total number of running processes that match any known PoB executable names. local function getMatchingProcessCount() if not isWindows then - -- Non-Windows platforms rely on manual confirmation instead of auto-detection. - return 1 + return nil, "Process detection only supported on Windows" end if not GetProcessCount then @@ -358,9 +357,6 @@ function launch:ApplyUpdate(mode) if not self:EnsureUpdateExclusiveAccess() then return end - if not isWindows then - self.runtimeUpdateManualConfirmPromptShown = nil - end -- Need to revert to the basic environment to fully apply the update LoadModule("UpdateApply", "Update/opFile.txt") SpawnProcess(GetRuntimePath()..'/Update', 'UpdateApply.lua Update/opFileRuntime.txt') @@ -386,16 +382,6 @@ function launch:GetAdditionalInstanceCount() end function launch:EnsureUpdateExclusiveAccess() - if not isWindows then - -- On Unix-like platforms the updater cannot reliably detect sibling processes, so require manual confirmation. - if not self.runtimeUpdateManualConfirmPromptShown then - self.runtimeUpdateManualConfirmPromptShown = true - local prompt = "^3Linux/Unix confirmation required:^0\n\nClose every other Path of Building window before updating core runtime files.\nOnce all other instances are closed, press Update again to continue.\n\nPress Enter or Escape to dismiss this message." - self:ShowPrompt(1, 0.5, 0, prompt) - return false - end - return true - end -- Block runtime updates until all other PoB processes exit to avoid DLL write failures. local otherCount, err = self:GetAdditionalInstanceCount() if otherCount == nil then