Skip to content

Commit d2db88a

Browse files
authored
feat: auto update (#192)
Closes #170
1 parent 7b84077 commit d2db88a

File tree

7 files changed

+285
-6
lines changed

7 files changed

+285
-6
lines changed

Diff for: lib/next_ls.ex

+11-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ defmodule NextLS do
4646
{args, opts} =
4747
Keyword.split(args, [
4848
:cache,
49+
:auto_update,
4950
:task_supervisor,
5051
:runtime_task_supervisor,
5152
:dynamic_supervisor,
@@ -70,6 +71,7 @@ defmodule NextLS do
7071

7172
{:ok,
7273
assign(lsp,
74+
auto_update: Keyword.get(args, :auto_update, false),
7375
exit_code: 1,
7476
documents: %{},
7577
refresh_refs: %{},
@@ -360,6 +362,14 @@ defmodule NextLS do
360362
def handle_notification(%Initialized{}, lsp) do
361363
GenLSP.log(lsp, "[NextLS] NextLS v#{version()} has initialized!")
362364

365+
with opts when is_list(opts) <- lsp.assigns.auto_update do
366+
{:ok, _} =
367+
DynamicSupervisor.start_child(
368+
lsp.assigns.dynamic_supervisor,
369+
{NextLS.Updater, Keyword.merge(opts, logger: lsp.assigns.logger)}
370+
)
371+
end
372+
363373
for extension <- lsp.assigns.extensions do
364374
{:ok, _} =
365375
DynamicSupervisor.start_child(
@@ -664,7 +674,7 @@ defmodule NextLS do
664674
{:noreply, lsp}
665675
end
666676

667-
defp version do
677+
def version do
668678
case :application.get_key(:next_ls, :vsn) do
669679
{:ok, version} -> to_string(version)
670680
_ -> "dev"

Diff for: lib/next_ls/logger.ex

+15
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ defmodule NextLS.Logger do
1111
def info(server, msg), do: GenServer.cast(server, {:log, :info, msg})
1212
def warning(server, msg), do: GenServer.cast(server, {:log, :warning, msg})
1313

14+
def show_message(server, type, msg) when type in [:error, :warning, :info, :log] do
15+
GenServer.cast(server, {:show_message, type, msg})
16+
end
17+
1418
def init(args) do
1519
lsp = Keyword.fetch!(args, :lsp)
1620
{:ok, %{lsp: lsp}}
@@ -20,4 +24,15 @@ defmodule NextLS.Logger do
2024
apply(GenLSP, type, [state.lsp, String.trim("[NextLS] #{msg}")])
2125
{:noreply, state}
2226
end
27+
28+
def handle_cast({:show_message, type, msg}, state) do
29+
GenLSP.notify(state.lsp, %GenLSP.Notifications.WindowShowMessage{
30+
params: %GenLSP.Structures.ShowMessageParams{
31+
type: apply(GenLSP.Enumerations.MessageType, type, []),
32+
message: msg
33+
}
34+
})
35+
36+
{:noreply, state}
37+
end
2338
end

Diff for: lib/next_ls/lsp_supervisor.ex

+13
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ defmodule NextLS.LSPSupervisor do
5353
raise OptionsError, invalid
5454
end
5555

56+
auto_update =
57+
if "NEXTLS_AUTO_UPDATE" |> System.get_env("false") |> String.to_existing_atom() do
58+
[
59+
binpath: System.get_env("NEXTLS_BINPATH", Path.expand("~/.cache/elixir-tools/nextls/bin/nextls")),
60+
api_host: System.get_env("NEXTLS_GITHUB_API", "https://api.github.com"),
61+
github_host: System.get_env("NEXTLS_GITHUB", "https://github.com"),
62+
current_version: Version.parse!(NextLS.version())
63+
]
64+
else
65+
false
66+
end
67+
5668
children = [
5769
{DynamicSupervisor, name: NextLS.DynamicSupervisor},
5870
{Task.Supervisor, name: NextLS.TaskSupervisor},
@@ -61,6 +73,7 @@ defmodule NextLS.LSPSupervisor do
6173
{NextLS.DiagnosticCache, name: :diagnostic_cache},
6274
{Registry, name: NextLS.Registry, keys: :duplicate},
6375
{NextLS,
76+
auto_update: auto_update,
6477
buffer: NextLS.Buffer,
6578
cache: :diagnostic_cache,
6679
task_supervisor: NextLS.TaskSupervisor,

Diff for: lib/next_ls/updater.ex

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
defmodule NextLS.Updater do
2+
@moduledoc false
3+
use Task
4+
5+
def start_link(arg \\ []) do
6+
Task.start_link(__MODULE__, :run, [arg])
7+
end
8+
9+
def run(opts) do
10+
Logger.put_module_level(Req.Steps, :none)
11+
12+
binpath = Keyword.get(opts, :binpath, Path.expand("~/.cache/elixir-tools/nextls/bin/nextls"))
13+
api_host = Keyword.get(opts, :api_host, "https://api.github.com")
14+
github_host = Keyword.get(opts, :github_host, "https://github.com")
15+
logger = Keyword.fetch!(opts, :logger)
16+
current_version = Keyword.fetch!(opts, :current_version)
17+
retry = Keyword.get(opts, :retry, :safe)
18+
19+
case Req.get("/repos/elixir-tools/next-ls/releases/latest", base_url: api_host, retry: retry) do
20+
{:ok, %{body: %{"tag_name" => "v" <> version = tag}}} ->
21+
with {:ok, latest_version} <- Version.parse(version),
22+
:gt <- Version.compare(latest_version, current_version) do
23+
with :ok <- File.rename(binpath, binpath <> "-#{Version.to_string(current_version)}"),
24+
{:ok, _} <-
25+
File.open(binpath, [:write], fn file ->
26+
fun = fn request, finch_request, finch_name, finch_options ->
27+
fun = fn
28+
{:status, status}, response ->
29+
%{response | status: status}
30+
31+
{:headers, headers}, response ->
32+
%{response | headers: headers}
33+
34+
{:data, data}, response ->
35+
IO.binwrite(file, data)
36+
response
37+
end
38+
39+
case Finch.stream(finch_request, finch_name, Req.Response.new(), fun, finch_options) do
40+
{:ok, response} -> {request, response}
41+
{:error, exception} -> {request, exception}
42+
end
43+
end
44+
45+
with {:error, error} <-
46+
Req.get("/elixir-tools/next-ls/releases/download/#{tag}/next_ls_#{os()}_#{arch()}",
47+
finch_request: fun,
48+
base_url: github_host,
49+
retry: retry
50+
) do
51+
NextLS.Logger.show_message(logger, :error, "Failed to download version #{version} of Next LS!")
52+
NextLS.Logger.error(logger, "Failed to download Next LS: #{inspect(error)}")
53+
:error
54+
end
55+
end) do
56+
File.chmod(binpath, 0o755)
57+
58+
NextLS.Logger.show_message(
59+
logger,
60+
:info,
61+
"[Next LS] Downloaded v#{version}, please restart your editor for it to take effect."
62+
)
63+
64+
NextLS.Logger.info(logger, "Downloaded #{version} of Next LS")
65+
end
66+
end
67+
68+
{:error, error} ->
69+
NextLS.Logger.error(
70+
logger,
71+
"Failed to retrieve the latest version number of Next LS from the GitHub API: #{inspect(error)}"
72+
)
73+
end
74+
end
75+
76+
defp arch do
77+
arch_str = :erlang.system_info(:system_architecture)
78+
[arch | _] = arch_str |> List.to_string() |> String.split("-")
79+
80+
case {:os.type(), arch, :erlang.system_info(:wordsize) * 8} do
81+
{{:win32, _}, _arch, 64} -> :amd64
82+
{_os, arch, 64} when arch in ~w(arm aarch64) -> :arm64
83+
{_os, arch, 64} when arch in ~w(amd64 x86_64) -> :amd64
84+
{os, arch, _wordsize} -> raise "Unsupported system: os=#{inspect(os)}, arch=#{inspect(arch)}"
85+
end
86+
end
87+
88+
defp os do
89+
case :os.type() do
90+
{:win32, _} -> :windows
91+
{:unix, :darwin} -> :darwin
92+
{:unix, :linux} -> :linux
93+
unknown_os -> raise "Unsupported system: os=#{inspect(unknown_os)}}"
94+
end
95+
end
96+
end

Diff for: mix.exs

+8-5
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,15 @@ defmodule NextLS.MixProject do
6060
# Run "mix help deps" to learn about dependencies.
6161
defp deps do
6262
[
63-
{:gen_lsp, "~> 0.6"},
6463
{:exqlite, "~> 0.13.14"},
65-
{:styler, "~> 0.8", only: :dev},
66-
{:ex_doc, ">= 0.0.0", only: :dev},
64+
{:gen_lsp, "~> 0.6"},
65+
{:req, "~> 0.3.11"},
66+
6767
{:burrito, github: "burrito-elixir/burrito", only: [:dev, :prod]},
68-
{:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}
68+
{:bypass, "~> 2.1", only: :test},
69+
{:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false},
70+
{:ex_doc, ">= 0.0.0", only: :dev},
71+
{:styler, "~> 0.8", only: :dev}
6972
]
7073
end
7174

@@ -76,7 +79,7 @@ defmodule NextLS.MixProject do
7679
links: %{
7780
GitHub: "https://github.com/elixir-tools/next-ls",
7881
Sponsor: "https://github.com/sponsors/mhanberg",
79-
Downloads: "https://github.com/elixir-tools/next-ls/releases",
82+
Downloads: "https://github.com/elixir-tools/next-ls/releases"
8083
},
8184
files: ~w(lib LICENSE mix.exs priv README.md .formatter.exs)
8285
]

Diff for: mix.lock

+8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
%{
22
"burrito": {:git, "https://github.com/burrito-elixir/burrito.git", "68ec772f22f623d75bd1f667b1cb4c95f2935b3b", []},
3+
"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"},
34
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
45
"cc_precompiler": {:hex, :cc_precompiler, "0.1.8", "933a5f4da3b19ee56539a076076ce4d7716d64efc8db46fd066996a7e46e2bfd", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "176bdf4366956e456bf761b54ad70bc4103d0269ca9558fd7cee93d1b3f116db"},
6+
"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"},
7+
"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"},
8+
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
59
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
610
"dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
711
"earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
@@ -21,6 +25,10 @@
2125
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
2226
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
2327
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
28+
"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"},
29+
"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"},
30+
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
31+
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
2432
"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"},
2533
"schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"},
2634
"styler": {:hex, :styler, "0.8.1", "f3c0f65023e4bfbf7e7aa752d128b8475fdabfd30f96ee7314b84480cc56e788", [:mix], [], "hexpm", "1aa48d3aa689a639289af3d8254d40e068e98c083d6e5e3d1a695e71a147b344"},

Diff for: test/next_ls/updater_test.exs

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
defmodule NextLS.UpdaterTest do
2+
use ExUnit.Case, async: true
3+
4+
alias NextLS.Updater
5+
6+
@moduletag :tmp_dir
7+
8+
setup do
9+
me = self()
10+
11+
{:ok, logger} =
12+
Task.start_link(fn ->
13+
recv = fn recv ->
14+
receive do
15+
{:"$gen_cast", msg} ->
16+
# dbg(msg)
17+
send(me, msg)
18+
end
19+
20+
recv.(recv)
21+
end
22+
23+
recv.(recv)
24+
end)
25+
26+
[logger: logger]
27+
end
28+
29+
test "downloads the exe", %{tmp_dir: tmp_dir, logger: logger} do
30+
api = Bypass.open(port: 8000)
31+
github = Bypass.open(port: 8001)
32+
33+
Bypass.expect(api, "GET", "/repos/elixir-tools/next-ls/releases/latest", fn conn ->
34+
conn
35+
|> Plug.Conn.put_resp_header("content-type", "application/json")
36+
|> Plug.Conn.resp(200, Jason.encode!(%{tag_name: "v1.0.0"}))
37+
end)
38+
39+
exe = String.duplicate("time to hack\n", 1000)
40+
41+
Bypass.expect(github, fn conn ->
42+
assert "GET" == conn.method
43+
assert "/elixir-tools/next-ls/releases/download/v1.0.0/next_ls_" <> rest = conn.request_path
44+
45+
assert rest in [
46+
"darwin_arm64",
47+
"darwin_amd64",
48+
"linux_arm64",
49+
"linux_amd64",
50+
"windows_amd64"
51+
]
52+
53+
Plug.Conn.resp(conn, 200, exe)
54+
end)
55+
56+
binpath = Path.join(tmp_dir, "nextls")
57+
File.write(binpath, "yoyoyo")
58+
59+
Updater.run(
60+
current_version: Version.parse!("0.9.0"),
61+
binpath: binpath,
62+
api_host: "http://localhost:8000",
63+
github_host: "http://localhost:8001",
64+
logger: logger
65+
)
66+
67+
assert File.read!(binpath) == exe
68+
assert File.stat!(binpath).mode == 33_261
69+
assert File.exists?(binpath <> "-0.9.0")
70+
end
71+
72+
test "doesn't download when the version is at the latest", %{tmp_dir: tmp_dir, logger: logger} do
73+
api = Bypass.open(port: 8000)
74+
75+
Bypass.expect(api, "GET", "/repos/elixir-tools/next-ls/releases/latest", fn conn ->
76+
conn
77+
|> Plug.Conn.put_resp_header("content-type", "application/json")
78+
|> Plug.Conn.resp(200, Jason.encode!(%{tag_name: "v1.0.0"}))
79+
end)
80+
81+
binpath = Path.join(tmp_dir, "nextls")
82+
83+
Updater.run(
84+
current_version: Version.parse!("1.0.0"),
85+
binpath: binpath,
86+
api_host: "http://localhost:8000",
87+
github_host: "http://localhost:8001",
88+
logger: logger
89+
)
90+
91+
refute File.exists?(binpath)
92+
end
93+
94+
test "logs that it failed when api call fails", %{tmp_dir: tmp_dir, logger: logger} do
95+
binpath = Path.join(tmp_dir, "nextls")
96+
File.write(binpath, "yoyoyo")
97+
98+
Updater.run(
99+
current_version: Version.parse!("1.0.0"),
100+
binpath: binpath,
101+
api_host: "http://localhost:8000",
102+
github_host: "http://localhost:8001",
103+
logger: logger,
104+
retry: false
105+
)
106+
107+
assert_receive {:log, :error, "Failed to retrieve the latest version number of Next LS from the GitHub API: " <> _}
108+
end
109+
110+
test "logs that it failed when download fails", %{tmp_dir: tmp_dir, logger: logger} do
111+
api = Bypass.open(port: 8000)
112+
113+
Bypass.expect(api, "GET", "/repos/elixir-tools/next-ls/releases/latest", fn conn ->
114+
conn
115+
|> Plug.Conn.put_resp_header("content-type", "application/json")
116+
|> Plug.Conn.resp(200, Jason.encode!(%{tag_name: "v1.0.0"}))
117+
end)
118+
119+
binpath = Path.join(tmp_dir, "nextls")
120+
File.write(binpath, "yoyoyo")
121+
122+
Updater.run(
123+
current_version: Version.parse!("0.9.0"),
124+
binpath: binpath,
125+
api_host: "http://localhost:8000",
126+
github_host: "http://localhost:8001",
127+
logger: logger,
128+
retry: false
129+
)
130+
131+
assert_receive {:show_message, :error, "Failed to download version 1.0.0 of Next LS!"}
132+
assert_receive {:log, :error, "Failed to download Next LS: " <> _}
133+
end
134+
end

0 commit comments

Comments
 (0)