Packages
livebook
0.11.4
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
# This module is responsible for starting and discovering apps.
#
# App processes are tracked using `Livebook.Tracker` in the same way
# that sessions are.
require Logger
alias Livebook.App
@doc """
Deploys the given notebook as an app.
If there is no app process under the corresponding slug, it is started.
Otherwise the notebook is deployed as a new version into the existing
app.
## Options
* `:warnings` - a list of warnings to show for the new deployment
* `:files_source` - a location to fetch notebook files from, see
`Livebook.Session.start_link/1` for more details
* `:start_only` - when `true`, deploys only if the app does not
exist already. Defaults to `false`
"""
@spec deploy(Livebook.Notebook.t(), keyword()) ::
{:ok, pid()} | {:error, :already_started} | {:error, term()}
def deploy(notebook, opts \\ []) do
opts = Keyword.validate!(opts, warnings: [], files_source: nil, start_only: false)
slug = notebook.app_settings.slug
name = name(slug)
case :global.whereis_name(name) do
:undefined ->
:global.trans({{:app_registration, name}, node()}, fn ->
case :global.whereis_name(name) do
:undefined ->
with {:ok, pid} <- start_app(notebook, opts[:warnings], opts[:files_source]) do
:yes = :global.register_name(name, pid)
{:ok, pid}
end
pid ->
redeploy_app(pid, notebook, opts)
end
end)
pid ->
redeploy_app(pid, notebook, opts)
end
end
defp start_app(notebook, warnings, files_source) do
opts = [notebook: notebook, warnings: warnings, files_source: files_source]
case DynamicSupervisor.start_child(Livebook.AppSupervisor, {App, opts}) do
{:ok, pid} ->
app = App.get_by_pid(pid)
case Livebook.Tracker.track_app(app) do
:ok ->
{:ok, pid}
{:error, reason} ->
App.close(pid)
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
defp redeploy_app(pid, notebook, opts) do
if opts[:start_only] do
{:error, :already_started}
else
App.deploy(pid, notebook, warnings: opts[:warnings], files_source: opts[:files_source])
{:ok, pid}
end
end
@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(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
case :global.whereis_name(name(slug)) do
:undefined -> :error
pid -> {: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(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
defp 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 """
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 update in 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 """
Deploys an app for each notebook in the given directory.
## Options
* `:password` - a password to set for every loaded app
* `:warmup` - when `true`, run setup cell for each of the
notebooks before the actual deployment. The setup cells are
run one by one to avoid race conditions. Defaults to `true`
* `:skip_deploy` - when `true`, the apps are not deployed.
This can be used to warmup apps without deployment. Defaults
to `false`
* `:start_only` - when `true`, deploys only if the app does not
exist already. Defaults to `false`
"""
@spec deploy_apps_in_dir(String.t(), keyword()) :: :ok
def deploy_apps_in_dir(path, opts \\ []) do
opts =
Keyword.validate!(opts, [:password, warmup: true, skip_deploy: false, start_only: false])
infos = import_app_notebooks(path)
if infos == [] do
Logger.warning("No .livemd files were found for deployment at #{path}")
end
for %{status: {:error, message}} = info <- infos do
Logger.warning(
"Skipping deployment for app at #{info.relative_path}. #{Livebook.Utils.upcase_first(message)}."
)
end
infos = Enum.filter(infos, &(&1.status == :ok))
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\n" <>
Enum.join(items, "\n")
)
end
if infos != [] and opts[:warmup] do
Logger.info("Running app warmups")
for info <- infos do
with {:error, message} <- run_app_setup_sync(info.notebook, info.files_source) do
Logger.warning(
"Failed to run setup for app at #{info.relative_path}. #{Livebook.Utils.upcase_first(message)}."
)
end
end
end
if infos != [] and not opts[:skip_deploy] do
Logger.info("Deploying apps")
for %{notebook: notebook} = info <- infos do
notebook =
if password = opts[:password] do
put_in(notebook.app_settings.password, password)
else
if 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
notebook
end
warnings = Enum.map(info.import_warnings, &("Import: " <> &1))
deploy(notebook,
warnings: warnings,
files_source: info.files_source,
start_only: opts[:start_only]
)
end
end
:ok
end
defp import_app_notebooks(dir) do
pattern = Path.join([dir, "**", "*.livemd"])
for path <- Path.wildcard(pattern) do
markdown = File.read!(path)
{notebook, warnings} = Livebook.LiveMarkdown.notebook_from_livemd(markdown)
apps_path_hub_id = Livebook.Config.apps_path_hub_id()
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"}
apps_path_hub_id && apps_path_hub_id != notebook.hub_id ->
{:error, "the notebook is not verified to come from hub #{apps_path_hub_id}"}
true ->
:ok
end
notebook_file = Livebook.FileSystem.File.local(path)
files_dir = Livebook.Session.files_dir_for_notebook(notebook_file)
%{
relative_path: Path.relative_to(path, dir),
status: status,
notebook: notebook,
import_warnings: warnings,
files_source: {:dir, files_dir}
}
end
end
defp run_app_setup_sync(notebook, files_source) do
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
@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
end