diff options
| author | William Boman <william@redwill.se> | 2023-11-07 00:29:18 +0100 |
|---|---|---|
| committer | William Boman <william@redwill.se> | 2025-02-19 12:15:48 +0100 |
| commit | 6a7662760c515c74f2c37fc825776ead65d307f9 (patch) | |
| tree | 0f4496d0678c7029b10236cbf48cc0f5ff63c1dc /lua/mason-core/installer/InstallHandle.lua | |
| parent | fix(pypi): remove -U flag and fix log message (diff) | |
| download | mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.gz mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.bz2 mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.lz mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.xz mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.zst mason-6a7662760c515c74f2c37fc825776ead65d307f9.zip | |
refactor!: change Package API
This changes the following public APIs:
**(_breaking_) Events on the `Package` class**
The `uninstall:success` event on the `Package` class now receives an `InstallReceipt` as argument, instead of an
`InstallHandle`. This receipt is an in-memory representation of what was uninstalled. There's also a new
`uninstall:failed` event for situations where uninstallation for some
reason fails. Note: this also applies to the registry events (i.e.
`package:uninstall:success` and `package:uninstall:failed`).
---
**(_breaking_) `Package:uninstall()` is now asynchronous and receives two new arguments, similarly to `Package:install()`**
While package uninstallations remain synchronous under the hood, the public API has been changed from synchronous ->
asynchronous. Users of this method are recommended to provide a callback in situations where code needs to execute after
uninstallation fully completes.
---
**(_breaking_) `Package:get_install_path()` has been removed.
---
**`Package:install()` now takes an optional callback**
This callback allows consumers to be informed whether installation was successful or not without having to go through a
different, low-level, API. See below for a comparison between the old and new APIs:
```lua
-- before
local handle = pkg:install()
handle:once("closed", function ()
-- ...
end)
-- after
pkg:install({}, function (success, result)
-- ...
end)
```
Diffstat (limited to 'lua/mason-core/installer/InstallHandle.lua')
| -rw-r--r-- | lua/mason-core/installer/InstallHandle.lua | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/lua/mason-core/installer/InstallHandle.lua b/lua/mason-core/installer/InstallHandle.lua new file mode 100644 index 00000000..f5a42c53 --- /dev/null +++ b/lua/mason-core/installer/InstallHandle.lua @@ -0,0 +1,228 @@ +local EventEmitter = require "mason-core.EventEmitter" +local Optional = require "mason-core.optional" +local _ = require "mason-core.functional" +local a = require "mason-core.async" +local log = require "mason-core.log" +local platform = require "mason-core.platform" +local process = require "mason-core.process" +local spawn = require "mason-core.spawn" + +local uv = vim.loop + +---@alias InstallHandleState +--- | '"IDLE"' +--- | '"QUEUED"' +--- | '"ACTIVE"' +--- | '"CLOSED"' + +---@class InstallHandleSpawnHandle +---@field uv_handle luv_handle +---@field pid integer +---@field cmd string +---@field args string[] +local InstallHandleSpawnHandle = {} +InstallHandleSpawnHandle.__index = InstallHandleSpawnHandle + +---@param luv_handle luv_handle +---@param pid integer +---@param cmd string +---@param args string[] +function InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args) + ---@type InstallHandleSpawnHandle + local instance = {} + setmetatable(instance, InstallHandleSpawnHandle) + instance.uv_handle = luv_handle + instance.pid = pid + instance.cmd = cmd + instance.args = args + return instance +end + +function InstallHandleSpawnHandle:__tostring() + return ("%s %s"):format(self.cmd, table.concat(self.args, " ")) +end + +---@class InstallHandle : EventEmitter +---@field package AbstractPackage +---@field state InstallHandleState +---@field stdio { buffers: { stdout: string[], stderr: string[] }, sink: StdioSink } +---@field is_terminated boolean +---@field location InstallLocation +---@field private spawn_handles InstallHandleSpawnHandle[] +local InstallHandle = {} +InstallHandle.__index = InstallHandle +setmetatable(InstallHandle, { __index = EventEmitter }) + +---@param handle InstallHandle +local function new_sink(handle) + local stdout, stderr = {}, {} + return { + buffers = { stdout = stdout, stderr = stderr }, + sink = { + stdout = function(chunk) + stdout[#stdout + 1] = chunk + handle:emit("stdout", chunk) + end, + stderr = function(chunk) + stderr[#stderr + 1] = chunk + handle:emit("stderr", chunk) + end, + }, + } +end + +---@param pkg AbstractPackage +---@param location InstallLocation +function InstallHandle:new(pkg, location) + ---@type InstallHandle + local instance = EventEmitter.new(self) + instance.state = "IDLE" + instance.package = pkg + instance.spawn_handles = {} + instance.stdio = new_sink(instance) + instance.is_terminated = false + instance.location = location + return instance +end + +---@param luv_handle luv_handle +---@param pid integer +---@param cmd string +---@param args string[] +function InstallHandle:register_spawn_handle(luv_handle, pid, cmd, args) + local spawn_handles = InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args) + log.fmt_trace("Pushing spawn_handles stack for %s: %s (pid: %s)", self, spawn_handles, pid) + self.spawn_handles[#self.spawn_handles + 1] = spawn_handles + self:emit "spawn_handles:change" +end + +---@param luv_handle luv_handle +function InstallHandle:deregister_spawn_handle(luv_handle) + for i = #self.spawn_handles, 1, -1 do + if self.spawn_handles[i].uv_handle == luv_handle then + log.fmt_trace("Popping spawn_handles stack for %s: %s", self, self.spawn_handles[i]) + table.remove(self.spawn_handles, i) + self:emit "spawn_handles:change" + return true + end + end + return false +end + +---@return Optional # Optional<InstallHandleSpawnHandle> +function InstallHandle:peek_spawn_handle() + return Optional.of_nilable(self.spawn_handles[#self.spawn_handles]) +end + +function InstallHandle:is_idle() + return self.state == "IDLE" +end + +function InstallHandle:is_queued() + return self.state == "QUEUED" +end + +function InstallHandle:is_active() + return self.state == "ACTIVE" +end + +function InstallHandle:is_closed() + return self.state == "CLOSED" +end + +function InstallHandle:is_closing() + return self:is_closed() or self.is_terminated +end + +---@param new_state InstallHandleState +function InstallHandle:set_state(new_state) + local old_state = self.state + self.state = new_state + log.fmt_trace("Changing %s state from %s to %s", self, old_state, new_state) + self:emit("state:change", new_state, old_state) +end + +---@param signal integer +function InstallHandle:kill(signal) + assert(not self:is_closed(), "Cannot kill closed handle.") + log.fmt_trace("Sending signal %s to luv handles in %s", signal, self) + for _, spawn_handles in pairs(self.spawn_handles) do + process.kill(spawn_handles.uv_handle, signal) + end + self:emit("kill", signal) +end + +---@param pid integer +local win_taskkill = a.scope(function(pid) + spawn.taskkill { + "/f", + "/t", + "/pid", + pid, + } +end) + +function InstallHandle:terminate() + assert(not self:is_closed(), "Cannot terminate closed handle.") + if self.is_terminated then + log.fmt_trace("Handle is already terminated %s", self) + return + end + log.fmt_trace("Terminating %s", self) + -- https://github.com/libuv/libuv/issues/1133 + if platform.is.win then + for _, spawn_handles in ipairs(self.spawn_handles) do + win_taskkill(spawn_handles.pid) + end + else + self:kill(15) -- SIGTERM + end + self.is_terminated = true + self:emit "terminate" + local check = uv.new_check() + check:start(function() + for _, spawn_handles in ipairs(self.spawn_handles) do + local luv_handle = spawn_handles.uv_handle + local ok, is_closing = pcall(luv_handle.is_closing, luv_handle) + if ok and not is_closing then + return + end + end + check:stop() + if not self:is_closed() then + self:close() + end + end) +end + +function InstallHandle:queued() + assert(self:is_idle(), "Can only queue idle handles.") + self:set_state "QUEUED" +end + +function InstallHandle:active() + assert(self:is_idle() or self:is_queued(), "Can only activate idle or queued handles.") + self:set_state "ACTIVE" +end + +function InstallHandle:close() + log.fmt_trace("Closing %s", self) + assert(not self:is_closed(), "Handle is already closed.") + for _, spawn_handles in ipairs(self.spawn_handles) do + local luv_handle = spawn_handles.uv_handle + local ok, is_closing = pcall(luv_handle.is_closing, luv_handle) + if ok then + assert(is_closing, "There are open libuv handles.") + end + end + self.spawn_handles = {} + self:set_state "CLOSED" + self:emit "closed" + self:__clear_event_handlers() +end + +function InstallHandle:__tostring() + return ("InstallHandle(package=%s, state=%s)"):format(self.package, self.state) +end + +return InstallHandle |
