Skip to content

Commit

Permalink
feat: auto update (#192)
Browse files Browse the repository at this point in the history
Closes #170
  • Loading branch information
mhanberg committed Aug 20, 2023
1 parent 7b84077 commit d2db88a
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 6 deletions.
12 changes: 11 additions & 1 deletion lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ defmodule NextLS do
{args, opts} =
Keyword.split(args, [
:cache,
:auto_update,
:task_supervisor,
:runtime_task_supervisor,
:dynamic_supervisor,
Expand All @@ -70,6 +71,7 @@ defmodule NextLS do

{:ok,
assign(lsp,
auto_update: Keyword.get(args, :auto_update, false),
exit_code: 1,
documents: %{},
refresh_refs: %{},
Expand Down Expand Up @@ -360,6 +362,14 @@ defmodule NextLS do
def handle_notification(%Initialized{}, lsp) do
GenLSP.log(lsp, "[NextLS] NextLS v#{version()} has initialized!")

with opts when is_list(opts) <- lsp.assigns.auto_update do
{:ok, _} =
DynamicSupervisor.start_child(
lsp.assigns.dynamic_supervisor,
{NextLS.Updater, Keyword.merge(opts, logger: lsp.assigns.logger)}
)
end

for extension <- lsp.assigns.extensions do
{:ok, _} =
DynamicSupervisor.start_child(
Expand Down Expand Up @@ -664,7 +674,7 @@ defmodule NextLS do
{:noreply, lsp}
end

defp version do
def version do
case :application.get_key(:next_ls, :vsn) do
{:ok, version} -> to_string(version)
_ -> "dev"
Expand Down
15 changes: 15 additions & 0 deletions lib/next_ls/logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ defmodule NextLS.Logger do
def info(server, msg), do: GenServer.cast(server, {:log, :info, msg})
def warning(server, msg), do: GenServer.cast(server, {:log, :warning, msg})

def show_message(server, type, msg) when type in [:error, :warning, :info, :log] do
GenServer.cast(server, {:show_message, type, msg})
end

def init(args) do
lsp = Keyword.fetch!(args, :lsp)
{:ok, %{lsp: lsp}}
Expand All @@ -20,4 +24,15 @@ defmodule NextLS.Logger do
apply(GenLSP, type, [state.lsp, String.trim("[NextLS] #{msg}")])
{:noreply, state}
end

def handle_cast({:show_message, type, msg}, state) do
GenLSP.notify(state.lsp, %GenLSP.Notifications.WindowShowMessage{
params: %GenLSP.Structures.ShowMessageParams{
type: apply(GenLSP.Enumerations.MessageType, type, []),
message: msg
}
})

{:noreply, state}
end
end
13 changes: 13 additions & 0 deletions lib/next_ls/lsp_supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ defmodule NextLS.LSPSupervisor do
raise OptionsError, invalid
end

auto_update =
if "NEXTLS_AUTO_UPDATE" |> System.get_env("false") |> String.to_existing_atom() do
[
binpath: System.get_env("NEXTLS_BINPATH", Path.expand("~/.cache/elixir-tools/nextls/bin/nextls")),
api_host: System.get_env("NEXTLS_GITHUB_API", "https://api.github.com"),
github_host: System.get_env("NEXTLS_GITHUB", "https://github.com"),
current_version: Version.parse!(NextLS.version())
]
else
false
end

children = [
{DynamicSupervisor, name: NextLS.DynamicSupervisor},
{Task.Supervisor, name: NextLS.TaskSupervisor},
Expand All @@ -61,6 +73,7 @@ defmodule NextLS.LSPSupervisor do
{NextLS.DiagnosticCache, name: :diagnostic_cache},
{Registry, name: NextLS.Registry, keys: :duplicate},
{NextLS,
auto_update: auto_update,
buffer: NextLS.Buffer,
cache: :diagnostic_cache,
task_supervisor: NextLS.TaskSupervisor,
Expand Down
96 changes: 96 additions & 0 deletions lib/next_ls/updater.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule NextLS.Updater do
@moduledoc false
use Task

def start_link(arg \\ []) do
Task.start_link(__MODULE__, :run, [arg])
end

def run(opts) do
Logger.put_module_level(Req.Steps, :none)

binpath = Keyword.get(opts, :binpath, Path.expand("~/.cache/elixir-tools/nextls/bin/nextls"))
api_host = Keyword.get(opts, :api_host, "https://api.github.com")
github_host = Keyword.get(opts, :github_host, "https://github.com")
logger = Keyword.fetch!(opts, :logger)
current_version = Keyword.fetch!(opts, :current_version)
retry = Keyword.get(opts, :retry, :safe)

case Req.get("/repos/elixir-tools/next-ls/releases/latest", base_url: api_host, retry: retry) do
{:ok, %{body: %{"tag_name" => "v" <> version = tag}}} ->
with {:ok, latest_version} <- Version.parse(version),
:gt <- Version.compare(latest_version, current_version) do
with :ok <- File.rename(binpath, binpath <> "-#{Version.to_string(current_version)}"),
{:ok, _} <-
File.open(binpath, [:write], fn file ->
fun = fn request, finch_request, finch_name, finch_options ->
fun = fn
{:status, status}, response ->
%{response | status: status}

{:headers, headers}, response ->
%{response | headers: headers}

{:data, data}, response ->
IO.binwrite(file, data)
response
end

case Finch.stream(finch_request, finch_name, Req.Response.new(), fun, finch_options) do
{:ok, response} -> {request, response}
{:error, exception} -> {request, exception}
end
end

with {:error, error} <-
Req.get("/elixir-tools/next-ls/releases/download/#{tag}/next_ls_#{os()}_#{arch()}",
finch_request: fun,
base_url: github_host,
retry: retry
) do
NextLS.Logger.show_message(logger, :error, "Failed to download version #{version} of Next LS!")
NextLS.Logger.error(logger, "Failed to download Next LS: #{inspect(error)}")
:error
end
end) do
File.chmod(binpath, 0o755)

NextLS.Logger.show_message(
logger,
:info,
"[Next LS] Downloaded v#{version}, please restart your editor for it to take effect."
)

NextLS.Logger.info(logger, "Downloaded #{version} of Next LS")
end
end

{:error, error} ->
NextLS.Logger.error(
logger,
"Failed to retrieve the latest version number of Next LS from the GitHub API: #{inspect(error)}"
)
end
end

defp arch do
arch_str = :erlang.system_info(:system_architecture)
[arch | _] = arch_str |> List.to_string() |> String.split("-")

case {:os.type(), arch, :erlang.system_info(:wordsize) * 8} do
{{:win32, _}, _arch, 64} -> :amd64
{_os, arch, 64} when arch in ~w(arm aarch64) -> :arm64
{_os, arch, 64} when arch in ~w(amd64 x86_64) -> :amd64
{os, arch, _wordsize} -> raise "Unsupported system: os=#{inspect(os)}, arch=#{inspect(arch)}"
end
end

defp os do
case :os.type() do
{:win32, _} -> :windows
{:unix, :darwin} -> :darwin
{:unix, :linux} -> :linux
unknown_os -> raise "Unsupported system: os=#{inspect(unknown_os)}}"
end
end
end
13 changes: 8 additions & 5 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,15 @@ defmodule NextLS.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:gen_lsp, "~> 0.6"},
{:exqlite, "~> 0.13.14"},
{:styler, "~> 0.8", only: :dev},
{:ex_doc, ">= 0.0.0", only: :dev},
{:gen_lsp, "~> 0.6"},
{:req, "~> 0.3.11"},

{:burrito, github: "burrito-elixir/burrito", only: [:dev, :prod]},
{:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}
{:bypass, "~> 2.1", only: :test},
{:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev},
{:styler, "~> 0.8", only: :dev}
]
end

Expand All @@ -76,7 +79,7 @@ defmodule NextLS.MixProject do
links: %{
GitHub: "https://github.com/elixir-tools/next-ls",
Sponsor: "https://github.com/sponsors/mhanberg",
Downloads: "https://github.com/elixir-tools/next-ls/releases",
Downloads: "https://github.com/elixir-tools/next-ls/releases"
},
files: ~w(lib LICENSE mix.exs priv README.md .formatter.exs)
]
Expand Down
8 changes: 8 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
%{
"burrito": {:git, "https://github.com/burrito-elixir/burrito.git", "68ec772f22f623d75bd1f667b1cb4c95f2935b3b", []},
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.8", "933a5f4da3b19ee56539a076076ce4d7716d64efc8db46fd066996a7e46e2bfd", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "176bdf4366956e456bf761b54ad70bc4103d0269ca9558fd7cee93d1b3f116db"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
"dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
"earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
Expand All @@ -21,6 +25,10 @@
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"req": {:hex, :req, "0.3.11", "462315e50db6c6e1f61c45e8c0b267b0d22b6bd1f28444c136908dfdca8d515a", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0e4b331627fedcf90b29aa8064cd5a95619ef6134d5ab13919b6e1c4d7cccd4b"},
"schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"},
"styler": {:hex, :styler, "0.8.1", "f3c0f65023e4bfbf7e7aa752d128b8475fdabfd30f96ee7314b84480cc56e788", [:mix], [], "hexpm", "1aa48d3aa689a639289af3d8254d40e068e98c083d6e5e3d1a695e71a147b344"},
Expand Down
134 changes: 134 additions & 0 deletions test/next_ls/updater_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
defmodule NextLS.UpdaterTest do
use ExUnit.Case, async: true

alias NextLS.Updater

@moduletag :tmp_dir

setup do
me = self()

{:ok, logger} =
Task.start_link(fn ->
recv = fn recv ->
receive do
{:"$gen_cast", msg} ->
# dbg(msg)
send(me, msg)
end

recv.(recv)
end

recv.(recv)
end)

[logger: logger]
end

test "downloads the exe", %{tmp_dir: tmp_dir, logger: logger} do
api = Bypass.open(port: 8000)
github = Bypass.open(port: 8001)

Bypass.expect(api, "GET", "/repos/elixir-tools/next-ls/releases/latest", fn conn ->
conn
|> Plug.Conn.put_resp_header("content-type", "application/json")
|> Plug.Conn.resp(200, Jason.encode!(%{tag_name: "v1.0.0"}))
end)

exe = String.duplicate("time to hack\n", 1000)

Bypass.expect(github, fn conn ->
assert "GET" == conn.method
assert "/elixir-tools/next-ls/releases/download/v1.0.0/next_ls_" <> rest = conn.request_path

assert rest in [
"darwin_arm64",
"darwin_amd64",
"linux_arm64",
"linux_amd64",
"windows_amd64"
]

Plug.Conn.resp(conn, 200, exe)
end)

binpath = Path.join(tmp_dir, "nextls")
File.write(binpath, "yoyoyo")

Updater.run(
current_version: Version.parse!("0.9.0"),
binpath: binpath,
api_host: "http://localhost:8000",
github_host: "http://localhost:8001",
logger: logger
)

assert File.read!(binpath) == exe
assert File.stat!(binpath).mode == 33_261
assert File.exists?(binpath <> "-0.9.0")
end

test "doesn't download when the version is at the latest", %{tmp_dir: tmp_dir, logger: logger} do
api = Bypass.open(port: 8000)

Bypass.expect(api, "GET", "/repos/elixir-tools/next-ls/releases/latest", fn conn ->
conn
|> Plug.Conn.put_resp_header("content-type", "application/json")
|> Plug.Conn.resp(200, Jason.encode!(%{tag_name: "v1.0.0"}))
end)

binpath = Path.join(tmp_dir, "nextls")

Updater.run(
current_version: Version.parse!("1.0.0"),
binpath: binpath,
api_host: "http://localhost:8000",
github_host: "http://localhost:8001",
logger: logger
)

refute File.exists?(binpath)
end

test "logs that it failed when api call fails", %{tmp_dir: tmp_dir, logger: logger} do
binpath = Path.join(tmp_dir, "nextls")
File.write(binpath, "yoyoyo")

Updater.run(
current_version: Version.parse!("1.0.0"),
binpath: binpath,
api_host: "http://localhost:8000",
github_host: "http://localhost:8001",
logger: logger,
retry: false
)

assert_receive {:log, :error, "Failed to retrieve the latest version number of Next LS from the GitHub API: " <> _}
end

test "logs that it failed when download fails", %{tmp_dir: tmp_dir, logger: logger} do
api = Bypass.open(port: 8000)

Bypass.expect(api, "GET", "/repos/elixir-tools/next-ls/releases/latest", fn conn ->
conn
|> Plug.Conn.put_resp_header("content-type", "application/json")
|> Plug.Conn.resp(200, Jason.encode!(%{tag_name: "v1.0.0"}))
end)

binpath = Path.join(tmp_dir, "nextls")
File.write(binpath, "yoyoyo")

Updater.run(
current_version: Version.parse!("0.9.0"),
binpath: binpath,
api_host: "http://localhost:8000",
github_host: "http://localhost:8001",
logger: logger,
retry: false
)

assert_receive {:show_message, :error, "Failed to download version 1.0.0 of Next LS!"}
assert_receive {:log, :error, "Failed to download Next LS: " <> _}
end
end

0 comments on commit d2db88a

Please sign in to comment.