Skip to content

fix: client updater - #1632

Open
LaercioLeal wants to merge 7 commits into
opentibiabr:mainfrom
LaercioLeal:fix-autoupdater
Open

fix: client updater#1632
LaercioLeal wants to merge 7 commits into
opentibiabr:mainfrom
LaercioLeal:fix-autoupdater

Conversation

@LaercioLeal

@LaercioLeal LaercioLeal commented Feb 4, 2026

Copy link
Copy Markdown

Description

Fix for Client Auto Updater. The client updater was not working properly, which blocked downloads.

Behavior

Actual

The client couldn't compare files or download the updates from the server.

Expected

The Client should download the files and update successfully.

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested

The files were changed using Claude Opus 4.5 and may contains some errors. I have no experience with PHP neither this kind of file transfer, so I used AI to generate the fix.

The changes were tested on a nginx server and everything seems OK.
Android Version need better tests.

Test Configuration:

  • Server Version: 860
  • Client: 4.0
  • Operating System:
    Client - Windows 11
    Server - Ubuntu 24.04

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I checked the PR checks reports
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works

Summary by CodeRabbit

  • New Features

    • Interactive option to change the updater server URL with confirm/cancel, cancellation of ongoing operations, and automatic restart.
  • Bug Fixes

    • Stronger validation of update metadata to prevent invalid binary selection.
    • More accurate binary matching when selecting updates.
    • Safer server-side update delivery with robust caching, locking, retrying, and hardened response headers.
  • Stability

    • UI robustness improvements to avoid nil-related errors.
    • Improved mobile/custom-server update flow for reliable behavior.

@coderabbitai

coderabbitai Bot commented Feb 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a public Updater.changeUrl() UI and restart flow, strengthens client-side validation and gating, guards a missing UI element, and hardens the server updater API with stricter input checks, file locking, atomic cache writes, checksum population, and explicit response headers.

Changes

Cohort / File(s) Summary
Client updater module
modules/updater/updater.lua
Added public Updater.changeUrl() to show a URL-change dialog, cancel/stop ongoing operations, tear down/recreate windows, and restart checks with the new URL. Added defensive validation for data.binary (file and checksum strings), nil-guard for updaterWindow.changeUrlButton, and replaced legacy gating with allowCustomServers and loadModulesFunction.
Client UI manifest
modules/updater/updater.otui
Changed Change URL button handler to only invoke Updater.changeUrl() when Updater.enableChangeUrl is truthy.
Server API & cache
tools/api/updater.php
Tightened input validation (require object), used null-coalescing defaults, added directory existence checks, file locking with wait/retry, atomic cache writes via temp file + rename, checksum computation for cache population, binary selection by basename, explicit HTTP response headers (Content-Type, Content-Length, X-Content-Type-Options, Cache-Control), and improved lock lifecycle/error handling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client as Updater (Lua)
    participant Server as API (PHP)
    participant FS as Cache/Filesystem

    User->>Client: Open change-URL dialog (Updater.changeUrl)
    Client->>User: Show dialog (OK / Restart)
    User->>Client: Confirm new URL
    Client->>Client: Cancel/stop current ops, destroy updater windows
    Client->>Server: Request update metadata using new URL
    Server->>FS: Verify cache directory exists
    Server->>FS: Acquire lock (wait/retry if locked)
    alt Cache missing or stale
        Server->>FS: Compute checksums, write temp cache file
        Server->>FS: Atomic rename temp -> cache
    end
    Server->>Server: Select binary entry by basename
    Server->>Client: Return payload with explicit headers
    Server->>FS: Release lock
    Client->>Client: Restart updater flow with new URL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I nibble locks and stitch the trail,
I guard the button, patch the fail,
A URL hops in, bright and new,
I tear down, restart, and chew-then-do! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'fix: client updater' is vague and generic. While it relates to the changeset, it does not clearly convey the specific nature of the fix or what aspect of the updater was corrected. Consider a more specific title that indicates the main fix, such as 'fix: enhance updater validation and server sync for binary downloads' or 'fix: improve updater robustness and binary selection logic'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@modules/updater/updater.lua`:
- Around line 296-303: The Cancel button currently calls Updater.check(), which
restarts the entire update and discards progress; change this to resume behavior
by calling an existing resume/continue method (e.g., Updater.resume() or
Updater.continue()) instead of Updater.check(), or if no resume API exists
implement one on the Updater that picks up from saved state and continues the
download/apply process; alternatively, if restart is the intended behavior,
update the button text and comment to "Restart" and keep calling
Updater.check(); locate the button handler (cancelButton.onClick), the dialog
teardown (dialog:destroy), and the Updater methods to apply the appropriate
change.
- Around line 245-247: The comment above the changeUrl function flags it as
AI-generated and untested for mobile — remove the AI-disclosure comment and
instead add a clear TODO and gating: add a TODO/TICKET reference above the
function (changeUrl) stating “mobile testing required” and either wrap the
function activation behind a feature flag/guard (check a config flag like
enableChangeUrl or runtime detection) or disable wiring from the UI until
verified; also update the UI wiring in updater.otui (changeUrlButton) to only
call changeUrl when the flag is enabled and create a short manual test checklist
or ticket to verify behavior on mobile clients.

In `@tools/api/updater.php`:
- Around line 48-86: The cache-building path currently proceeds without holding
the lock when non-blocking flock() fails; change the logic so that when
acquiring the lock via $lock (created from $lock_file) fails with LOCK_EX |
LOCK_NB you wait/retry and then acquire a blocking flock() before proceeding to
build the cache; ensure $lock remains open for the entire cache creation/atomic
write (file_put_contents(... ".tmp") + rename(...)) and only call flock($lock,
LOCK_UN) and fclose($lock) after the rename, and guard any flock/ fclose calls
with an isset(is_resource($lock)) check to avoid unlocking/closing an
already-closed resource.
🧹 Nitpick comments (3)
tools/api/updater.php (1)

63-76: Inconsistent indentation within the foreach block.

Lines 64-66 have different indentation than the surrounding new code (lines 70-76). Consider aligning for consistency.

modules/updater/updater.lua (2)

209-215: Consider simplifying the triple-negation condition for readability.

The logic is correct but (not g_platform.isMobile() or not allowCustomServers or not loadModulesFunction) requires mental gymnastics. Consider extracting to a descriptive variable.

♻️ Suggested refactor for clarity
   local allowCustomServers = ALLOW_CUSTOM_SERVERS or false
+  local canWaitForCustomUrl = g_platform.isMobile() and allowCustomServers and loadModulesFunction
   local function progressUpdater(value)
     removeEvent(scheduledEvent)
     if value == 100 then
       return Updater.error(tr("Timeout"))
     end
-    if updateData and (value > 60 or (not g_platform.isMobile() or not allowCustomServers or not loadModulesFunction)) then -- gives 3s to set custom updater for mobile version
+    if updateData and (value > 60 or not canWaitForCustomUrl) then -- gives 6s to set custom updater for mobile version
       return updateFiles(updateData)
     end

282-294: Consider basic URL format validation.

The handler only checks length > 4, allowing invalid URLs like "aaaa" that will fail on the subsequent Updater.check(). A simple protocol prefix check would provide earlier feedback to users.

♻️ Suggested validation improvement
   okButton.onClick = function()
     local newUrl = textEdit:getText()
-    if newUrl and newUrl:len() > 4 then
+    if newUrl and (newUrl:find("^https?://") ~= nil) then
       Services.updater = newUrl
       dialog:destroy()

Comment thread modules/updater/updater.lua Outdated
Comment thread modules/updater/updater.lua Outdated
Comment thread tools/api/updater.php
Comment on lines +48 to 86
// File locking to prevent race conditions
$lock_file = $cache_file . ".lock";
$lock = fopen($lock_file, "w");
if (!flock($lock, LOCK_EX | LOCK_NB)) {
// Another process is updating cache, wait and use existing cache
fclose($lock);
usleep(100000); // 100ms
if (file_exists($cache_file)) {
$cache = json_decode(file_get_contents($cache_file), true);
}
}

if (!$cache) {
$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
$cache = array();
foreach ($rii as $file) {
if (!$file->isFile())
continue;
$path = str_replace($dir, '', $file->getPathname());
$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
$checksum = hash_file("crc32b", $file->getPathname());

if ($checksum === true || $checksum != "") {
$parsed_checksum = ltrim($checksum, '0');
if ($parsed_checksum === '') {
$parsed_checksum = '0';
if ($checksum !== false && $checksum !== "") {
$parsed_checksum = ltrim($checksum, '0');
if ($parsed_checksum === '') {
$parsed_checksum = '0';
}
$cache[$path] = $parsed_checksum;
}
$cache[$path] = $parsed_checksum;
}
file_put_contents($cache_file . ".tmp", json_encode($cache));
rename($cache_file . ".tmp", $cache_file);

// Release lock
flock($lock, LOCK_UN);
}
if (isset($lock) && is_resource($lock)) {
fclose($lock);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition: cache can be built without lock protection.

When flock() fails to acquire the lock (another process is updating), the code closes $lock on line 53, waits 100ms, then tries to read the cache. If the cache still doesn't exist at line 60, it proceeds to build the cache without holding any lock, defeating the locking mechanism.

Additionally, if this path is taken and cache is built, line 82 calls flock($lock, LOCK_UN) on an already-closed resource.

🐛 Proposed fix to maintain lock throughout cache building
     // File locking to prevent race conditions
     $lock_file = $cache_file . ".lock";
     $lock = fopen($lock_file, "w");
     if (!flock($lock, LOCK_EX | LOCK_NB)) {
         // Another process is updating cache, wait and use existing cache
-        fclose($lock);
         usleep(100000); // 100ms
         if (file_exists($cache_file)) {
             $cache = json_decode(file_get_contents($cache_file), true);
         }
+        if (!$cache) {
+            // Wait for the lock if cache still not available
+            flock($lock, LOCK_EX);
+            // Re-check cache after acquiring lock
+            if (file_exists($cache_file)) {
+                $cache = json_decode(file_get_contents($cache_file), true);
+            }
+        }
     }
     
     if (!$cache) {
         $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
         $cache = array(); 
         foreach ($rii as $file) {
         // ... rest of cache building ...
         }
         file_put_contents($cache_file . ".tmp", json_encode($cache));
         rename($cache_file . ".tmp", $cache_file);
-        
-        // Release lock
-        flock($lock, LOCK_UN);
     }
-    if (isset($lock) && is_resource($lock)) {
-        fclose($lock);
-    }
+    // Release lock and close handle
+    flock($lock, LOCK_UN);
+    fclose($lock);
 }
🤖 Prompt for AI Agents
In `@tools/api/updater.php` around lines 48 - 86, The cache-building path
currently proceeds without holding the lock when non-blocking flock() fails;
change the logic so that when acquiring the lock via $lock (created from
$lock_file) fails with LOCK_EX | LOCK_NB you wait/retry and then acquire a
blocking flock() before proceeding to build the cache; ensure $lock remains open
for the entire cache creation/atomic write (file_put_contents(... ".tmp") +
rename(...)) and only call flock($lock, LOCK_UN) and fclose($lock) after the
rename, and guard any flock/ fclose calls with an isset(is_resource($lock))
check to avoid unlocking/closing an already-closed resource.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/updater/updater.lua (1)

109-115: ⚠️ Potential issue | 🟡 Minor

Add checksum type validation to prevent download failures with malformed updater responses.

The condition on line 109-111 validates data.binary.file but not data.binary.checksum. If the updater API returns a binary entry with missing or empty checksum, line 113 will enqueue it with a nil/empty checksum. This causes the download validation at line 44 to fail with a misleading error message ("Should be nil/empty, is: [actual_checksum]"), triggering retries up to maxRetries before displaying an error dialog. The binary update should instead be silently skipped when the checksum is invalid. Add validation matching the data.binary.file check:

🛠️ Suggested fix
-  if type(data.binary) == "table" and type(data.binary.file) == "string" and data.binary.file:len() > 1 then
+  if type(data.binary) == "table"
+     and type(data.binary.file) == "string" and data.binary.file:len() > 1
+     and type(data.binary.checksum) == "string" and data.binary.checksum:len() > 0 then
🤖 Fix all issues with AI agents
In `@modules/updater/updater.lua`:
- Around line 245-309: In Updater.changeUrl(), don't cancel the current update
(removeEvent(scheduledEvent) and HTTP.cancel(httpOperationId)) before showing
the dialog; instead move those cancellations into the okButton.onClick and
restartButton.onClick handlers so they only occur when the user actually
confirms restart/change; additionally, when saving Services.updater (in
okButton.onClick and restartButton.onClick if applicable) normalize the newUrl
to ensure it ends with a trailing slash (append "/" if missing) so subsequent
concatenation like url .. file produces valid paths; reference:
Updater.changeUrl, scheduledEvent, httpOperationId, Services.updater,
okButton.onClick, restartButton.onClick.

Comment thread modules/updater/updater.lua Outdated
@InnerCircleTFS

Copy link
Copy Markdown
Contributor

Tell Opus claude that the problem is NOT in .php or .lua.

The problem is in cpp g_resources.filesChecksums()

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/updater/updater.lua (1)

109-115: ⚠️ Potential issue | 🟡 Minor

Validate data.binary.checksum before use.

If data.binary.checksum is missing or non-string, downloadFiles() will build an error message with .. file_checksum and throw. Guard the checksum to avoid a runtime error on malformed API data.

🛡️ Suggested guard
-  if type(data.binary) == "table" and type(data.binary.file) == "string" and data.binary.file:len() > 1 then
+  if type(data.binary) == "table"
+      and type(data.binary.file) == "string" and data.binary.file:len() > 1
+      and type(data.binary.checksum) == "string" and data.binary.checksum:len() > 0 then
🤖 Fix all issues with AI agents
In `@modules/updater/updater.lua`:
- Around line 299-309: The Restart button handler cancels the HTTP op but then
calls Updater.check() which returns early while updaterWindow is still set;
modify the restartButton.onClick to mirror the OK-path teardown by ensuring the
existing updaterWindow is destroyed/cleared before calling Updater.check():
removeEvent(scheduledEvent) and HTTP.cancel(httpOperationId) as already done,
then call dialog:destroy() and also clear the global/updaterWindow reference (or
call the same cleanup used by the OK button) so updaterWindow is nil, and only
then call Updater.check() so the restart actually proceeds.

Comment thread modules/updater/updater.lua Outdated
@majestyotbr majestyotbr changed the title Fix: client updater fix: client updater Feb 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open 45 days with no activity.

@github-actions github-actions Bot added Stale and removed Stale labels Mar 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open 45 days with no activity.

@github-actions github-actions Bot added the Stale label Apr 15, 2026
@github-actions github-actions Bot removed the Stale label May 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open 45 days with no activity.

@github-actions github-actions Bot added the Stale label Jun 17, 2026
@majestyotbr majestyotbr removed the Stale label Jul 31, 2026

@dudantas dudantas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for your PR and your time.

The reported updater freeze is still not addressed by this change.

Updater.updateFiles() calls g_resources.filesChecksums() without a file list. The C++ implementation recursively enumerates and reads every file under the virtual root ("/") on the UI thread before downloads start. When running from a source tree, this may include build artifacts, vcpkg_installed, and other files that are not part of the update manifest, making the client appear frozen and delaying the download phase.

The PHP/Lua changes may improve API robustness, but they do not change this checksum path. The client should compute checksums only for the files returned in data.files (or the configured updater roots), rather than scanning the whole resource tree. This is also tracked in #1041.

Please either include the C++/Lua checksum-scope fix in this PR, or narrow the PR description to an independently reproducible API issue that this patch actually resolves.

@LaercioLeal

LaercioLeal commented Aug 1, 2026

Copy link
Copy Markdown
Author

Hi, thanks for your PR and your time.

The reported updater freeze is still not addressed by this change.

Updater.updateFiles() calls g_resources.filesChecksums() without a file list. The C++ implementation recursively enumerates and reads every file under the virtual root ("/") on the UI thread before downloads start. When running from a source tree, this may include build artifacts, vcpkg_installed, and other files that are not part of the update manifest, making the client appear frozen and delaying the download phase.

The PHP/Lua changes may improve API robustness, but they do not change this checksum path. The client should compute checksums only for the files returned in data.files (or the configured updater roots), rather than scanning the whole resource tree. This is also tracked in #1041.

Please either include the C++/Lua checksum-scope fix in this PR, or narrow the PR description to an independently reproducible API issue that this patch actually resolves.

Hi, thanks for your reply.
I didn't know about this issue, I did this only thinking about build version, so I haven't this error...
I'll try to solve this but if I can't I'll change the description.

@LaercioLeal
LaercioLeal requested a review from dudantas August 1, 2026 14:57
@LaercioLeal

Copy link
Copy Markdown
Author

@dudantas
Please check if the changes solve the issue.
I don't have prior experience with checksums and used AI to assist with the fix, but I believe it has been resolved.
I ran local tests with the updater and encountered no issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants