Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show information when a new Livebook version is available #1121

Merged
merged 11 commits into from
Apr 17, 2022
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/livebook.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
4 changes: 3 additions & 1 deletion lib/livebook/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ defmodule Livebook.Application do
{Phoenix.PubSub, name: Livebook.PubSub},
# 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},
Expand Down
15 changes: 15 additions & 0 deletions lib/livebook/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down Expand Up @@ -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.
"""
Expand Down
21 changes: 20 additions & 1 deletion lib/livebook/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
146 changes: 146 additions & 0 deletions lib/livebook/update_check.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved

@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
send(self(), :check)

{:ok,
%{
enabled: Livebook.Settings.update_check_enabled?(),
new_version: nil,
timer_ref: nil
}}
end

@impl true
def handle_cast({:set_enabled, enabled}, state) do
Livebook.Settings.set_update_check_enabled(enabled)

state = cancel_timer(state)

if enabled do
send(self(), :check)
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
end

{:noreply, %{state | enabled: enabled}}
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
state =
if state.enabled do
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
case fetch_latest_version() do
{:ok, version} ->
new_version = if newer?(version), do: version
timer_ref = Process.send_after(self(), :check, @day_in_ms)
%{state | new_version: new_version, timer_ref: timer_ref}

{:error, error} ->
Logger.error("version check failed, #{error}")
timer_ref = Process.send_after(self(), :check, @hour_in_ms)
%{state | timer_ref: timer_ref}
end
else
state
end

{:noreply, state}
end

defp cancel_timer(%{timer_ref: nil} = state), do: state

defp cancel_timer(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
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
{:ok, status, _headers, body} ->
with 200 <- status,
{:ok, data} <- Jason.decode(body),
%{"tag_name" => "v" <> version} <- data do
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
{: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
49 changes: 37 additions & 12 deletions lib/livebook_web/live/home_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)}
Expand All @@ -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">
<.memory_notification memory={@memory} app_service_url={@app_service_url} />
<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">
Expand Down Expand Up @@ -157,18 +160,40 @@ 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! 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>
<%= if @instructions_url do %>
and follow the
<a class="font-medium border-b border-gray-900 hover:border-transparent" href={@instructions_url} target="_blank">
update instructions
</a>
<% end %>
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
🚀
</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">
Less than 30 MB of memory left, consider adding more resources to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't want to keep the icon? :D It looked cool. Your call!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

<a class="font-medium border-b border-gray-900 hover:border-transparent" href={@app_service_url} target="_blank">
the instance
</a>
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
or closing
<a class="font-medium border-b border-gray-900 hover:border-transparent" href="#running-sessions">
running sessions
</a>.
</div>
<% end %>
"""
Expand Down
27 changes: 22 additions & 5 deletions lib/livebook_web/live/settings_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 %>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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)}
Expand Down