mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-12 16:04:39 +08:00
171 lines
4.3 KiB
Elixir
171 lines
4.3 KiB
Elixir
defmodule Livebook.UpdateCheck do
|
|
# Periodically checks for available Livebook update.
|
|
|
|
use GenServer
|
|
|
|
require Logger
|
|
|
|
@name __MODULE__
|
|
@timeout :infinity
|
|
|
|
@hour_in_ms 60 * 60 * 1000
|
|
@day_in_ms 24 * @hour_in_ms
|
|
|
|
@doc false
|
|
def start_link(_opts) do
|
|
GenServer.start_link(__MODULE__, {}, name: @name)
|
|
end
|
|
|
|
@doc """
|
|
Returns the latest Livebook version if it's more recent than the
|
|
current one.
|
|
"""
|
|
@spec new_version() :: String.t() | nil
|
|
def new_version() do
|
|
GenServer.call(@name, :get_new_version, @timeout)
|
|
end
|
|
|
|
@doc """
|
|
Returns whether the update check is enabled.
|
|
"""
|
|
@spec enabled?() :: boolean()
|
|
def enabled?() do
|
|
GenServer.call(@name, :get_enabled, @timeout)
|
|
end
|
|
|
|
@doc """
|
|
Sets whether the update check is enabled.
|
|
"""
|
|
@spec set_enabled(boolean()) :: :ok
|
|
def set_enabled(enabled) do
|
|
GenServer.cast(@name, {:set_enabled, enabled})
|
|
end
|
|
|
|
@impl true
|
|
def init({}) do
|
|
state = %{
|
|
enabled: Livebook.Settings.update_check_enabled?(),
|
|
new_version: nil,
|
|
timer_ref: nil,
|
|
request_ref: nil
|
|
}
|
|
|
|
{:ok, schedule_check(state, 0)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast({:set_enabled, enabled}, state) do
|
|
Livebook.Settings.set_update_check_enabled(enabled)
|
|
state = %{state | enabled: enabled}
|
|
state = state |> cancel_check() |> schedule_check(0)
|
|
{:noreply, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:get_enabled, _from, state) do
|
|
{:reply, state.enabled, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:get_new_version, _from, state) do
|
|
new_version = if state.enabled, do: state.new_version
|
|
{:reply, new_version, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(:check, state) do
|
|
task = Task.Supervisor.async_nolink(Livebook.TaskSupervisor, &fetch_latest_version/0)
|
|
{:noreply, %{state | request_ref: task.ref}}
|
|
end
|
|
|
|
def handle_info({ref, response}, %{request_ref: ref} = state) do
|
|
Process.demonitor(ref, [:flush])
|
|
|
|
state =
|
|
case response do
|
|
{:ok, release} ->
|
|
state = %{state | new_version: new_version(release)}
|
|
schedule_check(state, @day_in_ms)
|
|
|
|
{:error, error} ->
|
|
Logger.warning("version check failed, #{error}")
|
|
schedule_check(state, @hour_in_ms)
|
|
end
|
|
|
|
{:noreply, %{state | request_ref: nil}}
|
|
end
|
|
|
|
def handle_info({:DOWN, ref, :process, _pid, reason}, %{request_ref: ref} = state) do
|
|
Logger.warning("version check failed, reason: #{inspect(reason)}")
|
|
{:noreply, %{state | request_ref: nil} |> schedule_check(@hour_in_ms)}
|
|
end
|
|
|
|
def handle_info(_msg, state), do: {:noreply, state}
|
|
|
|
defp schedule_check(%{enabled: false} = state, _time), do: state
|
|
|
|
defp schedule_check(state, time) do
|
|
timer_ref = Process.send_after(self(), :check, time)
|
|
%{state | timer_ref: timer_ref}
|
|
end
|
|
|
|
defp cancel_check(%{timer_ref: nil} = state), do: state
|
|
|
|
defp cancel_check(state) do
|
|
if Process.cancel_timer(state.timer_ref) == false do
|
|
receive do
|
|
:check -> :ok
|
|
end
|
|
end
|
|
|
|
%{state | timer_ref: nil}
|
|
end
|
|
|
|
defp fetch_latest_version() do
|
|
url = "https://api.github.com/repos/livebook-dev/livebook/releases/latest"
|
|
headers = [{"accept", "application/vnd.github.v3+json"}]
|
|
|
|
case Livebook.Utils.HTTP.request(:get, url, headers: headers) do
|
|
{:ok, status, _headers, body} ->
|
|
with 200 <- status,
|
|
{:ok, release} <- Jason.decode(body) do
|
|
{:ok, release}
|
|
else
|
|
_ -> {:error, "unexpected response"}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, "failed to make a request, reason: #{inspect(reason)}"}
|
|
end
|
|
end
|
|
|
|
defp new_version(release) do
|
|
current_version = Livebook.Config.app_version()
|
|
|
|
with %{
|
|
"tag_name" => "v" <> version,
|
|
"published_at" => published_at,
|
|
"draft" => false
|
|
} <- release,
|
|
{:ok, published_at} <- NaiveDateTime.from_iso8601(published_at),
|
|
true <- at_least_one_day_ago?(published_at) and stable?(version),
|
|
:lt <- Version.compare(current_version, version) do
|
|
version
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
@one_day_in_seconds 60 * 60 * 24
|
|
|
|
defp at_least_one_day_ago?(naive_datetime) do
|
|
NaiveDateTime.diff(NaiveDateTime.utc_now(), naive_datetime) > @one_day_in_seconds
|
|
end
|
|
|
|
defp stable?(version) do
|
|
case Version.parse(version) do
|
|
{:ok, %{pre: []}} -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
end
|