aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/installer/InstallRunner.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/mason-core/installer/InstallRunner.lua')
-rw-r--r--lua/mason-core/installer/InstallRunner.lua227
1 files changed, 227 insertions, 0 deletions
diff --git a/lua/mason-core/installer/InstallRunner.lua b/lua/mason-core/installer/InstallRunner.lua
new file mode 100644
index 00000000..fa2b3fcf
--- /dev/null
+++ b/lua/mason-core/installer/InstallRunner.lua
@@ -0,0 +1,227 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local a = require "mason-core.async"
+local compiler = require "mason-core.installer.compiler"
+local control = require "mason-core.async.control"
+local fs = require "mason-core.fs"
+local linker = require "mason-core.installer.linker"
+local log = require "mason-core.log"
+local registry = require "mason-registry"
+
+local OneShotChannel = control.OneShotChannel
+
+local InstallContext = require "mason-core.installer.context"
+
+---@class InstallRunner
+---@field handle InstallHandle
+---@field global_semaphore Semaphore
+---@field global_permit Permit?
+---@field package_permit Permit?
+local InstallRunner = {}
+InstallRunner.__index = InstallRunner
+
+---@param handle InstallHandle
+---@param semaphore Semaphore
+function InstallRunner:new(handle, semaphore)
+ ---@type InstallRunner
+ local instance = {}
+ setmetatable(instance, self)
+ instance.location = location
+ instance.global_semaphore = semaphore
+ instance.handle = handle
+ return instance
+end
+
+---@alias InstallRunnerCallback fun(success: true, receipt: InstallReceipt) | fun(success: false, handle: InstallHandle, error: any)
+
+---@param opts PackageInstallOpts
+---@param callback? InstallRunnerCallback
+function InstallRunner:execute(opts, callback)
+ local handle = self.handle
+ log.fmt_info("Executing installer for %s %s", handle.package, opts)
+
+ local context = InstallContext:new(handle, opts)
+
+ local tailed_output = {}
+
+ if opts.debug then
+ local function append_log(chunk)
+ tailed_output[#tailed_output + 1] = chunk
+ end
+ handle:on("stdout", append_log)
+ handle:on("stderr", append_log)
+ end
+
+ ---@async
+ local function finalize_logs(success, result)
+ if not success then
+ context.stdio_sink.stderr(tostring(result))
+ context.stdio_sink.stderr "\n"
+ end
+
+ if opts.debug then
+ context.fs:write_file("mason-debug.log", table.concat(tailed_output, ""))
+ context.stdio_sink.stdout(("[debug] Installation directory retained at %q.\n"):format(context.cwd:get()))
+ end
+ end
+
+ ---@async
+ local finalize = a.scope(function(success, result)
+ finalize_logs(success, result)
+
+ if not opts.debug and not success then
+ -- clean up installation dir
+ pcall(function()
+ fs.async.rmrf(context.cwd:get())
+ end)
+ end
+
+ if not handle:is_closing() then
+ handle:close()
+ end
+
+ self:release_lock()
+ self:release_permit()
+
+ if success then
+ log.fmt_info("Installation succeeded for %s", handle.package)
+ if callback then
+ callback(true, result.receipt)
+ end
+ handle.package:emit("install:success", result.receipt)
+ registry:emit("package:install:success", handle.package, result.receipt)
+ else
+ log.fmt_error("Installation failed for %s error=%s", handle.package, result)
+ if callback then
+ callback(false, result)
+ end
+ handle.package:emit("install:failed", result)
+ registry:emit("package:install:failed", handle.package, result)
+ end
+ end)
+
+ local cancel_execution = a.run(function()
+ return Result.try(function(try)
+ try(self.handle.location:initialize())
+ try(self:acquire_permit()):receive()
+ try(self:acquire_lock(opts.force))
+
+ context.receipt:with_start_time(vim.loop.gettimeofday())
+
+ -- 1. initialize working directory
+ try(context.cwd:initialize())
+
+ -- 2. run installer
+ ---@type async fun(ctx: InstallContext): Result
+ local installer = try(compiler.compile_installer(handle.package.spec, opts))
+ try(context:execute(installer))
+
+ -- 3. promote temporary installation dir
+ try(Result.pcall(function()
+ context:promote_cwd()
+ end))
+
+ -- 4. link package & write receipt
+ try(linker.link(context):on_failure(function()
+ -- unlink any links that were made before failure
+ context:build_receipt():on_success(
+ ---@param receipt InstallReceipt
+ function(receipt)
+ linker.unlink(handle.package, receipt, self.handle.location):on_failure(function(err)
+ log.error("Failed to unlink failed installation.", err)
+ end)
+ end
+ )
+ end))
+ ---@type InstallReceipt
+ local receipt = try(context:build_receipt())
+ try(Result.pcall(fs.sync.write_file, handle.location:receipt(handle.package.name), receipt:to_json()))
+ return {
+ receipt = receipt,
+ }
+ end):get_or_throw()
+ end, finalize)
+
+ handle:once("terminate", function()
+ cancel_execution()
+ local function on_close()
+ finalize(false, "Installation was aborted.")
+ end
+ if handle:is_closed() then
+ on_close()
+ else
+ handle:once("closed", on_close)
+ end
+ end)
+end
+
+---@async
+---@private
+function InstallRunner:release_lock()
+ pcall(fs.async.unlink, self.handle.location:lockfile(self.handle.package.name))
+end
+
+---@async
+---@param force boolean?
+---@private
+function InstallRunner:acquire_lock(force)
+ local pkg = self.handle.package
+ log.debug("Attempting to lock package", pkg)
+ local lockfile = self.handle.location:lockfile(pkg.name)
+ if force ~= true and fs.async.file_exists(lockfile) then
+ log.error("Lockfile already exists.", pkg)
+ return Result.failure(
+ ("Lockfile exists, installation is already running in another process (pid: %s). Run with :MasonInstall --force to bypass."):format(
+ fs.async.read_file(lockfile)
+ )
+ )
+ end
+ a.scheduler()
+ fs.async.write_file(lockfile, vim.fn.getpid())
+ log.debug("Wrote lockfile", pkg)
+ return Result.success(lockfile)
+end
+
+---@private
+function InstallRunner:acquire_permit()
+ local channel = OneShotChannel:new()
+ log.fmt_debug("Acquiring permit for %s", self.handle.package)
+ local handle = self.handle
+ if handle:is_active() or handle:is_closing() then
+ log.fmt_debug("Received active or closing handle %s", handle)
+ return Result.failure "Invalid handle state."
+ end
+
+ handle:queued()
+ a.run(function()
+ self.global_permit = self.global_semaphore:acquire()
+ self.package_permit = handle.package:acquire_permit()
+ end, function(success, err)
+ if not success or handle:is_closing() then
+ if not success then
+ log.error("Acquiring permits failed", err)
+ end
+ self:release_permit()
+ else
+ log.fmt_debug("Activating handle %s", handle)
+ handle:active()
+ channel:send()
+ end
+ end)
+
+ return Result.success(channel)
+end
+
+---@private
+function InstallRunner:release_permit()
+ if self.global_permit then
+ self.global_permit:forget()
+ self.global_permit = nil
+ end
+ if self.package_permit then
+ self.package_permit:forget()
+ self.package_permit = nil
+ end
+end
+
+return InstallRunner