aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/process.lua
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2022-07-08 18:34:38 +0200
committerGitHub <noreply@github.com>2022-07-08 18:34:38 +0200
commit976aa4fbee8a070f362cab6f6ec84e9251a90cf9 (patch)
tree5e8d9c9c59444a25c7801b8f39763c4ba6e1f76d /lua/mason-core/process.lua
parentfeat: add gotests, gomodifytags, impl (#28) (diff)
downloadmason-976aa4fbee8a070f362cab6f6ec84e9251a90cf9.tar
mason-976aa4fbee8a070f362cab6f6ec84e9251a90cf9.tar.gz
mason-976aa4fbee8a070f362cab6f6ec84e9251a90cf9.tar.bz2
mason-976aa4fbee8a070f362cab6f6ec84e9251a90cf9.tar.lz
mason-976aa4fbee8a070f362cab6f6ec84e9251a90cf9.tar.xz
mason-976aa4fbee8a070f362cab6f6ec84e9251a90cf9.tar.zst
mason-976aa4fbee8a070f362cab6f6ec84e9251a90cf9.zip
refactor: add mason-schemas and mason-core modules (#29)
* refactor: add mason-schemas and move generated filetype map to mason-lspconfig * refactor: add mason-core module
Diffstat (limited to 'lua/mason-core/process.lua')
-rw-r--r--lua/mason-core/process.lua213
1 files changed, 213 insertions, 0 deletions
diff --git a/lua/mason-core/process.lua b/lua/mason-core/process.lua
new file mode 100644
index 00000000..fd4eb94f
--- /dev/null
+++ b/lua/mason-core/process.lua
@@ -0,0 +1,213 @@
+local log = require "mason-core.log"
+local _ = require "mason-core.functional"
+local platform = require "mason-core.platform"
+local uv = vim.loop
+
+---@alias luv_pipe any
+---@alias luv_handle any
+
+---@class StdioSink
+---@field stdout fun(chunk: string)
+---@field stderr fun(chunk: string)
+
+local M = {}
+
+---@param pipe luv_pipe
+---@param sink fun(chunk: string)
+local function connect_sink(pipe, sink)
+ ---@param err string | nil
+ ---@param data string | nil
+ return function(err, data)
+ if err then
+ log.error("Unexpected error when reading pipe.", err)
+ end
+ if data ~= nil then
+ sink(data)
+ else
+ pipe:read_stop()
+ pipe:close()
+ end
+ end
+end
+
+-- We gather the root env immediately, primarily because of E5560.
+-- Also, there's no particular reason we need to refresh the environment (yet).
+local initial_environ = vim.fn.environ()
+
+---@param new_paths string[] @A list of paths to prepend the existing PATH with.
+function M.extend_path(new_paths)
+ local new_path_str = table.concat(new_paths, platform.path_sep)
+ return ("%s%s%s"):format(new_path_str, platform.path_sep, initial_environ.PATH or "")
+end
+
+---Merges the provided env param with the user's full environent. Provided env has precedence.
+---@param env table<string, string>
+---@param excluded_var_names string[]|nil
+function M.graft_env(env, excluded_var_names)
+ local excluded_var_names_set = excluded_var_names and _.set_of(excluded_var_names) or {}
+ local merged_env = {}
+ for key, val in pairs(initial_environ) do
+ if not excluded_var_names_set[key] and env[key] == nil then
+ merged_env[#merged_env + 1] = key .. "=" .. val
+ end
+ end
+ for key, val in pairs(env) do
+ if not excluded_var_names_set[key] then
+ merged_env[#merged_env + 1] = key .. "=" .. val
+ end
+ end
+ return merged_env
+end
+
+---@param env_list string[]
+local function sanitize_env_list(env_list)
+ local sanitized_list = {}
+ for __, env in ipairs(env_list) do
+ local safe_envs = {
+ "GO111MODULE",
+ "GOBIN",
+ "GOPATH",
+ "PATH",
+ "GEM_HOME",
+ "GEM_PATH",
+ }
+ local is_safe_env = _.any(function(safe_env)
+ return env:find(safe_env .. "=") == 1
+ end, safe_envs)
+ if is_safe_env then
+ sanitized_list[#sanitized_list + 1] = env
+ else
+ local idx = env:find "="
+ sanitized_list[#sanitized_list + 1] = env:sub(1, idx) .. "<redacted>"
+ end
+ end
+ return sanitized_list
+end
+
+---@alias JobSpawnCallback fun(success: boolean, exit_code: integer, signal: integer)
+
+---@class JobSpawnOpts
+---@field env string[] @List of "key=value" string.
+---@field args string[]
+---@field cwd string
+---@field stdio_sink StdioSink
+
+---@param cmd string @The command/executable.
+---@param opts JobSpawnOpts
+---@param callback JobSpawnCallback
+---@return luv_handle,luv_pipe[],integer @Returns the job handle and the stdio array on success, otherwise returns nil.
+function M.spawn(cmd, opts, callback)
+ local stdin = uv.new_pipe(false)
+ local stdout = uv.new_pipe(false)
+ local stderr = uv.new_pipe(false)
+
+ local stdio = { stdin, stdout, stderr }
+
+ local spawn_opts = {
+ env = opts.env,
+ stdio = stdio,
+ args = opts.args,
+ cwd = opts.cwd,
+ detached = false,
+ hide = true,
+ }
+
+ log.lazy_debug(function()
+ local sanitized_env = opts.env and sanitize_env_list(opts.env) or nil
+ return "Spawning cmd=%s, spawn_opts=%s",
+ cmd,
+ {
+ args = opts.args,
+ cwd = opts.cwd,
+ env = sanitized_env,
+ }
+ end)
+
+ local handle, pid_or_err
+ handle, pid_or_err = uv.spawn(cmd, spawn_opts, function(exit_code, signal)
+ local successful = exit_code == 0 and signal == 0
+ handle:close()
+ if not stdin:is_closing() then
+ stdin:close()
+ end
+
+ -- ensure all pipes are closed, for I am a qualified plumber
+ local check = uv.new_check()
+ check:start(function()
+ for i = 1, #stdio do
+ local pipe = stdio[i]
+ if not pipe:is_closing() then
+ return
+ end
+ end
+ check:stop()
+ callback(successful, exit_code, signal)
+ end)
+
+ log.fmt_debug("Job pid=%s exited with exit_code=%s, signal=%s", pid_or_err, exit_code, signal)
+ end)
+
+ if handle == nil then
+ log.fmt_error("Failed to spawn process. cmd=%s, err=%s", cmd, pid_or_err)
+ if type(pid_or_err) == "string" and pid_or_err:find "ENOENT" == 1 then
+ opts.stdio_sink.stderr(("Could not find executable %q in path.\n"):format(cmd))
+ else
+ opts.stdio_sink.stderr(("Failed to spawn process cmd=%s err=%s\n"):format(cmd, pid_or_err))
+ end
+ callback(false)
+ return nil, nil
+ end
+
+ log.debug("Spawned with pid", pid_or_err)
+
+ stdout:read_start(connect_sink(stdout, opts.stdio_sink.stdout))
+ stderr:read_start(connect_sink(stderr, opts.stdio_sink.stderr))
+
+ return handle, stdio, pid_or_err
+end
+
+function M.empty_sink()
+ local function noop() end
+ return {
+ stdout = noop,
+ stderr = noop,
+ }
+end
+
+function M.simple_sink()
+ return {
+ stdout = vim.schedule_wrap(vim.api.nvim_out_write),
+ stderr = vim.schedule_wrap(vim.api.nvim_err_write),
+ }
+end
+
+function M.in_memory_sink()
+ local stdout, stderr = {}, {}
+ return {
+ buffers = { stdout = stdout, stderr = stderr },
+ sink = {
+ stdout = function(chunk)
+ stdout[#stdout + 1] = chunk
+ end,
+ stderr = function(chunk)
+ stderr[#stderr + 1] = chunk
+ end,
+ },
+ }
+end
+
+---@param luv_handle luv_handle
+---@param signal integer
+function M.kill(luv_handle, signal)
+ assert(type(signal) == "number", "signal is not a number")
+ assert(signal > 0 and signal < 32, "signal must be between 1-31")
+ log.fmt_trace("Sending signal %s to handle %s", signal, luv_handle)
+ local ok, is_active = pcall(uv.is_active, luv_handle)
+ if not ok or not is_active then
+ log.fmt_trace("Tried to send signal %s to inactive uv handle.", signal)
+ return
+ end
+ uv.process_kill(luv_handle, signal)
+end
+
+return M