Packages
livebook
0.18.6
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/apps.ex
defmodule Livebook.Apps do
# Top-level module for keeping track of apps.
#
# App processes are tracked using `Livebook.Tracker` in the same way
# that sessions are.
require Logger
alias Livebook.App
alias Livebook.Apps
@doc """
Returns app process pid for the given slug.
"""
@spec fetch_pid(App.slug()) :: {:ok, pid()} | :error
def fetch_pid(slug) do
case :global.whereis_name(global_name(slug)) do
:undefined -> :error
pid -> {:ok, pid}
end
end
@doc """
Returns app info for the given slug.
"""
@spec fetch_app(App.slug()) :: {:ok, App.t()} | :error
def fetch_app(slug) do
with {:ok, pid} <- fetch_pid(slug) do
{:ok, App.get_by_pid(pid)}
end
end
@doc """
Checks if app with the given slug exists.
"""
@spec exists?(App.slug()) :: boolean()
def exists?(slug) do
:global.whereis_name(global_name(slug)) != :undefined
end
@doc """
Looks up app with the given slug and returns its settings.
"""
@spec fetch_settings(App.slug()) :: {:ok, Livebook.Notebook.AppSettings.t()} | :error
def fetch_settings(slug) do
with {:ok, pid} <- fetch_pid(slug) do
app_settings = App.get_settings(pid)
{:ok, app_settings}
end
end
@doc """
Returns a global registration name for the given app slug.
"""
@spec global_name(App.slug()) :: term()
def global_name(slug), do: {:app, slug}
@doc """
Returns all the running apps.
"""
@spec list_apps() :: list(App.t())
def list_apps() do
Livebook.Tracker.list_apps()
end
@doc """
Returns all the running apps authorized to given user.
"""
@spec list_authorized_apps(Livebook.Users.User.t()) :: list(App.t())
def list_authorized_apps(user) do
for app <- list_apps(),
authorized?(app, user) do
app
end
end
@doc """
Returns if the given running app is authorized to given user.
Teams apps have authorization rules that can restrict access on
per-user basis. This kind of authorization is not applicable to
other types of apps, for those this function always returns `true`.
"""
@spec authorized?(App.t(), Livebook.Users.User.t()) :: boolean()
def authorized?(app, user)
def authorized?(_app, %{access_type: :full}), do: true
def authorized?(%{slug: slug, app_spec: %Apps.TeamsAppSpec{hub_id: id}}, user) do
Livebook.Hubs.TeamClient.user_app_access?(id, user.groups, slug)
end
def authorized?(_app, _user), do: true
@doc """
Updates the given app info across the cluster.
"""
@spec update_app(App.t()) :: :ok | {:error, any()}
def update_app(app) do
Livebook.Tracker.update_app(app)
end
@doc """
Subscribes to updates in the apps list.
## Messages
* `{:app_created, app}`
* `{:app_updated, app}`
* `{:app_closed, app}`
"""
@spec subscribe() :: :ok | {:error, term()}
def subscribe() do
Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_apps")
end
@doc """
Checks if the apps directory is configured and contains no notebooks.
"""
@spec empty_apps_path?() :: boolean()
def empty_apps_path?() do
if path = Livebook.Config.apps_path() do
pattern = Path.join([path, "**", "*.livemd"])
Path.wildcard(pattern) == []
else
false
end
end
@doc """
Gets all permanent app specs.
Permanent apps are apps that should be kept running in the cluster.
This includes apps from local directory and apps from hubs.
"""
@spec get_permanent_app_specs() :: list(Livebook.Apps.AppSpec.t())
def get_permanent_app_specs() do
app_specs = get_startup_app_specs() ++ Livebook.Hubs.get_app_specs()
# Just in case there is a slug conflict between startup specs and
# hub specs, we ensure slug uniqueness
Enum.uniq_by(app_specs, & &1.slug)
end
@startup_app_specs_key :livebook_startup_app_specs
@doc """
Sets permanent app specs that are kept only in memory.
"""
@spec set_startup_app_specs(list(Livebook.Apps.AppSpec.t())) :: :ok
def set_startup_app_specs(app_specs) do
:persistent_term.put(@startup_app_specs_key, app_specs)
end
@doc """
Gets the startup permanent app specs.
"""
@spec get_startup_app_specs() :: list(Livebook.Apps.AppSpec.t())
def get_startup_app_specs() do
:persistent_term.get(@startup_app_specs_key, [])
end
@doc """
Builds app specs for notebooks in the given directory.
## Options
* `:password` - a password to set for every loaded app
* `:should_warmup` - whether the app should be warmed up before
deployment. Disabling warmup makes sense if the app setup has
already been cached. Defaults to `true`
* `:hub_id` - when set, only imports notebooks from the given hub,
other notebooks are ignored
"""
@spec build_app_specs_in_dir(String.t(), keyword()) :: list(Livebook.Apps.AppSpec.t())
def build_app_specs_in_dir(path, opts \\ []) do
opts = Keyword.validate!(opts, [:password, :hub_id, should_warmup: true])
infos = import_app_notebooks(path, opts[:hub_id])
if infos == [] do
Logger.warning("No .livemd files were found for deployment at #{path}")
end
for %{status: {:error, message}} = info <- infos do
Logger.warning(
"Ignoring app at #{info.relative_path}. #{Livebook.Utils.upcase_first(message)}."
)
end
infos = Enum.filter(infos, &(&1.status == :ok))
infos =
infos
|> Enum.reduce({[], %{}}, fn info, {infos, slugs} ->
slug = info.notebook.app_settings.slug
case slugs do
%{^slug => other_path} ->
Logger.warning(
"Ignoring app at #{info.relative_path}. App with the same slug (#{slug}) is already present at #{other_path}"
)
{infos, slugs}
%{} ->
{[info | infos], Map.put(slugs, slug, info.relative_path)}
end
end)
|> elem(0)
|> Enum.reverse()
for info <- infos, info.import_warnings != [] do
items = Enum.map(info.import_warnings, &("- " <> &1))
Logger.warning(
"Found warnings while importing app notebook at #{info.relative_path}:\n " <>
Enum.join(items, "\n ")
)
end
for %{notebook: notebook} = info <- infos do
if opts[:password] == nil and notebook.app_settings.access_type == :protected do
Logger.warning(
"The app at #{info.relative_path} will use a random password." <>
" You may want to set LIVEBOOK_APPS_PATH_PASSWORD or make the app public."
)
end
%Livebook.Apps.PathAppSpec{
slug: notebook.app_settings.slug,
path: info.path,
password: opts[:password],
should_warmup: opts[:should_warmup]
}
end
end
defp import_app_notebooks(dir, hub_id) do
pattern = Path.join([dir, "**", "*.livemd"])
for path <- Path.wildcard(pattern) do
markdown = File.read!(path)
{notebook, %{warnings: warnings, stamp_verified?: stamp_verified?}} =
Livebook.LiveMarkdown.notebook_from_livemd(markdown)
status =
cond do
not Livebook.Notebook.AppSettings.valid?(notebook.app_settings) ->
{:error,
"the deployment settings are missing or invalid. Please configure them under the notebook deploy panel"}
# We only import notebooks from the given hub, but if that
# option is set then there should really be no other ones,
# so it makes sense to warn if there are
hub_id && notebook.hub_id != hub_id ->
{:error, "the notebook does not come from hub #{hub_id}"}
# We only deploy apps with valid stamp. We make an exception
# for personal hub, because the deployment instance has a
# different personal secret key anyway
notebook.hub_id != Livebook.Hubs.Personal.id() and not stamp_verified? ->
{:error, "the notebook does not have a valid stamp"}
true ->
:ok
end
notebook_file = Livebook.FileSystem.File.local(path)
files_dir = Livebook.Session.files_dir_for_notebook(notebook_file)
%{
path: path,
relative_path: Path.relative_to(path, dir),
status: status,
notebook: notebook,
import_warnings: warnings,
files_source: {:dir, files_dir}
}
end
end
@doc """
Returns a temporary dir for app files.
"""
@spec generate_files_tmp_path(String.t()) :: String.t()
def generate_files_tmp_path(slug) do
Path.join([
Livebook.Config.tmp_path(),
"app_files",
slug <> Livebook.Utils.random_short_id()
])
end
@doc """
Runs app warmup.
This evaluates the setup cell in the given notebook to populate the
relevant caches, such as dependency installation.
"""
@spec warmup_app(Livebook.Notebook.t(), String.t()) :: :ok | {:error, String.t()}
def warmup_app(notebook, files_tmp_path) do
run_app_setup_sync(notebook, files_tmp_path)
end
@doc """
Same as `warmup_app/2`, but loads app spec and immediately cleans up
afterwards.
"""
@spec warmup_app(Livebook.Apps.AppSpec.t()) :: :ok | {:error, String.t()}
def warmup_app(app_spec) do
files_tmp_path = generate_files_tmp_path(app_spec.slug)
result =
with {:ok, %{notebook: notebook}} <- Livebook.Apps.AppSpec.load(app_spec, files_tmp_path) do
warmup_app(notebook, files_tmp_path)
end
File.rm_rf(files_tmp_path)
result
end
defp run_app_setup_sync(notebook, files_tmp_path) do
files_source =
{:dir,
files_tmp_path
|> Livebook.FileSystem.Utils.ensure_dir_path()
|> Livebook.FileSystem.File.local()}
notebook = %{notebook | sections: []}
opts = [
notebook: notebook,
files_source: files_source,
mode: :app,
app_pid: self()
]
case Livebook.Sessions.create_session(opts) do
{:ok, %{id: session_id} = session} ->
ref = Process.monitor(session.pid)
receive do
{:app_status_changed, ^session_id, status} ->
Process.demonitor(ref)
Livebook.Session.close(session.pid)
if status.execution == :executed do
:ok
else
{:error, "setup cell finished with failure"}
end
{:DOWN, ^ref, :process, _, reason} ->
{:error, "session terminated unexpectedly, reason: #{inspect(reason)}"}
end
{:error, reason} ->
{:error, "failed to start session, reason: #{inspect(reason)}"}
end
end
end