Show information when a new Livebook version is available (#1121)

* Show information when a new Livebook version is available

* Wording

* Use more precise version comparison

* Update lib/livebook/application.ex

Co-authored-by: José Valim <jose.valim@dashbit.co>

* Up

* Update notificaion styles

* Update lib/livebook_web/live/home_live.ex

Co-authored-by: José Valim <jose.valim@dashbit.co>

* Update lib/livebook_web/live/home_live.ex

Co-authored-by: José Valim <jose.valim@dashbit.co>

* Apply review comments

* Use async_nolink

Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
Jonatan Kłosko 2022-04-18 00:55:08 +02:00 committed by GitHub
parent 78b9a11d8c
commit 134fbe0589
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 265 additions and 21 deletions

View file

@ -196,6 +196,9 @@ The following environment variables configure Livebook:
Enabled by default unless `LIVEBOOK_PASSWORD` is set. Set it to "false" to
disable it.
* LIVEBOOK_UPDATE_INSTRUCTIONS_URL - sets the URL to direct the user to for
updating Livebook when a new version becomes available.
<!-- Environment variables -->
If running Livebook as a Docker image or an Elixir release, [the environment

View file

@ -155,6 +155,11 @@ defmodule Livebook do
:app_service_url,
Livebook.Config.app_service_url!("LIVEBOOK_APP_SERVICE_URL")
end
if update_instructions_url =
Livebook.Config.update_instructions_url!("LIVEBOOK_UPDATE_INSTRUCTIONS_URL") do
config :livebook, :update_instructions_url, update_instructions_url
end
end
@doc """

View file

@ -18,16 +18,18 @@ defmodule Livebook.Application do
LivebookWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Livebook.PubSub},
# Start a supervisor for Livebook tasks
{Task.Supervisor, name: Livebook.TaskSupervisor},
# Start the storage module
Livebook.Storage.current(),
# Periodid measurement of system resources
# Start the periodic version check
Livebook.UpdateCheck,
# Periodic measurement of system resources
Livebook.SystemResources,
# Start the tracker server on this node
{Livebook.Tracker, pubsub_server: Livebook.PubSub},
# Start the supervisor dynamically managing sessions
{DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one},
# Start a supervisor for Livebook tasks
{Task.Supervisor, name: Livebook.TaskSupervisor},
# Start the server responsible for associating files with sessions
Livebook.Session.FileGuard,
# Start the Node Pool for managing node names

View file

@ -122,6 +122,14 @@ defmodule Livebook.Config do
Application.fetch_env!(:livebook, :app_service_url)
end
@doc """
Returns the update check URL.
"""
@spec update_instructions_url() :: String.t() | nil
def update_instructions_url() do
Application.get_env(:livebook, :update_instructions_url)
end
## Parsing
@doc """
@ -254,6 +262,13 @@ defmodule Livebook.Config do
System.get_env(env)
end
@doc """
Parses update instructions url from env.
"""
def update_instructions_url!(env) do
System.get_env(env)
end
@doc """
Parses and validates default runtime from env.
"""

View file

@ -13,7 +13,7 @@ defmodule Livebook.Settings do
@doc """
Returns the current autosave path.
"""
@spec autosave_path() :: String.t() | nil
@spec autosave_path() :: String.t()
def autosave_path() do
case storage().fetch_key(:settings, "global", :autosave_path) do
{:ok, value} -> value
@ -98,4 +98,23 @@ defmodule Livebook.Settings do
{:error, message} -> raise ArgumentError, "invalid S3 filesystem: #{message}"
end
end
@doc """
Returns whether the update check is enabled.
"""
@spec update_check_enabled?() :: boolean()
def update_check_enabled?() do
case storage().fetch_key(:settings, "global", :update_check_enabled) do
{:ok, value} -> value
:error -> true
end
end
@doc """
Sets whether the update check is enabled.
"""
@spec set_update_check_enabled(boolean()) :: :ok
def set_update_check_enabled(enabled) do
storage().insert(:settings, "global", update_check_enabled: enabled)
end
end

View file

@ -0,0 +1,157 @@
defmodule Livebook.UpdateCheck do
@moduledoc false
# 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, version} ->
new_version = if newer?(version), do: version
state = %{state | new_version: new_version}
schedule_check(state, @day_in_ms)
{:error, error} ->
Logger.error("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.error("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, data} <- Jason.decode(body),
%{"tag_name" => "v" <> version} <- data do
{:ok, version}
else
_ -> {:error, "unexpected response"}
end
{:error, reason} ->
{:error, "failed to make a request, reason: #{inspect(reason)}"}
end
end
defp newer?(version) do
current_version = Application.spec(:livebook, :vsn) |> List.to_string()
stable?(version) and Version.compare(current_version, version) == :lt
end
defp stable?(version) do
case Version.parse(version) do
{:ok, %{pre: []}} -> true
_ -> false
end
end
end

View file

@ -26,6 +26,8 @@ defmodule LivebookWeb.HomeLive do
sessions: sessions,
notebook_infos: notebook_infos,
page_title: "Livebook",
new_version: Livebook.UpdateCheck.new_version(),
update_instructions_url: Livebook.Config.update_instructions_url(),
app_service_url: Livebook.Config.app_service_url(),
memory: Livebook.SystemResources.memory()
)}
@ -39,9 +41,10 @@ defmodule LivebookWeb.HomeLive do
<SidebarHelpers.sidebar>
<SidebarHelpers.shared_home_footer socket={@socket} current_user={@current_user} />
</SidebarHelpers.sidebar>
<div class="grow px-6 py-8 overflow-y-auto">
<div class="max-w-screen-lg w-full mx-auto px-4 pb-8 space-y-4">
<div class="grow overflow-y-auto">
<.update_notification version={@new_version} instructions_url={@update_instructions_url} />
<.memory_notification memory={@memory} app_service_url={@app_service_url} />
<div class="max-w-screen-lg w-full mx-auto px-8 pt-8 pb-32 space-y-4">
<div class="flex flex-col space-y-2 items-center pb-4 border-b border-gray-200
sm:flex-row sm:space-y-0 sm:justify-between">
<div class="text-2xl text-gray-800 font-semibold">
@ -157,18 +160,41 @@ defmodule LivebookWeb.HomeLive do
end
end
defp update_notification(%{version: nil} = assigns), do: ~H""
defp update_notification(assigns) do
~H"""
<div class="px-2 py-2 bg-blue-200 text-gray-900 text-sm text-center">
<span>
Livebook v<%= @version %> available!
<%= if @instructions_url do %>
Check out the news on
<a class="font-medium border-b border-gray-900 hover:border-transparent" href="https://livebook.dev/" target="_blank">
livebook.dev
</a>
and follow the
<a class="font-medium border-b border-gray-900 hover:border-transparent" href={@instructions_url} target="_blank">
update instructions
</a>
<% else %>
Check out the news and installation steps on
<a class="font-medium border-b border-gray-900 hover:border-transparent" href="https://livebook.dev/" target="_blank">livebook.dev</a>
<% end %>
🚀
</span>
</div>
"""
end
defp memory_notification(assigns) do
~H"""
<%= if @app_service_url && @memory.free < 30_000_000 do %>
<div class="flex justify-between items-center border-b border-gray-200 pb-4 text-gray-700">
<span class="flex items-end">
<.remix_icon icon="alarm-warning-line" class="text-xl mr-2" />
<span>
Less than 30 MB of memory left, consider adding more resources to
<a class="font-semibold" href={@app_service_url} target="_blank">the instance</a>
or closing <a class="font-semibold" href="#running-sessions">running sessions</a>.
</span>
</span>
<div class="px-2 py-2 bg-red-200 text-gray-900 text-sm text-center">
<.remix_icon icon="alarm-warning-line" class="align-text-bottom mr-0.5" />
Less than 30 MB of memory left, consider
<a class="font-medium border-b border-gray-900 hover:border-transparent" href={@app_service_url} target="_blank">adding more resources to the instance</a>
or closing
<a class="font-medium border-b border-gray-900 hover:border-transparent" href="#running-sessions">running sessions</a>
</div>
<% end %>
"""

View file

@ -7,17 +7,16 @@ defmodule LivebookWeb.SettingsLive do
@impl true
def mount(_params, _session, socket) do
file_systems = Livebook.Settings.file_systems()
{:ok,
socket
|> SidebarHelpers.shared_home_handlers()
|> assign(
file_systems: file_systems,
file_systems: Livebook.Settings.file_systems(),
autosave_path_state: %{
file: autosave_dir(),
dialog_opened?: false
},
update_check_enabled: Livebook.UpdateCheck.enabled?(),
page_title: "Livebook - Settings"
)}
end
@ -45,9 +44,9 @@ defmodule LivebookWeb.SettingsLive do
<!-- System details -->
<div class="flex flex-col space-y-4">
<h1 class="text-xl text-gray-800 font-semibold">
<h2 class="text-xl text-gray-800 font-semibold">
About
</h1>
</h2>
<div class="flex items-center justify-between border border-gray-200 rounded-lg p-4">
<div class="flex items-center space-x-12">
<%= if app_name = Livebook.Config.app_service_name() do %>
@ -76,6 +75,18 @@ defmodule LivebookWeb.SettingsLive do
<% end %>
</div>
</div>
<!-- Preferences -->
<div class="flex flex-col space-y-4">
<h2 class="text-xl text-gray-800 font-semibold">
Preferences
</h2>
<form phx-change="save" onsubmit="return false;">
<.switch_checkbox
name="update_check_enabled"
label="Show available Livebook updates"
checked={@update_check_enabled} />
</form>
</div>
<!-- Autosave path configuration -->
<div class="flex flex-col space-y-4">
<div>
@ -253,6 +264,12 @@ defmodule LivebookWeb.SettingsLive do
{:noreply, assign(socket, file_systems: file_systems)}
end
def handle_event("save", %{"update_check_enabled" => enabled}, socket) do
enabled = enabled == "true"
Livebook.UpdateCheck.set_enabled(enabled)
{:noreply, assign(socket, :update_check_enabled, enabled)}
end
@impl true
def handle_info({:file_systems_updated, file_systems}, socket) do
{:noreply, assign(socket, file_systems: file_systems)}