[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/harrisoncramer/gitlab.nvim/develop/lua/gitlab/server.lua [Back]  [Original]

-- This module contains the logic responsible for building and starting
-- the Golang server. The Go server is responsible for making API calls
-- to Gitlab and returning the data
local state = require("gitlab.state")
local u = require("gitlab.utils")
local client = require("gitlab.client")
local version = require("gitlab.version")
local GitlabGroup = require("gitlab.autocmd")

local M = {}

---@type vim.SystemObj?
local server_system_obj
local kill_autocmd_created = false
-- Set when the VimLeavePre autocmd kills the server, so that the server's
-- on_exit callback skips the "server exited" error notification: it would
-- otherwise fire (and might fail to render) while nvim is tearing down.
local killed_on_exit = false

-- Builds the binary if it doesn't exist, and starts the server. If the pre-existing binary has an older
-- tag than the Lua code (exposed via the /version endpoint) then shuts down the server, rebuilds it, and
-- restarts the server again.
M.build_and_start = function(callback)
  if M.build(false) == false then
    return
  end

  -- When the user provides their own binary, skip the version check and rebuild cycle.
  -- The user is responsible for keeping their binary up to date.
  local user_provided_binary = state.settings.server.binary_provided

  M.start(function()
    if user_provided_binary then
      callback()
      return
    end
    M.get_version(function(versions)
      if versions.plugin_version ~= versions.binary_version then
        M.shutdown(function()
          if M.build(true) then
            M.start(callback)
          end
        end)
      else
        callback()
      end
    end)
  end)
end

---Starts the Go server and call the callback provided
M.start = function(callback)
  if state.settings.port ~= nil and state.settings.server.port == nil then
    state.settings.server.port = state.settings.port
    u.notify("The setting `port` has been renamed `server.port`", vim.log.levels.WARN)
  end
  local port = tonumber(state.settings.server.port) or 0
  local parsed_port = nil
  local callback_called = false

  local go_server_settings = {
    gitlab_url = state.settings.gitlab_url,
    port = port,
    auth_token = state.settings.auth_token,
    debug = state.settings.debug,
    log_path = state.settings.log_path,
    connection_settings = state.settings.connection_settings,
    chosen_mr_iid = state.chosen_mr_iid,
  }

  state.chosen_mr_iid = 0 -- Do not let this interfere with subsequent reviewer.open() calls

  local settings = vim.json.encode(go_server_settings)

  if not kill_autocmd_created then
    kill_autocmd_created = true
    vim.api.nvim_create_autocmd({ "VimLeavePre" }, {
      callback = function()
        if server_system_obj == nil then
          return
        end
        killed_on_exit = true
        -- Send SIGKILL rather than SIGTERM: the server is a stateless proxy to the
        -- Gitlab API, and a graceful shutdown would wait for in-flight requests.
        -- Use `pcall` because the handle is already closed if the process exited on
        -- its own, e.g. through the /shutdown endpoint.
        pcall(function()
          server_system_obj:kill("sigkill")
        end)
        server_system_obj = nil
      end,
      desc = "Kill the gitlab.nvim Go server process",
      group = GitlabGroup,
    })
  end

  local ok, obj = pcall(vim.system, { state.settings.server.binary, settings }, {
    -- The `stdin` param opens a pipe on the server's stdin that nvim holds open but
    -- never writes to. The server exits when it sees EOF on the pipe, which is how it
    -- notices that nvim is gone in the cases the VimLeavePre autocmd cannot cover:
    -- SIGKILL, an OOM kill, or a crash.
    stdin = true,
    stdout = function(_, data)
      if data == nil or parsed_port ~= nil then
        return
      end
      for line in data:gmatch("[^\r\n]+") do
        local matched = line:match("Server started on port:%s+(%d+)")
        if matched ~= nil then
          parsed_port = matched
          vim.schedule(function()
            state.settings.server.port = matched
            state.go_server_running = true
            if not callback_called then
              callback_called = true
              callback()
            end
          end)
          break
        end
      end
    end,
  }, function(out)
    if out.code ~= 0 and not killed_on_exit then
      vim.schedule(function()
        local msg = "Golang gitlab server exited: code: " .. out.code .. ", signal: " .. (out.signal or 0)
        if out.stderr ~= "" then
          msg = msg .. ", msg: " .. vim.trim(out.stderr)
        end
        u.notify(msg, vim.log.levels.ERROR)
      end)
    end
  end)

  if ok then
    server_system_obj = obj
  else
    u.notify("Could not start gitlab.nvim binary: " .. tostring(obj), vim.log.levels.ERROR)
  end
end

-- Builds the Go binary with the current Git tag.
M.build = function(override)
  state.settings.root_path = u.get_root_path()

  -- If the user provided a path to the server, don't build it.
  if state.settings.server.binary_provided then
    return
  end

  local version_issue = version.check_go_version()
  if version_issue ~= nil then
    return
  end

  -- If the user did not provide a path, we build it and place it in either the data path, or the
  -- first writable path we find in the runtime.
  local datapath = vim.fn.stdpath("data")
  local runtimepath = vim.api.nvim_list_runtime_paths()
  table.insert(runtimepath, 1, datapath)

  local bin_name = u.is_windows() and "server.exe" or "server"
  local bin_folder
  for _, path in ipairs(runtimepath) do
    local ok, err = vim.loop.fs_access(path, "w")
    if err == nil and ok ~= nil and ok then
      bin_folder = path .. u.path_separator .. "gitlab.nvim" .. u.path_separator .. "bin"
      if vim.fn.mkdir(bin_folder, "p") == 1 then
        state.settings.server.binary = bin_folder .. u.path_separator .. bin_name
        break
      end
    end
  end

  if state.settings.server.binary == nil then
    u.notify("Could not find a writable folder in the runtime path to save the server to.", vim.log.levels.ERROR)
    return
  end

  if not override then
    local binary_exists = vim.loop.fs_stat(state.settings.server.binary)
    if binary_exists ~= nil then
      return
    end
  end

  local version_output = vim
    .system({ "git", "describe", "--tags", "--always" }, { cwd = state.settings.root_path })
    :wait()
  local plugin_version = version_output.code == 0 and vim.trim(version_output.stdout) or "unknown"

  local ldflags = string.format("-X main.Version=%s", plugin_version)
  local res = vim
    .system(
      { "go", "build", "-buildvcs=false", "-ldflags", ldflags, "-o", state.settings.server.binary },
      { cwd = state.settings.root_path .. u.path_separator .. "cmd" }
    )
    :wait()

  if res.code ~= 0 then
    u.notify(string.format("Failed to install with status code %d:\n%s", res.code, res.stderr), vim.log.levels.ERROR)
    return false
  end

  u.notify("Installed successfully!", vim.log.levels.INFO)
  return true
end

-- Shuts down the Go server and clears out all old gitlab.nvim state
M.shutdown = function(cb)
  if not state.go_server_running then
    vim.notify("The gitlab.nvim server is not running", vim.log.levels.ERROR)
    return
  end
  client.send_request("/shutdown", "POST", { restart = false }, function(data)
    state.go_server_running = false
    state.clear_data()
    if cb then
      cb()
    else
      u.notify(data.message, vim.log.levels.INFO)
    end
  end)
end

---Restarts the Go server and clears out all gitlab.nvim state
M.restart = function(cb)
  if not state.go_server_running then
    vim.notify("The gitlab.nvim server is not running", vim.log.levels.ERROR)
    return
  end
  client.send_request("/shutdown", "POST", { restart = true }, function(data)
    state.go_server_running = false
    M.start(function()
      state.clear_data()
      if cb then
        cb()
      else
        u.notify(data.message, vim.log.levels.INFO)
      end
    end)
  end)
end

-- Runs `callback` with the version (git tag) that was baked into the binary when it was last built.
M.get_version = function(callback)
  if not state.go_server_running then
    u.notify("Gitlab server not running", vim.log.levels.ERROR)
    return
  end
  local parent_dir = u.get_root_path()

  local version_output = vim.system({ "git", "describe", "--tags", "--always" }, { cwd = parent_dir }):wait()
  local plugin_version = version_output.code == 0 and vim.trim(version_output.stdout) or "unknown"

  -- We call the "/version" endpoint here instead of through the regular send_request
  -- pattern because earlier versions of the plugin may not have the endpoint. We handle
  -- a 404 as an "unknown" version error.
  local cmd = {
    "curl",
    "--noproxy",
    "localhost",
    "-s",
    "-X",
    "GET",
    string.format("localhost:%s/version", state.settings.server.port),
  }
  local result = vim.system(cmd):wait()
  local _, data = pcall(vim.json.decode, result.stdout)
  local binary_version = data and data.version and data.version or "unknown"

  callback({ plugin_version = plugin_version, binary_version = binary_version })
end

return M

Web Proxy Viewer  |  New URL  |  Original Page