From 62ff30aa64c297d736ce6eece9f578b40d412299 Mon Sep 17 00:00:00 2001
From: gesslar <1266935+gesslar@users.noreply.github.com>
Date: Tue, 1 Sep 2026 02:01:08 -0400
Subject: [PATCH] some fixes and things
---
BUILD.md | 2 +-
LICENSE.txt | 2 -
mfile | 2 +-
package-lock.json | 53 +++-
package.json | 17 +-
src/resources/test/dependency_queue_spec.lua | 243 +++++++++++++++++--
src/scripts/conditions.lua | 2 +-
src/scripts/dependency_queue.lua | 94 ++++++-
8 files changed, 363 insertions(+), 52 deletions(-)
diff --git a/BUILD.md b/BUILD.md
index 9a465fd..efa6ff2 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -66,7 +66,7 @@ python mini.py build/tmp/Glu-unified.lua build/Glu-min.lua
### `npm run build`
-Runs the full build process (muddle + single + min)
+Runs the full build process (muddy + single + min)
## Distribution Usage
diff --git a/LICENSE.txt b/LICENSE.txt
index 77bdec6..23c49c1 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,5 +1,3 @@
-Copyright (C) 2026 by Brian M. Workman bmw@gesslar.dev
-
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
diff --git a/mfile b/mfile
index fc872f0..ffd16f1 100644
--- a/mfile
+++ b/mfile
@@ -1,7 +1,7 @@
{
"package": "Glu",
"title": "Lua class library for Mudlet",
- "version": "2.1.0",
+ "version": "2.2.0",
"author": "gesslar",
"icon": "liquid-glue.png",
"dependencies": "",
diff --git a/package-lock.json b/package-lock.json
index 78ccdfc..46acb45 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
{
"name": "glu",
- "version": "2.1.0",
+ "version": "2.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "glu",
- "version": "2.1.0",
+ "version": "2.2.0",
"license": "0BSD",
"devDependencies": {
"@gesslar/bedoc": "^2.1.3",
- "@gesslar/muddy": "^2.2.3",
- "@gesslar/toolkit": "^5.6.0"
+ "@gesslar/muddy": "^3.3.0",
+ "@gesslar/toolkit": "^5.9.1"
}
},
"node_modules/@gesslar/actioneer": {
@@ -62,16 +62,17 @@
}
},
"node_modules/@gesslar/muddy": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/@gesslar/muddy/-/muddy-2.2.3.tgz",
- "integrity": "sha512-n3zZromLSJZeGxGF1MZygep10h0Qr5nz+8OIUeKjzLOTs484UAgPf7RNvPt9JBvTcbBswYfYnFTApbTBKzOcDA==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@gesslar/muddy/-/muddy-3.3.0.tgz",
+ "integrity": "sha512-BRW0bvpf3z5u6vczJhmQ+CjIn4FTWk+wZF+NEpGStd+ibhCEc9hIOql/h16Yvix6aoawHYXA9t7skE8/XmqIEg==",
"dev": true,
"license": "0BSD",
"dependencies": {
"@gesslar/actioneer": "^3.1.1",
"@gesslar/colours": "^1.0.0",
- "@gesslar/toolkit": "^5.6.0",
+ "@gesslar/toolkit": "^5.8.0",
"adm-zip": "^0.5.17",
+ "chokidar": "^5.0.0",
"commander": "^15.0.0",
"xmlbuilder2": "^4.0.3"
},
@@ -99,9 +100,9 @@
}
},
"node_modules/@gesslar/toolkit": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/@gesslar/toolkit/-/toolkit-5.6.0.tgz",
- "integrity": "sha512-GPcA7qRBNp+6yPmn1jP2y2o0jrVFU3qn6U5+sdejcQgLi3rv7qDTrUosGIKTV0YhYJsiV5zoAxB4G6GHt7H0tw==",
+ "version": "5.9.1",
+ "resolved": "https://registry.npmjs.org/@gesslar/toolkit/-/toolkit-5.9.1.tgz",
+ "integrity": "sha512-SjfcMGZxu1IDVJQznO87p8whs3T3O9/b1DxEwz1uq+HHZB+JNsKAoeNzW/TkJ7GWn9ioM0HUEya3ISB+FcB8Nw==",
"dev": true,
"hasInstallScript": true,
"license": "0BSD",
@@ -201,6 +202,22 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/commander": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
@@ -288,6 +305,20 @@
"node": ">=6"
}
},
+ "node_modules/readdirp": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
+ "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
diff --git a/package.json b/package.json
index 585cbe5..2811829 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,9 @@
{
"name": "glu",
- "version": "2.1.0",
+ "version": "2.2.0",
"description": "A modular utility library for Mudlet that just works. No fuss, no muss.",
"scripts": {
- "build": "(\nset -e pipefail\nnpx @gesslar/muddy\nnpm run build:single\nnpm run build:min\nnpm run build:docs\nnpm run build:lib\n)",
+ "build": "npx -y @gesslar/muddy",
"build:single": "python unify.py src/scripts build/Glu-single.lua",
"build:min": "python unify.py src/scripts build/tmp/Glu-unified.lua && python mini.py build/tmp/Glu-unified.lua build/Glu-min.lua",
"build:docs": "npx bedoc --config bedoc/bedoc.yaml --sub docs",
@@ -12,10 +12,11 @@
"test": "docker run --rm -v \"$PWD\":/workspace gesslardev/mudlet-busted",
"test:tree": "docker run --rm -e BUSTED_OUTPUT=treeOutput -v \"$PWD\":/workspace gesslardev/mudlet-busted",
"docker:update": "docker pull gesslardev/mudlet-busted",
- "major": "npx @gesslar/muddy version major",
- "minor": "npx @gesslar/muddy version minor",
- "patch": "npx @gesslar/muddy version patch",
- "update": "npx npm-check-updates -u && npm install"
+ "watch": "npx -y @gesslar/muddy -w",
+ "major": "npx -y @gesslar/muddy version major --package && npm i",
+ "minor": "npx -y @gesslar/muddy version minor --package && npm i",
+ "patch": "npx -y @gesslar/muddy version patch --package && npm i",
+ "update": "npx -y npm-check-updates -u && npm install"
},
"keywords": [
"mudlet",
@@ -28,8 +29,8 @@
"license": "0BSD",
"devDependencies": {
"@gesslar/bedoc": "^2.1.3",
- "@gesslar/muddy": "^2.2.3",
- "@gesslar/toolkit": "^5.6.0"
+ "@gesslar/muddy": "^3.3.0",
+ "@gesslar/toolkit": "^5.9.1"
},
"files": [
"README.md",
diff --git a/src/resources/test/dependency_queue_spec.lua b/src/resources/test/dependency_queue_spec.lua
index 85847cd..99aca35 100644
--- a/src/resources/test/dependency_queue_spec.lua
+++ b/src/resources/test/dependency_queue_spec.lua
@@ -169,10 +169,12 @@ describe("dependency_queue module", function()
describe("install flow", function()
local real_installPackage
local real_tempTimer
+ local real_downloadFile
before_each(function()
real_installPackage = _G.installPackage
real_tempTimer = _G.tempTimer
+ real_downloadFile = _G.downloadFile
-- Mock tempTimer to execute callback immediately (same pattern as Mudlet's own tests)
_G.tempTimer = function(time, code)
@@ -185,11 +187,15 @@ describe("dependency_queue module", function()
-- Mock installPackage to do nothing — we fire sysInstall manually
_G.installPackage = function() end
+
+ -- Mock downloadFile to do nothing — we fire sysDownloadDone/Error manually
+ _G.downloadFile = function() end
end)
after_each(function()
_G.installPackage = real_installPackage
_G.tempTimer = real_tempTimer
+ _G.downloadFile = real_downloadFile
-- Clean up the test package if it somehow got installed
if table.index_of(getPackages(), "ThreshCopy") then
@@ -211,8 +217,9 @@ describe("dependency_queue module", function()
dq.start()
- -- Simulate Mudlet firing sysInstall after the package installs
- raiseEvent("sysInstall", "ThreshCopy")
+ -- The download lands, then Mudlet fires sysInstall once installed
+ raiseEvent("sysDownloadDone", dq.current.target)
+ raiseEvent("sysInstallPackage", "ThreshCopy", dq.current.target)
assert.is_true(cb_success)
assert.is_nil(cb_message)
@@ -222,21 +229,70 @@ describe("dependency_queue module", function()
local cb_success = nil
local cb_message = nil
- g.dependency_queue.new_dependency_queue(
- {{name = "ThreshCopy", url = "https://example.com/fake"}},
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "ThreshCopy", url = "https://example.com/fake.mpackage"}},
function(success, message)
cb_success = success
cb_message = message
end
)
- -- Simulate Mudlet firing sysDownloadError
- raiseEvent("sysDownloadError", "ThreshCopy")
+ dq.start()
+
+ -- Mudlet fires sysDownloadError(error, local_file, url) for the file we chose
+ raiseEvent("sysDownloadError", "404 not found", dq.current.target,
+ "https://example.com/fake.mpackage")
assert.is_false(cb_success)
assert.is_truthy(cb_message)
end)
+ it("should install from the downloaded file when sysDownloadDone fires", function()
+ local installed_from = nil
+ _G.installPackage = function(path) installed_from = path end
+
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "FakePkgDone", url = "https://example.com/FakePkgDone.mpackage"}},
+ function() end
+ )
+
+ dq.start()
+
+ local target = dq.current.target
+ raiseEvent("sysDownloadDone", target)
+
+ assert.are.equal(target, installed_from)
+ dq.clean_up()
+ end)
+
+ it("should download to a uuid file name keeping the url extension", function()
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "FakePkgPath", url = "https://example.com/pkg/FakePkgPath.mpackage"}},
+ function() end
+ )
+
+ dq.start()
+
+ -- The name is a uuid; only the extension is carried over from the url
+ assert.is_truthy(string.find(dq.current.target, "%.mpackage$"))
+ assert.is_falsy(string.find(dq.current.target, "FakePkgPath"))
+ assert.is_truthy(string.find(dq.current.target,
+ "/%x+%-%x+%-%x+%-%x+%-%x+%.mpackage$"))
+ dq.clean_up()
+ end)
+
+ it("should keep a non-mpackage extension from the url", function()
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "FakePkgZip", url = "https://example.com/pkg/FakePkgZip.zip"}},
+ function() end
+ )
+
+ dq.start()
+
+ assert.is_truthy(string.find(dq.current.target, "%.zip$"))
+ dq.clean_up()
+ end)
+
it("should install multiple packages in sequence", function()
local cb_success = nil
@@ -252,10 +308,12 @@ describe("dependency_queue module", function()
dq.start()
- -- First package installs
- raiseEvent("sysInstall", "FakePkgA")
- -- Second package installs
- raiseEvent("sysInstall", "FakePkgB")
+ -- First package downloads, then installs
+ raiseEvent("sysDownloadDone", dq.current.target)
+ raiseEvent("sysInstallPackage", "FakePkgA", dq.current.target)
+ -- Second package downloads, then installs
+ raiseEvent("sysDownloadDone", dq.current.target)
+ raiseEvent("sysInstallPackage", "FakePkgB", dq.current.target)
assert.is_true(cb_success)
end)
@@ -291,10 +349,12 @@ describe("dependency_queue module", function()
describe("install flow edge cases", function()
local real_installPackage
local real_tempTimer
+ local real_downloadFile
before_each(function()
real_installPackage = _G.installPackage
real_tempTimer = _G.tempTimer
+ real_downloadFile = _G.downloadFile
_G.tempTimer = function(time, code)
if type(code) == "function" then
@@ -305,11 +365,13 @@ describe("dependency_queue module", function()
end
_G.installPackage = function() end
+ _G.downloadFile = function() end
end)
after_each(function()
_G.installPackage = real_installPackage
_G.tempTimer = real_tempTimer
+ _G.downloadFile = real_downloadFile
end)
it("should ignore sysInstall for non-matching package names", function()
@@ -323,9 +385,10 @@ describe("dependency_queue module", function()
)
dq.start()
+ raiseEvent("sysDownloadDone", dq.current.target)
- -- Fire sysInstall for a different package — should be ignored
- raiseEvent("sysInstall", "SomeOtherPackage")
+ -- Fire an install for a different package's file — should be ignored
+ raiseEvent("sysInstallPackage", "SomeOtherPackage", "/tmp/elsewhere/SomeOtherPackage.mpackage")
assert.is_nil(cb_success)
dq.clean_up()
@@ -349,7 +412,8 @@ describe("dependency_queue module", function()
dq.start()
-- First package fails to download
- raiseEvent("sysDownloadError", "FakePkgFirst")
+ raiseEvent("sysDownloadError", "404 not found", dq.current.target,
+ "https://example.com/first")
assert.is_false(cb_success)
assert.is_truthy(cb_message)
@@ -373,14 +437,16 @@ describe("dependency_queue module", function()
dq.start()
-- First package succeeds
- raiseEvent("sysInstall", "FakePkgAlpha")
+ raiseEvent("sysDownloadDone", dq.current.target)
+ raiseEvent("sysInstallPackage", "FakePkgAlpha", dq.current.target)
-- Reset to check second callback
cb_success = nil
cb_message = nil
- -- Second package fails
- raiseEvent("sysDownloadError", "FakePkgBeta")
+ -- Second package fails; current now points at the second download
+ raiseEvent("sysDownloadError", "404 not found", dq.current.target,
+ "https://example.com/beta")
assert.is_false(cb_success)
assert.is_truthy(cb_message)
@@ -394,13 +460,153 @@ describe("dependency_queue module", function()
local handler_name = dq.handler_name
dq.start()
- raiseEvent("sysInstall", "FakePkgCleanup")
+ raiseEvent("sysDownloadDone", dq.current.target)
+ raiseEvent("sysInstallPackage", "FakePkgCleanup", dq.current.target)
-- After completion, handler_name should be nil (cleaned up)
assert.is_nil(dq.handler_name)
assert.is_nil(dq.queue)
end)
+ it("should ignore a download error for an unrelated download", function()
+ local cb_success = nil
+
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "FakePkgMine", url = "https://example.com/mine.mpackage"}},
+ function(success, message)
+ cb_success = success
+ end
+ )
+
+ dq.start()
+
+ -- Something else in the profile fails to download — a path we did not choose
+ raiseEvent("sysDownloadError", "404 not found", "/tmp/theirs.mpackage",
+ "https://elsewhere.example.com/theirs.mpackage")
+
+ assert.is_nil(cb_success)
+ assert.is_not_nil(dq.handler_name)
+ assert.is_not_nil(dq.queue)
+
+ -- Our own download and install still complete normally afterwards
+ raiseEvent("sysDownloadDone", dq.current.target)
+ raiseEvent("sysInstallPackage", "FakePkgMine", dq.current.target)
+
+ assert.is_true(cb_success)
+ end)
+
+ it("should ignore an unrelated download sharing our file name", function()
+ local cb_success = nil
+
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "FakePkgSame", url = "https://example.com/same.mpackage"}},
+ function(success, message)
+ cb_success = success
+ end
+ )
+
+ dq.start()
+
+ -- Same final file name, different directory: the uuid keeps it distinct
+ local collision = string.gsub(dq.current.target, "same%.mpackage$", "") ..
+ "../other/same.mpackage"
+
+ raiseEvent("sysDownloadError", "404 not found", collision,
+ "https://elsewhere.example.com/same.mpackage")
+
+ assert.is_nil(cb_success)
+ assert.is_not_nil(dq.queue)
+ dq.clean_up()
+ end)
+
+ it("should match a download error however the url was redirected", function()
+ local cb_success = nil
+
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "FakePkgRedir", url = "https://example.com/pkg/redir.mpackage"}},
+ function(success, message)
+ cb_success = success
+ end
+ )
+
+ dq.start()
+
+ -- Redirection rewrote the url to a signed path with no recognisable file
+ -- name; identity comes from the local path, so this is still ours
+ raiseEvent("sysDownloadError", "connection reset", dq.current.target,
+ "https://cdn.example.net/asset/5dd0c819-e4ee-447e-b18f-24a7b5ed17fe?sig=abc")
+
+ assert.is_false(cb_success)
+ end)
+
+ it("should ignore an install of the same name from elsewhere", function()
+ local cb_success = nil
+
+ local dq = g.dependency_queue.new_dependency_queue(
+ {
+ {name = "FakePkgRace", url = "https://example.com/race.mpackage"},
+ {name = "FakePkgAfter", url = "https://example.com/after.mpackage"},
+ },
+ function(success, message)
+ cb_success = success
+ end
+ )
+
+ dq.start()
+
+ local target = dq.current.target
+
+ -- Another component installs a package of the same name from its own
+ -- file — same name, different path, so it is not ours
+ raiseEvent("sysInstallPackage", "FakePkgRace", "/tmp/elsewhere/FakePkgRace.mpackage")
+
+ assert.is_nil(cb_success)
+ assert.are.equal(2, #dq.packages)
+ assert.is_not_nil(dq.current)
+ assert.are.equal(target, dq.current.target)
+
+ -- Our own download and install still proceed normally
+ raiseEvent("sysDownloadDone", target)
+ raiseEvent("sysInstallPackage", "FakePkgRace", target)
+
+ assert.are.equal(1, #dq.packages)
+ dq.clean_up()
+ end)
+
+ it("should not discard the active download on an install from elsewhere", function()
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "FakePkgKeep", url = "https://example.com/keep.mpackage"}},
+ function() end
+ )
+
+ dq.start()
+
+ local target = dq.current.target
+ raiseEvent("sysInstallPackage", "FakePkgKeep", "/tmp/elsewhere/FakePkgKeep.mpackage")
+
+ -- Our in-flight download must not be discarded
+ assert.is_not_nil(dq.current)
+ assert.are.equal(target, dq.current.target)
+ dq.clean_up()
+ end)
+
+ it("should ignore sysDownloadDone for an unrelated download", function()
+ local installed = false
+ _G.installPackage = function() installed = true end
+
+ local dq = g.dependency_queue.new_dependency_queue(
+ {{name = "FakePkgOther", url = "https://example.com/other.mpackage"}},
+ function() end
+ )
+
+ dq.start()
+
+ raiseEvent("sysDownloadDone", "/tmp/not-ours.mpackage")
+
+ assert.is_false(installed)
+ dq.clean_up()
+ end)
+
it("should clean up event handlers after download error", function()
local dq = g.dependency_queue.new_dependency_queue(
{{name = "FakePkgErrClean", url = "https://example.com/errclean"}},
@@ -408,7 +614,8 @@ describe("dependency_queue module", function()
)
dq.start()
- raiseEvent("sysDownloadError", "FakePkgErrClean")
+ raiseEvent("sysDownloadError", "404 not found", dq.current.target,
+ "https://example.com/errclean")
assert.is_nil(dq.handler_name)
assert.is_nil(dq.queue)
diff --git a/src/scripts/conditions.lua b/src/scripts/conditions.lua
index 12ec8cb..5231e26 100644
--- a/src/scripts/conditions.lua
+++ b/src/scripts/conditions.lua
@@ -155,7 +155,7 @@ local ConditionsClass = Glu.glass.register({
--- Checks if a value is of a specific type.
---@param value any The value to check
- ---@param type string The type to check against
+ ---@param expected_type string The type to check against
---@param message string|nil The message to return if the values are not of the specified type
---@return boolean ok The condition
---@return string|nil message The message
diff --git a/src/scripts/dependency_queue.lua b/src/scripts/dependency_queue.lua
index ac82bc5..423eb36 100644
--- a/src/scripts/dependency_queue.lua
+++ b/src/scripts/dependency_queue.lua
@@ -3,11 +3,12 @@ local DependencyQueueClass = Glu.glass.register({
name = "dependency_queue",
extends = "queue",
call = "new_dependency_queue",
- dependencies = { "queue", "table", },
+ dependencies = { "queue", "table", "fd", "url", },
setup = function(___, self)
--- Builds a queue that installs any of the given Mudlet packages that are not already installed.
- --- Filters out already-installed packages, wires up sysInstall and sysDownloadError event
- --- handlers, and invokes the callback with success and a message when finished.
+ --- Filters out already-installed packages, downloads each remaining package into its own
+ --- temporary directory, installs it from there, and invokes the callback with success and a
+ --- message when finished.
---
---@param packages table List of package descriptors, each a {name, url} table.
---@param cb function Callback called with a success boolean and a message string when finished.
@@ -30,6 +31,7 @@ local DependencyQueueClass = Glu.glass.register({
cb = cb,
queue = self.new_queue(),
packages = not_installed,
+ current = nil,
handler_name = f "dependency_{id}_installed",
}
___.table.add(self, this)
@@ -37,16 +39,46 @@ local DependencyQueueClass = Glu.glass.register({
for _, package in ipairs(not_installed) do
local func = function()
cecho("Installing dependency `" .. package.name .. "`...\n")
- installPackage(package.url)
+
+ -- Mudlet's download and install events are global, and the URL they
+ -- carry may have been rewritten by redirection, so the URL cannot
+ -- identify an event as ours. The local path can: we name the file
+ -- for a fresh UUID that nothing else in the profile can collide
+ -- with. The name is ours to choose because Mudlet takes the package
+ -- name from the archive's config.lua manifest, not from the file --
+ -- only the extension has to survive, since that selects the format.
+ local dir = ___.fd.fix_path(getMudletHomeDir() .. "/tmp")
+ local ok, err = ___.fd.assure_dir(dir)
+ if not ok then
+ local reason = err or "unknown error"
+ self.cb(false, f "Could not create a temporary directory for `{package.name}`: {reason}\n")
+ self.clean_up()
+ return
+ end
+
+ local parsed = ___.url.parse(package.url)
+ local ext = parsed and parsed.file and string.match(parsed.file, "%.(%w+)$") or "mpackage"
+
+ self.current = {
+ package = package,
+ target = ___.fd.fix_path(dir .. "/" .. ___.id() .. "." .. ext),
+ }
+
+ downloadFile(self.current.target, package.url)
end
self.queue.push(func)
end
- registerNamedEventHandler("glu", self.handler_name, "sysInstall",
- function(event, package)
- if package ~= self.packages[1].name then return end
+ registerNamedEventHandler("glu", self.handler_name, "sysInstallPackage",
+ function(event, package, file)
+ -- sysInstallPackage reports the file Mudlet was asked to install, so
+ -- the same UUID path that identifies our downloads identifies our
+ -- installs. sysInstall carries only a package name, which any other
+ -- component installing something of that name would also produce.
+ if not self.is_ours(file) then return end
+ self.discard_temp()
___.table.shift(self.packages)
tempTimer(1, function()
local q, count = self.queue.execute()
@@ -58,17 +90,59 @@ local DependencyQueueClass = Glu.glass.register({
end
)
+ registerNamedEventHandler("glu", self.handler_name .. "_download_done", "sysDownloadDone",
+ function(event, local_file)
+ if not self.is_ours(local_file) then return end
+
+ installPackage(self.current.target)
+ end
+ )
+
registerNamedEventHandler("glu", self.handler_name .. "_download_error", "sysDownloadError",
- function(event, package)
- self.cb(false, f "Failed to download dependency `{package}`.\nCleaning up.\n")
+ function(event, err, local_file, url)
+ if not self.is_ours(local_file) then return end
+
+ local name = self.current.package.name
+ local reason = err or "unknown error"
+
+ self.discard_temp()
+ self.cb(false, f "Failed to download dependency `{name}`: {reason}\nCleaning up.\n")
self.clean_up()
end
)
+ --- Determines whether a download or install event refers to the file this
+ --- queue is currently working on. The path was chosen by us and contains a UUID,
+ --- so an exact match is unambiguous and no guessing is required.
+ ---
+ ---@param local_file string? The local path reported by the download event.
+ ---@return boolean result Whether the event refers to our download.
+ function self.is_ours(local_file)
+ if not self.current or type(local_file) ~= "string" then return false end
+
+ return ___.fd.fix_path(local_file) == self.current.target
+ end
+
+ --- Removes the temporary file holding the current download, if any.
+ --- Safe to call more than once.
+ function self.discard_temp()
+ local current = self.current
+ if not current then return end
+
+ if ___.fd.file_exists(current.target) then ___.fd.rmfile(current.target) end
+
+ self.current = nil
+ end
+
--- Removes the registered event handlers and tears down the queue.
- --- Deletes the named sysInstall and sysDownloadError handlers and clears the handler name and queue.
+ --- Deletes the named sysInstallPackage, sysDownloadDone and sysDownloadError
+ --- handlers, discards any temporary download, and clears the handler name
+ --- and queue.
function self.clean_up()
+ self.discard_temp()
+
deleteNamedEventHandler("glu", self.handler_name)
+ deleteNamedEventHandler("glu", self.handler_name .. "_download_done")
deleteNamedEventHandler("glu", self.handler_name .. "_download_error")
self.handler_name = nil
self.queue = nil