mirror of
https://github.com/livebook-dev/livebook.git
synced 2026-01-08 16:48:35 +08:00
Use Phoenix.Tracker to keep track of sessions within the cluster (#540)
* Use Phoenix.Tracker to keep track of sessions within the cluster * Apply review comments * Cleanup topics and updates * Update lib/livebook_web/live/session_live.ex
This commit is contained in:
parent
f83e51409a
commit
4ff1ff0d5a
31 changed files with 959 additions and 842 deletions
|
|
@ -15,8 +15,10 @@ defmodule Livebook.Application do
|
|||
LivebookWeb.Telemetry,
|
||||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Livebook.PubSub},
|
||||
# Start the tracker server on this node
|
||||
{Livebook.Tracker, pubsub_server: Livebook.PubSub},
|
||||
# Start the supervisor dynamically managing sessions
|
||||
Livebook.SessionSupervisor,
|
||||
{DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one},
|
||||
# Start the server responsible for associating files with sessions
|
||||
Livebook.Session.FileGuard,
|
||||
# Start the Node Pool for managing node names
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ defmodule Livebook.Session do
|
|||
# for a single specific evaluation context we make sure to copy
|
||||
# as little memory as necessary.
|
||||
|
||||
# The struct holds the basic session information that we track
|
||||
# and pass around. The notebook and evaluation state is kept
|
||||
# within the process state.
|
||||
defstruct [:id, :pid, :origin, :notebook_name, :file, :images_dir]
|
||||
|
||||
use GenServer, restart: :temporary
|
||||
|
||||
alias Livebook.Session.{Data, FileGuard}
|
||||
|
|
@ -50,6 +55,15 @@ defmodule Livebook.Session do
|
|||
alias Livebook.Users.User
|
||||
alias Livebook.Notebook.{Cell, Section}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: id(),
|
||||
pid: pid(),
|
||||
origin: {:file, FileSystem.File.t()} | {:url, String.t()} | nil,
|
||||
notebook_name: String.t(),
|
||||
file: FileSystem.File.t() | nil,
|
||||
images_dir: FileSystem.File.t()
|
||||
}
|
||||
|
||||
@type state :: %{
|
||||
session_id: id(),
|
||||
data: Data.t(),
|
||||
|
|
@ -58,15 +72,6 @@ defmodule Livebook.Session do
|
|||
save_task_pid: pid() | nil
|
||||
}
|
||||
|
||||
@type summary :: %{
|
||||
session_id: id(),
|
||||
pid: pid(),
|
||||
notebook_name: String.t(),
|
||||
file: FileSystem.File.t() | nil,
|
||||
images_dir: FileSystem.File.t(),
|
||||
origin: {:file, FileSystem.File.t()} | {:url, String.t()} | nil
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
An id assigned to every running session process.
|
||||
"""
|
||||
|
|
@ -75,12 +80,11 @@ defmodule Livebook.Session do
|
|||
## API
|
||||
|
||||
@doc """
|
||||
Starts the server process and registers it globally using the `:global` module,
|
||||
so that it's identifiable by the given id.
|
||||
Starts a session server process.
|
||||
|
||||
## Options
|
||||
|
||||
* `:id` (**required**) - a unique identifier to register the session under
|
||||
* `:id` (**required**) - a unique session identifier
|
||||
|
||||
* `:notebook` - the initial `Notebook` structure (e.g. imported from a file)
|
||||
|
||||
|
|
@ -94,22 +98,17 @@ defmodule Livebook.Session do
|
|||
* `:images` - a map from image name to its binary content, an alternative
|
||||
to `:copy_images_from` when the images are in memory
|
||||
"""
|
||||
@spec start_link(keyword()) :: GenServer.on_start()
|
||||
@spec start_link(keyword()) :: {:ok, pid} | {:error, any()}
|
||||
def start_link(opts) do
|
||||
id = Keyword.fetch!(opts, :id)
|
||||
GenServer.start_link(__MODULE__, opts, name: name(id))
|
||||
end
|
||||
|
||||
defp name(session_id) do
|
||||
{:global, {:session, session_id}}
|
||||
GenServer.start_link(__MODULE__, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns session pid given its id.
|
||||
Fetches session information from the session server.
|
||||
"""
|
||||
@spec get_pid(id()) :: pid() | nil
|
||||
def get_pid(session_id) do
|
||||
GenServer.whereis(name(session_id))
|
||||
@spec get_by_pid(pid()) :: Session.t()
|
||||
def get_by_pid(pid) do
|
||||
GenServer.call(pid, :describe_self)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -121,162 +120,153 @@ defmodule Livebook.Session do
|
|||
keep in sync with the server by subscribing to the `sessions:id` topic
|
||||
and receiving operations to apply.
|
||||
"""
|
||||
@spec register_client(id(), pid(), User.t()) :: Data.t()
|
||||
def register_client(session_id, client_pid, user) do
|
||||
GenServer.call(name(session_id), {:register_client, client_pid, user})
|
||||
@spec register_client(pid(), pid(), User.t()) :: Data.t()
|
||||
def register_client(pid, client_pid, user) do
|
||||
GenServer.call(pid, {:register_client, client_pid, user})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns data of the given session.
|
||||
"""
|
||||
@spec get_data(id()) :: Data.t()
|
||||
def get_data(session_id) do
|
||||
GenServer.call(name(session_id), :get_data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns basic information about the given session.
|
||||
"""
|
||||
@spec get_summary(id()) :: summary()
|
||||
def get_summary(session_id) do
|
||||
GenServer.call(name(session_id), :get_summary)
|
||||
@spec get_data(pid()) :: Data.t()
|
||||
def get_data(pid) do
|
||||
GenServer.call(pid, :get_data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current notebook structure.
|
||||
"""
|
||||
@spec get_notebook(id()) :: Notebook.t()
|
||||
def get_notebook(session_id) do
|
||||
GenServer.call(name(session_id), :get_notebook)
|
||||
@spec get_notebook(pid()) :: Notebook.t()
|
||||
def get_notebook(pid) do
|
||||
GenServer.call(pid, :get_notebook)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends notebook attributes update to the server.
|
||||
"""
|
||||
@spec set_notebook_attributes(id(), map()) :: :ok
|
||||
def set_notebook_attributes(session_id, attrs) do
|
||||
GenServer.cast(name(session_id), {:set_notebook_attributes, self(), attrs})
|
||||
@spec set_notebook_attributes(pid(), map()) :: :ok
|
||||
def set_notebook_attributes(pid, attrs) do
|
||||
GenServer.cast(pid, {:set_notebook_attributes, self(), attrs})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends section insertion request to the server.
|
||||
"""
|
||||
@spec insert_section(id(), non_neg_integer()) :: :ok
|
||||
def insert_section(session_id, index) do
|
||||
GenServer.cast(name(session_id), {:insert_section, self(), index})
|
||||
@spec insert_section(pid(), non_neg_integer()) :: :ok
|
||||
def insert_section(pid, index) do
|
||||
GenServer.cast(pid, {:insert_section, self(), index})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends section insertion request to the server.
|
||||
"""
|
||||
@spec insert_section_into(id(), Section.id(), non_neg_integer()) :: :ok
|
||||
def insert_section_into(session_id, section_id, index) do
|
||||
GenServer.cast(name(session_id), {:insert_section_into, self(), section_id, index})
|
||||
@spec insert_section_into(pid(), Section.id(), non_neg_integer()) :: :ok
|
||||
def insert_section_into(pid, section_id, index) do
|
||||
GenServer.cast(pid, {:insert_section_into, self(), section_id, index})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends parent update request to the server.
|
||||
"""
|
||||
@spec set_section_parent(id(), Section.id(), Section.id()) :: :ok
|
||||
def set_section_parent(session_id, section_id, parent_id) do
|
||||
GenServer.cast(name(session_id), {:set_section_parent, self(), section_id, parent_id})
|
||||
@spec set_section_parent(pid(), Section.id(), Section.id()) :: :ok
|
||||
def set_section_parent(pid, section_id, parent_id) do
|
||||
GenServer.cast(pid, {:set_section_parent, self(), section_id, parent_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends parent update request to the server.
|
||||
"""
|
||||
@spec unset_section_parent(id(), Section.id()) :: :ok
|
||||
def unset_section_parent(session_id, section_id) do
|
||||
GenServer.cast(name(session_id), {:unset_section_parent, self(), section_id})
|
||||
@spec unset_section_parent(pid(), Section.id()) :: :ok
|
||||
def unset_section_parent(pid, section_id) do
|
||||
GenServer.cast(pid, {:unset_section_parent, self(), section_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends cell insertion request to the server.
|
||||
"""
|
||||
@spec insert_cell(id(), Section.id(), non_neg_integer(), Cell.type()) ::
|
||||
:ok
|
||||
def insert_cell(session_id, section_id, index, type) do
|
||||
GenServer.cast(name(session_id), {:insert_cell, self(), section_id, index, type})
|
||||
@spec insert_cell(pid(), Section.id(), non_neg_integer(), Cell.type()) :: :ok
|
||||
def insert_cell(pid, section_id, index, type) do
|
||||
GenServer.cast(pid, {:insert_cell, self(), section_id, index, type})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends section deletion request to the server.
|
||||
"""
|
||||
@spec delete_section(id(), Section.id(), boolean()) :: :ok
|
||||
def delete_section(session_id, section_id, delete_cells) do
|
||||
GenServer.cast(name(session_id), {:delete_section, self(), section_id, delete_cells})
|
||||
@spec delete_section(pid(), Section.id(), boolean()) :: :ok
|
||||
def delete_section(pid, section_id, delete_cells) do
|
||||
GenServer.cast(pid, {:delete_section, self(), section_id, delete_cells})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends cell deletion request to the server.
|
||||
"""
|
||||
@spec delete_cell(id(), Cell.id()) :: :ok
|
||||
def delete_cell(session_id, cell_id) do
|
||||
GenServer.cast(name(session_id), {:delete_cell, self(), cell_id})
|
||||
@spec delete_cell(pid(), Cell.id()) :: :ok
|
||||
def delete_cell(pid, cell_id) do
|
||||
GenServer.cast(pid, {:delete_cell, self(), cell_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends cell restoration request to the server.
|
||||
"""
|
||||
@spec restore_cell(id(), Cell.id()) :: :ok
|
||||
def restore_cell(session_id, cell_id) do
|
||||
GenServer.cast(name(session_id), {:restore_cell, self(), cell_id})
|
||||
@spec restore_cell(pid(), Cell.id()) :: :ok
|
||||
def restore_cell(pid, cell_id) do
|
||||
GenServer.cast(pid, {:restore_cell, self(), cell_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends cell move request to the server.
|
||||
"""
|
||||
@spec move_cell(id(), Cell.id(), integer()) :: :ok
|
||||
def move_cell(session_id, cell_id, offset) do
|
||||
GenServer.cast(name(session_id), {:move_cell, self(), cell_id, offset})
|
||||
@spec move_cell(pid(), Cell.id(), integer()) :: :ok
|
||||
def move_cell(pid, cell_id, offset) do
|
||||
GenServer.cast(pid, {:move_cell, self(), cell_id, offset})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends section move request to the server.
|
||||
"""
|
||||
@spec move_section(id(), Section.id(), integer()) :: :ok
|
||||
def move_section(session_id, section_id, offset) do
|
||||
GenServer.cast(name(session_id), {:move_section, self(), section_id, offset})
|
||||
@spec move_section(pid(), Section.id(), integer()) :: :ok
|
||||
def move_section(pid, section_id, offset) do
|
||||
GenServer.cast(pid, {:move_section, self(), section_id, offset})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends cell evaluation request to the server.
|
||||
"""
|
||||
@spec queue_cell_evaluation(id(), Cell.id()) :: :ok
|
||||
def queue_cell_evaluation(session_id, cell_id) do
|
||||
GenServer.cast(name(session_id), {:queue_cell_evaluation, self(), cell_id})
|
||||
@spec queue_cell_evaluation(pid(), Cell.id()) :: :ok
|
||||
def queue_cell_evaluation(pid, cell_id) do
|
||||
GenServer.cast(pid, {:queue_cell_evaluation, self(), cell_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends cell evaluation cancellation request to the server.
|
||||
"""
|
||||
@spec cancel_cell_evaluation(id(), Cell.id()) :: :ok
|
||||
def cancel_cell_evaluation(session_id, cell_id) do
|
||||
GenServer.cast(name(session_id), {:cancel_cell_evaluation, self(), cell_id})
|
||||
@spec cancel_cell_evaluation(pid(), Cell.id()) :: :ok
|
||||
def cancel_cell_evaluation(pid, cell_id) do
|
||||
GenServer.cast(pid, {:cancel_cell_evaluation, self(), cell_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends notebook name update request to the server.
|
||||
"""
|
||||
@spec set_notebook_name(id(), String.t()) :: :ok
|
||||
def set_notebook_name(session_id, name) do
|
||||
GenServer.cast(name(session_id), {:set_notebook_name, self(), name})
|
||||
@spec set_notebook_name(pid(), String.t()) :: :ok
|
||||
def set_notebook_name(pid, name) do
|
||||
GenServer.cast(pid, {:set_notebook_name, self(), name})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends section name update request to the server.
|
||||
"""
|
||||
@spec set_section_name(id(), Section.id(), String.t()) :: :ok
|
||||
def set_section_name(session_id, section_id, name) do
|
||||
GenServer.cast(name(session_id), {:set_section_name, self(), section_id, name})
|
||||
@spec set_section_name(pid(), Section.id(), String.t()) :: :ok
|
||||
def set_section_name(pid, section_id, name) do
|
||||
GenServer.cast(pid, {:set_section_name, self(), section_id, name})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends a cell delta to apply to the server.
|
||||
"""
|
||||
@spec apply_cell_delta(id(), Cell.id(), Delta.t(), Data.cell_revision()) :: :ok
|
||||
def apply_cell_delta(session_id, cell_id, delta, revision) do
|
||||
GenServer.cast(name(session_id), {:apply_cell_delta, self(), cell_id, delta, revision})
|
||||
@spec apply_cell_delta(pid(), Cell.id(), Delta.t(), Data.cell_revision()) :: :ok
|
||||
def apply_cell_delta(pid, cell_id, delta, revision) do
|
||||
GenServer.cast(pid, {:apply_cell_delta, self(), cell_id, delta, revision})
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -284,17 +274,17 @@ defmodule Livebook.Session do
|
|||
|
||||
This helps to remove old deltas that are no longer necessary.
|
||||
"""
|
||||
@spec report_cell_revision(id(), Cell.id(), Data.cell_revision()) :: :ok
|
||||
def report_cell_revision(session_id, cell_id, revision) do
|
||||
GenServer.cast(name(session_id), {:report_cell_revision, self(), cell_id, revision})
|
||||
@spec report_cell_revision(pid(), Cell.id(), Data.cell_revision()) :: :ok
|
||||
def report_cell_revision(pid, cell_id, revision) do
|
||||
GenServer.cast(pid, {:report_cell_revision, self(), cell_id, revision})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends a cell attributes update to the server.
|
||||
"""
|
||||
@spec set_cell_attributes(id(), Cell.id(), map()) :: :ok
|
||||
def set_cell_attributes(session_id, cell_id, attrs) do
|
||||
GenServer.cast(name(session_id), {:set_cell_attributes, self(), cell_id, attrs})
|
||||
@spec set_cell_attributes(pid(), Cell.id(), map()) :: :ok
|
||||
def set_cell_attributes(pid, cell_id, attrs) do
|
||||
GenServer.cast(pid, {:set_cell_attributes, self(), cell_id, attrs})
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -303,9 +293,9 @@ defmodule Livebook.Session do
|
|||
Note that this results in initializing the corresponding remote node
|
||||
with modules and processes required for evaluation.
|
||||
"""
|
||||
@spec connect_runtime(id(), Runtime.t()) :: :ok
|
||||
def connect_runtime(session_id, runtime) do
|
||||
GenServer.cast(name(session_id), {:connect_runtime, self(), runtime})
|
||||
@spec connect_runtime(pid(), Runtime.t()) :: :ok
|
||||
def connect_runtime(pid, runtime) do
|
||||
GenServer.cast(pid, {:connect_runtime, self(), runtime})
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -313,17 +303,17 @@ defmodule Livebook.Session do
|
|||
|
||||
Note that this results in clearing the evaluation state.
|
||||
"""
|
||||
@spec disconnect_runtime(id()) :: :ok
|
||||
def disconnect_runtime(session_id) do
|
||||
GenServer.cast(name(session_id), {:disconnect_runtime, self()})
|
||||
@spec disconnect_runtime(pid()) :: :ok
|
||||
def disconnect_runtime(pid) do
|
||||
GenServer.cast(pid, {:disconnect_runtime, self()})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends file location update request to the server.
|
||||
"""
|
||||
@spec set_file(id(), FileSystem.File.t() | nil) :: :ok
|
||||
def set_file(session_id, file) do
|
||||
GenServer.cast(name(session_id), {:set_file, self(), file})
|
||||
@spec set_file(pid(), FileSystem.File.t() | nil) :: :ok
|
||||
def set_file(pid, file) do
|
||||
GenServer.cast(pid, {:set_file, self(), file})
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -334,17 +324,17 @@ defmodule Livebook.Session do
|
|||
|
||||
Note that notebooks are automatically persisted every @autosave_interval milliseconds.
|
||||
"""
|
||||
@spec save(id()) :: :ok
|
||||
def save(session_id) do
|
||||
GenServer.cast(name(session_id), :save)
|
||||
@spec save(pid()) :: :ok
|
||||
def save(pid) do
|
||||
GenServer.cast(pid, :save)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Synchronous version of `save/1`.
|
||||
"""
|
||||
@spec save_sync(id()) :: :ok
|
||||
def save_sync(session_id) do
|
||||
GenServer.call(name(session_id), :save_sync)
|
||||
@spec save_sync(pid()) :: :ok
|
||||
def save_sync(pid) do
|
||||
GenServer.call(pid, :save_sync)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -353,9 +343,9 @@ defmodule Livebook.Session do
|
|||
This results in saving the file and broadcasting
|
||||
a :closed message to the session topic.
|
||||
"""
|
||||
@spec close(id()) :: :ok
|
||||
def close(session_id) do
|
||||
GenServer.cast(name(session_id), :close)
|
||||
@spec close(pid()) :: :ok
|
||||
def close(pid) do
|
||||
GenServer.cast(pid, :close)
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
|
@ -432,6 +422,10 @@ defmodule Livebook.Session do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:describe_self, _from, state) do
|
||||
{:reply, self_from_state(state), state}
|
||||
end
|
||||
|
||||
def handle_call({:register_client, client_pid, user}, _from, state) do
|
||||
Process.monitor(client_pid)
|
||||
|
||||
|
|
@ -444,10 +438,6 @@ defmodule Livebook.Session do
|
|||
{:reply, state.data, state}
|
||||
end
|
||||
|
||||
def handle_call(:get_summary, _from, state) do
|
||||
{:reply, summary_from_state(state), state}
|
||||
end
|
||||
|
||||
def handle_call(:get_notebook, _from, state) do
|
||||
{:reply, state.data.notebook, state}
|
||||
end
|
||||
|
|
@ -694,14 +684,14 @@ defmodule Livebook.Session do
|
|||
|
||||
# ---
|
||||
|
||||
defp summary_from_state(state) do
|
||||
%{
|
||||
session_id: state.session_id,
|
||||
defp self_from_state(state) do
|
||||
%__MODULE__{
|
||||
id: state.session_id,
|
||||
pid: self(),
|
||||
origin: state.data.origin,
|
||||
notebook_name: state.data.notebook.name,
|
||||
file: state.data.file,
|
||||
images_dir: images_dir_from_state(state),
|
||||
origin: state.data.origin
|
||||
images_dir: images_dir_from_state(state)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -807,6 +797,11 @@ defmodule Livebook.Session do
|
|||
end
|
||||
end
|
||||
|
||||
defp after_operation(state, _prev_state, {:set_notebook_name, _pid, _name}) do
|
||||
notify_update(state)
|
||||
state
|
||||
end
|
||||
|
||||
defp after_operation(state, prev_state, {:set_file, _pid, _file}) do
|
||||
prev_images_dir = images_dir_from_state(prev_state)
|
||||
|
||||
|
|
@ -823,6 +818,8 @@ defmodule Livebook.Session do
|
|||
broadcast_error(state.session_id, "failed to copy images - #{message}")
|
||||
end
|
||||
|
||||
notify_update(state)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
|
|
@ -948,6 +945,12 @@ defmodule Livebook.Session do
|
|||
Phoenix.PubSub.broadcast(Livebook.PubSub, "sessions:#{session_id}", message)
|
||||
end
|
||||
|
||||
defp notify_update(state) do
|
||||
session = self_from_state(state)
|
||||
Livebook.Sessions.update_session(session)
|
||||
broadcast_message(state.session_id, {:session_updated, session})
|
||||
end
|
||||
|
||||
defp maybe_save_notebook_async(state) do
|
||||
if should_save_notebook?(state) do
|
||||
pid = self()
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
defmodule Livebook.SessionSupervisor do
|
||||
@moduledoc false
|
||||
|
||||
# Supervisor responsible for managing running notebook sessions.
|
||||
#
|
||||
# Allows for creating new session processes on demand
|
||||
# and managing them using random ids.
|
||||
|
||||
use DynamicSupervisor
|
||||
|
||||
alias Livebook.{Session, Utils}
|
||||
|
||||
@name __MODULE__
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
DynamicSupervisor.start_link(__MODULE__, opts, name: @name)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
DynamicSupervisor.init(strategy: :one_for_one)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns a new `Session` process with the given options.
|
||||
|
||||
Broadcasts `{:session_created, id}` message under the `"sessions"` topic.
|
||||
"""
|
||||
@spec create_session(keyword()) :: {:ok, Session.id()} | {:error, any()}
|
||||
def create_session(opts \\ []) do
|
||||
id = Utils.random_id()
|
||||
|
||||
opts = Keyword.put(opts, :id, id)
|
||||
|
||||
case DynamicSupervisor.start_child(@name, {Session, opts}) do
|
||||
{:ok, _} ->
|
||||
broadcast_sessions_message({:session_created, id})
|
||||
{:ok, id}
|
||||
|
||||
{:ok, _, _} ->
|
||||
broadcast_sessions_message({:session_created, id})
|
||||
{:ok, id}
|
||||
|
||||
:ignore ->
|
||||
{:error, :ignore}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously stops a session process identified by the given id.
|
||||
|
||||
Broadcasts `{:session_closed, id}` message under the `"sessions"` topic.
|
||||
"""
|
||||
@spec close_session(Session.id()) :: :ok
|
||||
def close_session(id) do
|
||||
Session.close(id)
|
||||
broadcast_sessions_message({:session_closed, id})
|
||||
:ok
|
||||
end
|
||||
|
||||
defp broadcast_sessions_message(message) do
|
||||
Phoenix.PubSub.broadcast(Livebook.PubSub, "sessions", message)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns ids of all the running session processes.
|
||||
"""
|
||||
@spec get_session_ids() :: list(Session.id())
|
||||
def get_session_ids() do
|
||||
:global.registered_names()
|
||||
|> Enum.flat_map(fn
|
||||
{:session, id} -> [id]
|
||||
_ -> []
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns summaries of all the running session processes.
|
||||
"""
|
||||
@spec get_session_summaries() :: list(Session.summary())
|
||||
def get_session_summaries() do
|
||||
Enum.map(get_session_ids(), &Session.get_summary/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a session process with the given id exists.
|
||||
"""
|
||||
@spec session_exists?(Session.id()) :: boolean()
|
||||
def session_exists?(id) do
|
||||
:global.whereis_name({:session, id}) != :undefined
|
||||
end
|
||||
|
||||
@doc """
|
||||
Retrieves pid of a session process identified by the given id.
|
||||
"""
|
||||
@spec get_session_pid(Session.id()) :: {:ok, pid()} | {:error, :nonexistent}
|
||||
def get_session_pid(id) do
|
||||
case :global.whereis_name({:session, id}) do
|
||||
:undefined -> {:error, :nonexistent}
|
||||
pid -> {:ok, pid}
|
||||
end
|
||||
end
|
||||
end
|
||||
83
lib/livebook/sessions.ex
Normal file
83
lib/livebook/sessions.ex
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
defmodule Livebook.Sessions do
|
||||
@moduledoc false
|
||||
|
||||
# This module is responsible for starting and discovering sessions.
|
||||
#
|
||||
# Every session has a server process and is described by a `%Session{}`
|
||||
# info struct. Information about all sessions in the cluster is
|
||||
# propagated using `Livebook.Tracker`, which serves as an ephemeral
|
||||
# distributed database for the `%Session{}` structs.
|
||||
|
||||
alias Livebook.{Session, Utils}
|
||||
|
||||
@doc """
|
||||
Spawns a new `Session` process with the given options.
|
||||
|
||||
Makes the session globally visible within the session tracker.
|
||||
|
||||
`{:session_created, session}` gets broadcasted under the "tracker_sessions" topic.
|
||||
"""
|
||||
@spec create_session(keyword()) :: {:ok, Session.t()} | {:error, any()}
|
||||
def create_session(opts \\ []) do
|
||||
id = Utils.random_node_aware_id()
|
||||
|
||||
opts = Keyword.put(opts, :id, id)
|
||||
|
||||
case DynamicSupervisor.start_child(Livebook.SessionSupervisor, {Session, opts}) do
|
||||
{:ok, pid} ->
|
||||
session = Session.get_by_pid(pid)
|
||||
|
||||
case Livebook.Tracker.track_session(session) do
|
||||
:ok ->
|
||||
{:ok, session}
|
||||
|
||||
{:error, reason} ->
|
||||
Session.close(pid)
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all the running sessions.
|
||||
"""
|
||||
@spec list_sessions() :: list(Session.t())
|
||||
def list_sessions() do
|
||||
Livebook.Tracker.list_sessions()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns tracked session with the given id.
|
||||
"""
|
||||
@spec fetch_session(Session.id()) :: {:ok, Session.t()} | :error
|
||||
def fetch_session(id) do
|
||||
case Livebook.Tracker.fetch_session(id) do
|
||||
{:ok, session} ->
|
||||
{:ok, session}
|
||||
|
||||
:error ->
|
||||
# The local tracker server doesn't know about this session,
|
||||
# but it may not have propagated yet, so we extract the session
|
||||
# node from id and ask the corresponding tracker directly
|
||||
with {:ok, other_node} when other_node != node() <- Utils.node_from_node_aware_id(id),
|
||||
{:ok, session} <- :rpc.call(other_node, Livebook.Tracker, :fetch_session, [id]) do
|
||||
{:ok, session}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the given session info across the cluster.
|
||||
|
||||
`{:session_updated, session}` gets broadcasted under the "tracker_sessions" topic.
|
||||
"""
|
||||
@spec update_session(Session.t()) :: :ok | {:error, any()}
|
||||
def update_session(session) do
|
||||
Livebook.Tracker.update_session(session)
|
||||
end
|
||||
end
|
||||
100
lib/livebook/tracker.ex
Normal file
100
lib/livebook/tracker.ex
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
defmodule Livebook.Tracker do
|
||||
@moduledoc false
|
||||
|
||||
use Phoenix.Tracker
|
||||
|
||||
alias Livebook.Session
|
||||
|
||||
@name __MODULE__
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
opts = Keyword.merge([name: @name], opts)
|
||||
Phoenix.Tracker.start_link(__MODULE__, opts, opts)
|
||||
end
|
||||
|
||||
@sessions_topic "sessions"
|
||||
|
||||
@doc """
|
||||
Starts tracking the given session, making it visible globally.
|
||||
"""
|
||||
@spec track_session(Session.t()) :: :ok | {:error, any()}
|
||||
def track_session(session) do
|
||||
case Phoenix.Tracker.track(@name, session.pid, @sessions_topic, session.id, %{
|
||||
session: session
|
||||
}) do
|
||||
{:ok, _ref} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the tracked session object matching the given id.
|
||||
"""
|
||||
@spec update_session(Session.t()) :: :ok | {:error, any()}
|
||||
def update_session(session) do
|
||||
case Phoenix.Tracker.update(@name, session.pid, @sessions_topic, session.id, %{
|
||||
session: session
|
||||
}) do
|
||||
{:ok, _ref} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all tracked sessions.
|
||||
"""
|
||||
@spec list_sessions() :: list(Session.t())
|
||||
def list_sessions() do
|
||||
presences = Phoenix.Tracker.list(@name, @sessions_topic)
|
||||
for {_id, %{session: session}} <- presences, do: session
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns tracked session with the given id.
|
||||
"""
|
||||
@spec fetch_session(Session.id()) :: {:ok, Session.t()} | :error
|
||||
def fetch_session(id) do
|
||||
case Phoenix.Tracker.get_by_key(@name, @sessions_topic, id) do
|
||||
[{_id, %{session: session}}] -> {:ok, session}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
server = Keyword.fetch!(opts, :pubsub_server)
|
||||
{:ok, %{pubsub_server: server, node_name: Phoenix.PubSub.node_name(server)}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_diff(diff, state) do
|
||||
for {topic, topic_diff} <- diff do
|
||||
handle_topic_diff(topic, topic_diff, state)
|
||||
end
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defp handle_topic_diff(@sessions_topic, {joins, leaves}, state) do
|
||||
joins = Map.new(joins)
|
||||
leaves = Map.new(leaves)
|
||||
|
||||
messages =
|
||||
for id <- Enum.uniq(Map.keys(joins) ++ Map.keys(leaves)) do
|
||||
case {joins[id], leaves[id]} do
|
||||
{%{session: session}, nil} -> {:session_created, session}
|
||||
{nil, %{session: session}} -> {:session_closed, session}
|
||||
{%{session: session}, %{}} -> {:session_updated, session}
|
||||
end
|
||||
end
|
||||
|
||||
for message <- messages do
|
||||
Phoenix.PubSub.direct_broadcast!(
|
||||
state.node_name,
|
||||
state.pubsub_server,
|
||||
"tracker_sessions",
|
||||
message
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,7 @@ defmodule Livebook.Utils do
|
|||
@doc """
|
||||
Generates a random binary id.
|
||||
"""
|
||||
@spec random_id() :: binary()
|
||||
@spec random_id() :: id()
|
||||
def random_id() do
|
||||
:crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower)
|
||||
end
|
||||
|
|
@ -14,7 +14,7 @@ defmodule Livebook.Utils do
|
|||
@doc """
|
||||
Generates a random short binary id.
|
||||
"""
|
||||
@spec random_short_id() :: binary()
|
||||
@spec random_short_id() :: id()
|
||||
def random_short_id() do
|
||||
:crypto.strong_rand_bytes(5) |> Base.encode32(case: :lower)
|
||||
end
|
||||
|
|
@ -27,6 +27,50 @@ defmodule Livebook.Utils do
|
|||
:crypto.strong_rand_bytes(42) |> Base.url_encode64() |> String.to_atom()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a random binary id that includes node information.
|
||||
|
||||
## Format
|
||||
|
||||
The id is formed from the following binary parts:
|
||||
|
||||
* 16B - hashed node name
|
||||
* 9B - random bytes
|
||||
|
||||
The binary is base32 encoded.
|
||||
"""
|
||||
@spec random_node_aware_id() :: id()
|
||||
def random_node_aware_id() do
|
||||
node_part = node_hash(node())
|
||||
random_part = :crypto.strong_rand_bytes(9)
|
||||
binary = <<node_part::binary, random_part::binary>>
|
||||
# 16B + 9B = 25B is suitable for base32 encoding without padding
|
||||
Base.encode32(binary, case: :lower)
|
||||
end
|
||||
|
||||
# Note: the result is always 16 bytes long
|
||||
defp node_hash(node) do
|
||||
content = Atom.to_string(node)
|
||||
:erlang.md5(content)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts node name from the given node aware id.
|
||||
|
||||
The node in question must be connected, otherwise it won't be found.
|
||||
"""
|
||||
@spec node_from_node_aware_id(id()) :: {:ok, node()} | :error
|
||||
def node_from_node_aware_id(id) do
|
||||
binary = Base.decode32!(id, case: :lower)
|
||||
<<node_part::binary-size(16), _random_part::binary-size(9)>> = binary
|
||||
|
||||
known_nodes = [node() | Node.list()]
|
||||
|
||||
Enum.find_value(known_nodes, :error, fn node ->
|
||||
node_hash(node) == node_part && {:ok, node}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given name to node identifier.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
defmodule LivebookWeb.SessionController do
|
||||
use LivebookWeb, :controller
|
||||
|
||||
alias Livebook.{SessionSupervisor, Session, FileSystem}
|
||||
alias Livebook.{Sessions, Session, FileSystem}
|
||||
|
||||
def show_image(conn, %{"id" => id, "image" => image}) do
|
||||
if SessionSupervisor.session_exists?(id) do
|
||||
%{images_dir: images_dir} = Session.get_summary(id)
|
||||
file = FileSystem.File.resolve(images_dir, image)
|
||||
serve_static(conn, file)
|
||||
else
|
||||
send_resp(conn, 404, "Not found")
|
||||
case Sessions.fetch_session(id) do
|
||||
{:ok, session} ->
|
||||
file = FileSystem.File.resolve(session.images_dir, image)
|
||||
serve_static(conn, file)
|
||||
|
||||
:error ->
|
||||
send_resp(conn, 404, "Not found")
|
||||
end
|
||||
end
|
||||
|
||||
def download_source(conn, %{"id" => id, "format" => format}) do
|
||||
if SessionSupervisor.session_exists?(id) do
|
||||
notebook = Session.get_notebook(id)
|
||||
case Sessions.fetch_session(id) do
|
||||
{:ok, session} ->
|
||||
notebook = Session.get_notebook(session.pid)
|
||||
|
||||
send_notebook_source(conn, notebook, format)
|
||||
else
|
||||
send_resp(conn, 404, "Not found")
|
||||
send_notebook_source(conn, notebook, format)
|
||||
|
||||
:error ->
|
||||
send_resp(conn, 404, "Not found")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ defmodule LivebookWeb.HomeLive do
|
|||
import LivebookWeb.SessionHelpers
|
||||
|
||||
alias LivebookWeb.{SidebarHelpers, ExploreHelpers}
|
||||
alias Livebook.{SessionSupervisor, Session, LiveMarkdown, Notebook, FileSystem}
|
||||
alias Livebook.{Sessions, Session, LiveMarkdown, Notebook, FileSystem}
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"current_user_id" => current_user_id} = session, socket) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "users:#{current_user_id}")
|
||||
end
|
||||
|
||||
current_user = build_current_user(session, socket)
|
||||
session_summaries = sort_session_summaries(SessionSupervisor.get_session_summaries())
|
||||
sessions = sort_sessions(Sessions.list_sessions())
|
||||
notebook_infos = Notebook.Explore.notebook_infos() |> Enum.take(3)
|
||||
|
||||
{:ok,
|
||||
|
|
@ -23,7 +23,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
current_user: current_user,
|
||||
file: Livebook.Config.default_dir(),
|
||||
file_info: %{exists: true, access: :read_write},
|
||||
session_summaries: session_summaries,
|
||||
sessions: sessions,
|
||||
notebook_infos: notebook_infos
|
||||
)}
|
||||
end
|
||||
|
|
@ -63,7 +63,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
id: "home-file-select",
|
||||
file: @file,
|
||||
extnames: [LiveMarkdown.extension()],
|
||||
running_files: files(@session_summaries) do %>
|
||||
running_files: files(@sessions) do %>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button class="button button-outlined-gray whitespace-nowrap"
|
||||
phx-click="fork"
|
||||
|
|
@ -71,15 +71,15 @@ defmodule LivebookWeb.HomeLive do
|
|||
<.remix_icon icon="git-branch-line" class="align-middle mr-1" />
|
||||
<span>Fork</span>
|
||||
</button>
|
||||
<%= if file_running?(@file, @session_summaries) do %>
|
||||
<%= if file_running?(@file, @sessions) do %>
|
||||
<%= live_redirect "Join session",
|
||||
to: Routes.session_path(@socket, :page, session_id_by_file(@file, @session_summaries)),
|
||||
to: Routes.session_path(@socket, :page, session_id_by_file(@file, @sessions)),
|
||||
class: "button button-blue" %>
|
||||
<% else %>
|
||||
<span {open_button_tooltip_attrs(@file, @file_info)}>
|
||||
<button class="button button-blue"
|
||||
phx-click="open"
|
||||
disabled={not path_openable?(@file, @file_info, @session_summaries)}>
|
||||
disabled={not path_openable?(@file, @file_info, @sessions)}>
|
||||
Open
|
||||
</button>
|
||||
</span>
|
||||
|
|
@ -112,7 +112,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
<h2 class="mb-4 text-xl font-semibold text-gray-800">
|
||||
Running sessions
|
||||
</h2>
|
||||
<.sessions_list session_summaries={@session_summaries} socket={@socket} />
|
||||
<.sessions_list sessions={@sessions} socket={@socket} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -131,7 +131,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
id: "close-session",
|
||||
modal_class: "w-full max-w-xl",
|
||||
return_to: Routes.home_path(@socket, :page),
|
||||
session_summary: @session_summary %>
|
||||
session: @session %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :import do %>
|
||||
|
|
@ -152,7 +152,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
end
|
||||
end
|
||||
|
||||
defp sessions_list(%{session_summaries: []} = assigns) do
|
||||
defp sessions_list(%{sessions: []} = assigns) do
|
||||
~H"""
|
||||
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
|
||||
<div>
|
||||
|
|
@ -170,35 +170,35 @@ defmodule LivebookWeb.HomeLive do
|
|||
defp sessions_list(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col space-y-4">
|
||||
<%= for summary <- @session_summaries do %>
|
||||
<%= for session <- @sessions do %>
|
||||
<div class="p-5 flex items-center border border-gray-200 rounded-lg"
|
||||
data-test-session-id={summary.session_id}>
|
||||
data-test-session-id={session.id}>
|
||||
<div class="flex-grow flex flex-col space-y-1">
|
||||
<%= live_redirect summary.notebook_name,
|
||||
to: Routes.session_path(@socket, :page, summary.session_id),
|
||||
<%= live_redirect session.notebook_name,
|
||||
to: Routes.session_path(@socket, :page, session.id),
|
||||
class: "font-semibold text-gray-800 hover:text-gray-900" %>
|
||||
<div class="text-gray-600 text-sm">
|
||||
<%= if summary.file, do: summary.file.path, else: "No file" %>
|
||||
<%= if session.file, do: session.file.path, else: "No file" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" id={"session-#{summary.session_id}-menu"} phx-hook="Menu" data-element="menu">
|
||||
<div class="relative" id={"session-#{session.id}-menu"} phx-hook="Menu" data-element="menu">
|
||||
<button class="icon-button" data-toggle>
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<button class="menu__item text-gray-500"
|
||||
phx-click="fork_session"
|
||||
phx-value-id={summary.session_id}>
|
||||
phx-value-id={session.id}>
|
||||
<.remix_icon icon="git-branch-line" />
|
||||
<span class="font-medium">Fork</span>
|
||||
</button>
|
||||
<a class="menu__item text-gray-500"
|
||||
href={live_dashboard_process_path(@socket, summary.pid)}
|
||||
href={live_dashboard_process_path(@socket, session.pid)}
|
||||
target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" />
|
||||
<span class="font-medium">See on Dashboard</span>
|
||||
</a>
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, summary.session_id),
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, session.id),
|
||||
class: "menu__item text-red-600" do %>
|
||||
<.remix_icon icon="close-circle-line" />
|
||||
<span class="font-medium">Close</span>
|
||||
|
|
@ -213,8 +213,8 @@ defmodule LivebookWeb.HomeLive do
|
|||
|
||||
@impl true
|
||||
def handle_params(%{"session_id" => session_id}, _url, socket) do
|
||||
session_summary = Enum.find(socket.assigns.session_summaries, &(&1.session_id == session_id))
|
||||
{:noreply, assign(socket, session_summary: session_summary)}
|
||||
session = Enum.find(socket.assigns.sessions, &(&1.id == session_id))
|
||||
{:noreply, assign(socket, session: session)}
|
||||
end
|
||||
|
||||
def handle_params(%{"tab" => tab}, _url, socket) do
|
||||
|
|
@ -270,9 +270,10 @@ defmodule LivebookWeb.HomeLive do
|
|||
end
|
||||
|
||||
def handle_event("fork_session", %{"id" => session_id}, socket) do
|
||||
data = Session.get_data(session_id)
|
||||
session = Enum.find(socket.assigns.sessions, &(&1.id == session_id))
|
||||
%{images_dir: images_dir} = session
|
||||
data = Session.get_data(session.pid)
|
||||
notebook = Notebook.forked(data.notebook)
|
||||
%{images_dir: images_dir} = Session.get_summary(session_id)
|
||||
|
||||
origin =
|
||||
if data.file do
|
||||
|
|
@ -303,15 +304,27 @@ defmodule LivebookWeb.HomeLive do
|
|||
{:noreply, assign(socket, file: file, file_info: file_info)}
|
||||
end
|
||||
|
||||
def handle_info({:session_created, id}, socket) do
|
||||
summary = Session.get_summary(id)
|
||||
session_summaries = sort_session_summaries([summary | socket.assigns.session_summaries])
|
||||
{:noreply, assign(socket, session_summaries: session_summaries)}
|
||||
def handle_info({:session_created, session}, socket) do
|
||||
if session in socket.assigns.sessions do
|
||||
{:noreply, socket}
|
||||
else
|
||||
sessions = sort_sessions([session | socket.assigns.sessions])
|
||||
{:noreply, assign(socket, sessions: sessions)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:session_closed, id}, socket) do
|
||||
session_summaries = Enum.reject(socket.assigns.session_summaries, &(&1.session_id == id))
|
||||
{:noreply, assign(socket, session_summaries: session_summaries)}
|
||||
def handle_info({:session_updated, session}, socket) do
|
||||
sessions =
|
||||
Enum.map(socket.assigns.sessions, fn other ->
|
||||
if other.id == session.id, do: session, else: other
|
||||
end)
|
||||
|
||||
{:noreply, assign(socket, sessions: sessions)}
|
||||
end
|
||||
|
||||
def handle_info({:session_closed, session}, socket) do
|
||||
sessions = Enum.reject(socket.assigns.sessions, &(&1.id == session.id))
|
||||
{:noreply, assign(socket, sessions: sessions)}
|
||||
end
|
||||
|
||||
def handle_info({:import_content, content, session_opts}, socket) do
|
||||
|
|
@ -330,20 +343,20 @@ defmodule LivebookWeb.HomeLive do
|
|||
|
||||
def handle_info(_message, socket), do: {:noreply, socket}
|
||||
|
||||
defp sort_session_summaries(session_summaries) do
|
||||
Enum.sort_by(session_summaries, & &1.notebook_name)
|
||||
defp sort_sessions(sessions) do
|
||||
Enum.sort_by(sessions, & &1.notebook_name)
|
||||
end
|
||||
|
||||
defp files(session_summaries) do
|
||||
Enum.map(session_summaries, & &1.file)
|
||||
defp files(sessions) do
|
||||
Enum.map(sessions, & &1.file)
|
||||
end
|
||||
|
||||
defp path_forkable?(file, file_info) do
|
||||
regular?(file, file_info)
|
||||
end
|
||||
|
||||
defp path_openable?(file, file_info, session_summaries) do
|
||||
regular?(file, file_info) and not file_running?(file, session_summaries) and
|
||||
defp path_openable?(file, file_info, sessions) do
|
||||
regular?(file, file_info) and not file_running?(file, sessions) and
|
||||
writable?(file_info)
|
||||
end
|
||||
|
||||
|
|
@ -355,8 +368,8 @@ defmodule LivebookWeb.HomeLive do
|
|||
file_info.access in [:read_write, :write]
|
||||
end
|
||||
|
||||
defp file_running?(file, session_summaries) do
|
||||
running_files = files(session_summaries)
|
||||
defp file_running?(file, sessions) do
|
||||
running_files = files(sessions)
|
||||
file in running_files
|
||||
end
|
||||
|
||||
|
|
@ -366,8 +379,8 @@ defmodule LivebookWeb.HomeLive do
|
|||
end
|
||||
end
|
||||
|
||||
defp session_id_by_file(file, session_summaries) do
|
||||
summary = Enum.find(session_summaries, &(&1.file == file))
|
||||
summary.session_id
|
||||
defp session_id_by_file(file, sessions) do
|
||||
session = Enum.find(sessions, &(&1.file == file))
|
||||
session.id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
defmodule LivebookWeb.HomeLive.CloseSessionComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.SessionSupervisor
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -12,7 +10,7 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do
|
|||
</h3>
|
||||
<p class="text-gray-700">
|
||||
Are you sure you want to close this section -
|
||||
<span class="font-semibold">“<%= @session_summary.notebook_name %>”</span>?
|
||||
<span class="font-semibold">“<%= @session.notebook_name %>”</span>?
|
||||
This won't delete any persisted files.
|
||||
</p>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
|
|
@ -28,7 +26,7 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do
|
|||
|
||||
@impl true
|
||||
def handle_event("close", %{}, socket) do
|
||||
SessionSupervisor.close_session(socket.assigns.session_summary.session_id)
|
||||
Livebook.Session.close(socket.assigns.session.pid)
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule LivebookWeb.SessionHelpers do
|
|||
Creates a new session, redirects on success,
|
||||
puts an error flash message on failure.
|
||||
|
||||
Accepts the same options as `Livebook.SessionSupervisor.create_session/1`.
|
||||
Accepts the same options as `Livebook.Sessions.create_session/1`.
|
||||
"""
|
||||
@spec create_session(Phoenix.LiveView.Socket.t(), keyword()) :: Phoenix.LiveView.Socket.t()
|
||||
def create_session(socket, opts \\ []) do
|
||||
|
|
@ -19,9 +19,9 @@ defmodule LivebookWeb.SessionHelpers do
|
|||
opts
|
||||
end
|
||||
|
||||
case Livebook.SessionSupervisor.create_session(opts) do
|
||||
{:ok, id} ->
|
||||
push_redirect(socket, to: Routes.session_path(socket, :page, id))
|
||||
case Livebook.Sessions.create_session(opts) do
|
||||
{:ok, session} ->
|
||||
push_redirect(socket, to: Routes.session_path(socket, :page, session.id))
|
||||
|
||||
{:error, reason} ->
|
||||
put_flash(socket, :error, "Failed to create session: #{reason}")
|
||||
|
|
|
|||
|
|
@ -6,49 +6,52 @@ defmodule LivebookWeb.SessionLive do
|
|||
import Livebook.Utils, only: [access_by_id: 1]
|
||||
|
||||
alias LivebookWeb.SidebarHelpers
|
||||
alias Livebook.{SessionSupervisor, Session, Delta, Notebook, Runtime, LiveMarkdown, FileSystem}
|
||||
alias Livebook.{Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown, FileSystem}
|
||||
alias Livebook.Notebook.Cell
|
||||
alias Livebook.JSInterop
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => session_id}, %{"current_user_id" => current_user_id} = session, socket) do
|
||||
if SessionSupervisor.session_exists?(session_id) do
|
||||
current_user = build_current_user(session, socket)
|
||||
def mount(%{"id" => session_id}, %{"current_user_id" => current_user_id} = web_session, socket) do
|
||||
# We use the tracked sessions to locate the session pid, but then
|
||||
# we talk to the session process exclusively for getting all the information
|
||||
case Sessions.fetch_session(session_id) do
|
||||
{:ok, %{pid: session_pid}} ->
|
||||
current_user = build_current_user(web_session, socket)
|
||||
|
||||
data =
|
||||
if connected?(socket) do
|
||||
data = Session.register_client(session_id, self(), current_user)
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "users:#{current_user_id}")
|
||||
data =
|
||||
if connected?(socket) do
|
||||
data = Session.register_client(session_pid, self(), current_user)
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "users:#{current_user_id}")
|
||||
|
||||
data
|
||||
else
|
||||
Session.get_data(session_id)
|
||||
end
|
||||
data
|
||||
else
|
||||
Session.get_data(session_pid)
|
||||
end
|
||||
|
||||
session_pid = Session.get_pid(session_id)
|
||||
session = Session.get_by_pid(session_pid)
|
||||
|
||||
platform = platform_from_socket(socket)
|
||||
platform = platform_from_socket(socket)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
platform: platform,
|
||||
session_id: session_id,
|
||||
session_pid: session_pid,
|
||||
current_user: current_user,
|
||||
self: self(),
|
||||
data_view: data_to_view(data),
|
||||
autofocus_cell_id: autofocus_cell_id(data.notebook)
|
||||
)
|
||||
|> assign_private(data: data)
|
||||
|> allow_upload(:cell_image,
|
||||
accept: ~w(.jpg .jpeg .png .gif),
|
||||
max_entries: 1,
|
||||
max_file_size: 5_000_000
|
||||
)}
|
||||
else
|
||||
{:ok, redirect(socket, to: Routes.home_path(socket, :page))}
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
session: session,
|
||||
platform: platform,
|
||||
current_user: current_user,
|
||||
self: self(),
|
||||
data_view: data_to_view(data),
|
||||
autofocus_cell_id: autofocus_cell_id(data.notebook)
|
||||
)
|
||||
|> assign_private(data: data)
|
||||
|> allow_upload(:cell_image,
|
||||
accept: ~w(.jpg .jpeg .png .gif),
|
||||
max_entries: 1,
|
||||
max_file_size: 5_000_000
|
||||
)}
|
||||
|
||||
:error ->
|
||||
{:ok, redirect(socket, to: Routes.home_path(socket, :page))}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -90,22 +93,22 @@ defmodule LivebookWeb.SessionLive do
|
|||
<SidebarHelpers.link_item
|
||||
icon="cpu-line"
|
||||
label="Runtime settings (sr)"
|
||||
path={Routes.session_path(@socket, :runtime_settings, @session_id)}
|
||||
path={Routes.session_path(@socket, :runtime_settings, @session.id)}
|
||||
active={@live_action == :runtime_settings} />
|
||||
<SidebarHelpers.link_item
|
||||
icon="delete-bin-6-fill"
|
||||
label="Bin (sb)"
|
||||
path={Routes.session_path(@socket, :bin, @session_id)}
|
||||
path={Routes.session_path(@socket, :bin, @session.id)}
|
||||
active={@live_action == :bin} />
|
||||
<SidebarHelpers.break_item />
|
||||
<SidebarHelpers.link_item
|
||||
icon="keyboard-box-fill"
|
||||
label="Keyboard shortcuts (?)"
|
||||
path={Routes.session_path(@socket, :shortcuts, @session_id)}
|
||||
path={Routes.session_path(@socket, :shortcuts, @session.id)}
|
||||
active={@live_action == :shortcuts} />
|
||||
<SidebarHelpers.user_item
|
||||
current_user={@current_user}
|
||||
path={Routes.session_path(@socket, :user, @session_id)} />
|
||||
path={Routes.session_path(@socket, :user, @session.id)} />
|
||||
</SidebarHelpers.sidebar>
|
||||
<div class="flex flex-col h-full w-full max-w-xs absolute z-30 top-0 left-[64px] overflow-y-auto shadow-xl md:static md:shadow-none bg-gray-50 border-r border-gray-100 px-6 py-10"
|
||||
data-element="side-panel">
|
||||
|
|
@ -201,17 +204,17 @@ defmodule LivebookWeb.SessionLive do
|
|||
<span class="font-medium">Fork</span>
|
||||
</button>
|
||||
<a class="menu__item text-gray-500"
|
||||
href={live_dashboard_process_path(@socket, @session_pid)}
|
||||
href={live_dashboard_process_path(@socket, @session.pid)}
|
||||
target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" />
|
||||
<span class="font-medium">See on Dashboard</span>
|
||||
</a>
|
||||
<%= live_patch to: Routes.session_path(@socket, :export, @session_id, "livemd"),
|
||||
<%= live_patch to: Routes.session_path(@socket, :export, @session.id, "livemd"),
|
||||
class: "menu__item text-gray-500" do %>
|
||||
<.remix_icon icon="download-2-line" />
|
||||
<span class="font-medium">Export</span>
|
||||
<% end %>
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, @session_id),
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, @session.id),
|
||||
class: "menu__item text-red-600" do %>
|
||||
<.remix_icon icon="close-circle-line" />
|
||||
<span class="font-medium">Close</span>
|
||||
|
|
@ -232,7 +235,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
<%= live_component LivebookWeb.SessionLive.SectionComponent,
|
||||
id: section_view.id,
|
||||
index: index,
|
||||
session_id: @session_id,
|
||||
session_id: @session.id,
|
||||
section_view: section_view %>
|
||||
<% end %>
|
||||
<div style="height: 80vh"></div>
|
||||
|
|
@ -241,7 +244,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
</div>
|
||||
<div class="fixed bottom-[0.4rem] right-[1.5rem]">
|
||||
<%= live_component LivebookWeb.SessionLive.IndicatorsComponent,
|
||||
session_id: @session_id,
|
||||
session_id: @session.id,
|
||||
file: @data_view.file,
|
||||
dirty: @data_view.dirty,
|
||||
autosave_interval_s: @data_view.autosave_interval_s,
|
||||
|
|
@ -255,15 +258,15 @@ defmodule LivebookWeb.SessionLive do
|
|||
id: "user",
|
||||
modal_class: "w-full max-w-sm",
|
||||
user: @current_user,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
return_to: Routes.session_path(@socket, :page, @session.id) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :runtime_settings do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.RuntimeComponent,
|
||||
id: "runtime-settings",
|
||||
modal_class: "w-full max-w-4xl",
|
||||
return_to: Routes.session_path(@socket, :page, @session_id),
|
||||
session_id: @session_id,
|
||||
return_to: Routes.session_path(@socket, :page, @session.id),
|
||||
session: @session,
|
||||
runtime: @data_view.runtime %>
|
||||
<% end %>
|
||||
|
||||
|
|
@ -271,9 +274,9 @@ defmodule LivebookWeb.SessionLive do
|
|||
<%= live_modal @socket, LivebookWeb.SessionLive.PersistenceLive,
|
||||
id: "persistence",
|
||||
modal_class: "w-full max-w-4xl",
|
||||
return_to: Routes.session_path(@socket, :page, @session_id),
|
||||
return_to: Routes.session_path(@socket, :page, @session.id),
|
||||
session: %{
|
||||
"session_id" => @session_id,
|
||||
"session" => @session,
|
||||
"file" => @data_view.file,
|
||||
"persist_outputs" => @data_view.persist_outputs,
|
||||
"autosave_interval_s" => @data_view.autosave_interval_s
|
||||
|
|
@ -285,54 +288,54 @@ defmodule LivebookWeb.SessionLive do
|
|||
id: "shortcuts",
|
||||
modal_class: "w-full max-w-6xl",
|
||||
platform: @platform,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
return_to: Routes.session_path(@socket, :page, @session.id) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :cell_settings do %>
|
||||
<%= live_modal settings_component_for(@cell),
|
||||
id: "cell-settings",
|
||||
modal_class: "w-full max-w-xl",
|
||||
session_id: @session_id,
|
||||
session: @session,
|
||||
cell: @cell,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
return_to: Routes.session_path(@socket, :page, @session.id) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :cell_upload do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.CellUploadComponent,
|
||||
id: "cell-upload",
|
||||
modal_class: "w-full max-w-xl",
|
||||
session_id: @session_id,
|
||||
session: @session,
|
||||
cell: @cell,
|
||||
uploads: @uploads,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
return_to: Routes.session_path(@socket, :page, @session.id) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :delete_section do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.DeleteSectionComponent,
|
||||
id: "delete-section",
|
||||
modal_class: "w-full max-w-xl",
|
||||
session_id: @session_id,
|
||||
session: @session,
|
||||
section: @section,
|
||||
is_first: @section.id == @first_section_id,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
return_to: Routes.session_path(@socket, :page, @session.id) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :bin do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.BinComponent,
|
||||
id: "bin",
|
||||
modal_class: "w-full max-w-4xl",
|
||||
session_id: @session_id,
|
||||
session: @session,
|
||||
bin_entries: @data_view.bin_entries,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
return_to: Routes.session_path(@socket, :page, @session.id) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :export do %>
|
||||
<%= live_modal LivebookWeb.SessionLive.ExportComponent,
|
||||
id: "export",
|
||||
modal_class: "w-full max-w-4xl",
|
||||
session_id: @session_id,
|
||||
session: @session,
|
||||
tab: @tab,
|
||||
return_to: Routes.session_path(@socket, :page, @session_id) %>
|
||||
return_to: Routes.session_path(@socket, :page, @session.id) %>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
|
@ -427,14 +430,14 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
def handle_event("append_section", %{}, socket) do
|
||||
idx = length(socket.private.data.notebook.sections)
|
||||
Session.insert_section(socket.assigns.session_id, idx)
|
||||
Session.insert_section(socket.assigns.session.pid, idx)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("insert_section_into", %{"section_id" => section_id, "index" => index}, socket) do
|
||||
index = ensure_integer(index) |> max(0)
|
||||
Session.insert_section_into(socket.assigns.session_id, section_id, index)
|
||||
Session.insert_section_into(socket.assigns.session.pid, section_id, index)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -444,13 +447,13 @@ defmodule LivebookWeb.SessionLive do
|
|||
%{"section_id" => section_id, "parent_id" => parent_id},
|
||||
socket
|
||||
) do
|
||||
Session.set_section_parent(socket.assigns.session_id, section_id, parent_id)
|
||||
Session.set_section_parent(socket.assigns.session.pid, section_id, parent_id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("unset_section_parent", %{"section_id" => section_id}, socket) do
|
||||
Session.unset_section_parent(socket.assigns.session_id, section_id)
|
||||
Session.unset_section_parent(socket.assigns.session.pid, section_id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -462,7 +465,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
) do
|
||||
index = ensure_integer(index) |> max(0)
|
||||
type = String.to_atom(type)
|
||||
Session.insert_cell(socket.assigns.session_id, section_id, index, type)
|
||||
Session.insert_cell(socket.assigns.session.pid, section_id, index, type)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -482,21 +485,21 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
|
||||
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
|
||||
Session.delete_cell(socket.assigns.session_id, cell_id)
|
||||
Session.delete_cell(socket.assigns.session.pid, cell_id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("set_notebook_name", %{"name" => name}, socket) do
|
||||
name = normalize_name(name)
|
||||
Session.set_notebook_name(socket.assigns.session_id, name)
|
||||
Session.set_notebook_name(socket.assigns.session.pid, name)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("set_section_name", %{"section_id" => section_id, "name" => name}, socket) do
|
||||
name = normalize_name(name)
|
||||
Session.set_section_name(socket.assigns.session_id, section_id, name)
|
||||
Session.set_section_name(socket.assigns.session.pid, section_id, name)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -507,7 +510,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
socket
|
||||
) do
|
||||
delta = Delta.from_compressed(delta)
|
||||
Session.apply_cell_delta(socket.assigns.session_id, cell_id, delta, revision)
|
||||
Session.apply_cell_delta(socket.assigns.session.pid, cell_id, delta, revision)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -517,7 +520,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
%{"cell_id" => cell_id, "revision" => revision},
|
||||
socket
|
||||
) do
|
||||
Session.report_cell_revision(socket.assigns.session_id, cell_id, revision)
|
||||
Session.report_cell_revision(socket.assigns.session.pid, cell_id, revision)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -527,34 +530,34 @@ defmodule LivebookWeb.SessionLive do
|
|||
# to more closely imitate an actual shell
|
||||
value = String.replace(value, "\r\n", "\n")
|
||||
|
||||
Session.set_cell_attributes(socket.assigns.session_id, cell_id, %{value: value})
|
||||
Session.set_cell_attributes(socket.assigns.session.pid, cell_id, %{value: value})
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("move_cell", %{"cell_id" => cell_id, "offset" => offset}, socket) do
|
||||
offset = ensure_integer(offset)
|
||||
Session.move_cell(socket.assigns.session_id, cell_id, offset)
|
||||
Session.move_cell(socket.assigns.session.pid, cell_id, offset)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("move_section", %{"section_id" => section_id, "offset" => offset}, socket) do
|
||||
offset = ensure_integer(offset)
|
||||
Session.move_section(socket.assigns.session_id, section_id, offset)
|
||||
Session.move_section(socket.assigns.session.pid, section_id, offset)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
||||
Session.queue_cell_evaluation(socket.assigns.session_id, cell_id)
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, cell_id)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("queue_section_cells_evaluation", %{"section_id" => section_id}, socket) do
|
||||
with {:ok, section} <- Notebook.fetch_section(socket.private.data.notebook, section_id) do
|
||||
for cell <- section.cells, is_struct(cell, Cell.Elixir) do
|
||||
Session.queue_cell_evaluation(socket.assigns.session_id, cell.id)
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, cell.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -566,7 +569,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
for {cell, _} <- Notebook.elixir_cells_with_section(data.notebook),
|
||||
data.cell_infos[cell.id].validity_status != :evaluated do
|
||||
Session.queue_cell_evaluation(socket.assigns.session_id, cell.id)
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, cell.id)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
|
|
@ -577,7 +580,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
Notebook.fetch_cell_and_section(socket.private.data.notebook, cell_id) do
|
||||
for {cell, _} <- Notebook.child_cells_with_section(socket.private.data.notebook, cell.id),
|
||||
is_struct(cell, Cell.Elixir) do
|
||||
Session.queue_cell_evaluation(socket.assigns.session_id, cell.id)
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, cell.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -589,7 +592,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
with {:ok, cell, _section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id) do
|
||||
for {bound_cell, _} <- Session.Data.bound_cells_with_section(data, cell.id) do
|
||||
Session.queue_cell_evaluation(socket.assigns.session_id, bound_cell.id)
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, bound_cell.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -597,38 +600,38 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
|
||||
def handle_event("cancel_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
||||
Session.cancel_cell_evaluation(socket.assigns.session_id, cell_id)
|
||||
Session.cancel_cell_evaluation(socket.assigns.session.pid, cell_id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("save", %{}, socket) do
|
||||
if socket.private.data.file do
|
||||
Session.save(socket.assigns.session_id)
|
||||
Session.save(socket.assigns.session.pid)
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:noreply,
|
||||
push_patch(socket,
|
||||
to: Routes.session_path(socket, :file_settings, socket.assigns.session_id)
|
||||
to: Routes.session_path(socket, :file_settings, socket.assigns.session.id)
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("show_shortcuts", %{}, socket) do
|
||||
{:noreply,
|
||||
push_patch(socket, to: Routes.session_path(socket, :shortcuts, socket.assigns.session_id))}
|
||||
push_patch(socket, to: Routes.session_path(socket, :shortcuts, socket.assigns.session.id))}
|
||||
end
|
||||
|
||||
def handle_event("show_runtime_settings", %{}, socket) do
|
||||
{:noreply,
|
||||
push_patch(socket,
|
||||
to: Routes.session_path(socket, :runtime_settings, socket.assigns.session_id)
|
||||
to: Routes.session_path(socket, :runtime_settings, socket.assigns.session.id)
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("show_bin", %{}, socket) do
|
||||
{:noreply,
|
||||
push_patch(socket, to: Routes.session_path(socket, :bin, socket.assigns.session_id))}
|
||||
push_patch(socket, to: Routes.session_path(socket, :bin, socket.assigns.session.id))}
|
||||
end
|
||||
|
||||
def handle_event("restart_runtime", %{}, socket) do
|
||||
|
|
@ -636,7 +639,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
if runtime = socket.private.data.runtime do
|
||||
case Runtime.duplicate(runtime) do
|
||||
{:ok, new_runtime} ->
|
||||
Session.connect_runtime(socket.assigns.session_id, new_runtime)
|
||||
Session.connect_runtime(socket.assigns.session.pid, new_runtime)
|
||||
socket
|
||||
|
||||
{:error, message} ->
|
||||
|
|
@ -694,10 +697,10 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
|
||||
def handle_event("fork_session", %{}, socket) do
|
||||
%{pid: pid, images_dir: images_dir} = socket.assigns.session
|
||||
# Fetch the data, as we don't keep cells' source in the state
|
||||
data = Session.get_data(socket.assigns.session_id)
|
||||
data = Session.get_data(pid)
|
||||
notebook = Notebook.forked(data.notebook)
|
||||
%{images_dir: images_dir} = Session.get_summary(socket.assigns.session_id)
|
||||
{:noreply, create_session(socket, notebook: notebook, copy_images_from: images_dir)}
|
||||
end
|
||||
|
||||
|
|
@ -705,7 +708,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
Phoenix.PubSub.broadcast_from(
|
||||
Livebook.PubSub,
|
||||
self(),
|
||||
"sessions:#{socket.assigns.session_id}",
|
||||
"sessions:#{socket.assigns.session.id}",
|
||||
{:location_report, self(), report}
|
||||
)
|
||||
|
||||
|
|
@ -774,6 +777,10 @@ defmodule LivebookWeb.SessionLive do
|
|||
|> assign(data_view: data_to_view(data))}
|
||||
end
|
||||
|
||||
def handle_info({:session_updated, session}, socket) do
|
||||
{:noreply, assign(socket, :session, session)}
|
||||
end
|
||||
|
||||
def handle_info(:session_closed, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|
|
@ -902,22 +909,22 @@ defmodule LivebookWeb.SessionLive do
|
|||
defp file_and_notebook(_fork?, _origin, notebook), do: {nil, notebook}
|
||||
|
||||
defp session_id_by_location(location) do
|
||||
session_summaries = SessionSupervisor.get_session_summaries()
|
||||
sessions = Sessions.list_sessions()
|
||||
|
||||
session_with_file =
|
||||
Enum.find(session_summaries, fn summary ->
|
||||
summary.file && {:file, summary.file} == location
|
||||
Enum.find(sessions, fn session ->
|
||||
session.file && {:file, session.file} == location
|
||||
end)
|
||||
|
||||
# A session associated with the given file takes
|
||||
# precedence over sessions originating from this file
|
||||
if session_with_file do
|
||||
{:ok, session_with_file.session_id}
|
||||
{:ok, session_with_file.id}
|
||||
else
|
||||
session_summaries
|
||||
|> Enum.filter(fn summary -> summary.origin == location end)
|
||||
sessions
|
||||
|> Enum.filter(fn session -> session.origin == location end)
|
||||
|> case do
|
||||
[summary] -> {:ok, summary.session_id}
|
||||
[session] -> {:ok, session.id}
|
||||
[] -> {:error, :none}
|
||||
_ -> {:error, :many}
|
||||
end
|
||||
|
|
@ -925,7 +932,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
|
||||
defp redirect_to_self(socket) do
|
||||
push_patch(socket, to: Routes.session_path(socket, :page, socket.assigns.session_id))
|
||||
push_patch(socket, to: Routes.session_path(socket, :page, socket.assigns.session.id))
|
||||
end
|
||||
|
||||
defp after_operation(socket, _prev_socket, {:client_join, client_pid, user}) do
|
||||
|
|
@ -978,7 +985,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
case type do
|
||||
:input ->
|
||||
push_patch(socket,
|
||||
to: Routes.session_path(socket, :cell_settings, socket.assigns.session_id, cell_id)
|
||||
to: Routes.session_path(socket, :cell_settings, socket.assigns.session.id, cell_id)
|
||||
)
|
||||
|
||||
_ ->
|
||||
|
|
@ -1079,7 +1086,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
defp insert_cell_next_to(socket, cell_id, type, idx_offset: idx_offset) do
|
||||
{:ok, cell, section} = Notebook.fetch_cell_and_section(socket.private.data.notebook, cell_id)
|
||||
index = Enum.find_index(section.cells, &(&1 == cell))
|
||||
Session.insert_cell(socket.assigns.session_id, section.id, index + idx_offset, type)
|
||||
Session.insert_cell(socket.assigns.session.pid, section.id, index + idx_offset, type)
|
||||
end
|
||||
|
||||
defp ensure_integer(n) when is_integer(n), do: n
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
alias Livebook.{Session, Runtime, Utils}
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"session_id" => session_id, "current_runtime" => current_runtime}, socket) do
|
||||
def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do
|
||||
{:ok,
|
||||
assign(socket,
|
||||
session_id: session_id,
|
||||
session: session,
|
||||
error_message: nil,
|
||||
data: initial_data(current_runtime)
|
||||
)}
|
||||
|
|
@ -75,7 +75,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
|
||||
case Runtime.Attached.init(node, cookie) do
|
||||
{:ok, runtime} ->
|
||||
Session.connect_runtime(socket.assigns.session_id, runtime)
|
||||
Session.connect_runtime(socket.assigns.session.pid, runtime)
|
||||
{:noreply, assign(socket, data: data, error_message: nil)}
|
||||
|
||||
{:error, error} ->
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ defmodule LivebookWeb.SessionLive.BinComponent do
|
|||
end
|
||||
|
||||
def handle_event("restore", %{"cell_id" => cell_id}, socket) do
|
||||
Livebook.Session.restore_cell(socket.assigns.session_id, cell_id)
|
||||
Livebook.Session.restore_cell(socket.assigns.session.pid, cell_id)
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule LivebookWeb.SessionLive.CellUploadComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.{Session, FileSystem}
|
||||
alias Livebook.FileSystem
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
|
|
@ -71,7 +71,7 @@ defmodule LivebookWeb.SessionLive.CellUploadComponent do
|
|||
end
|
||||
|
||||
def handle_event("save", %{"name" => name}, socket) do
|
||||
%{images_dir: images_dir} = Session.get_summary(socket.assigns.session_id)
|
||||
%{images_dir: images_dir} = socket.assigns.session
|
||||
|
||||
consume_uploaded_entries(socket, :cell_image, fn %{path: path}, entry ->
|
||||
# Ensure the path is normalized (see https://github.com/elixir-plug/plug/issues/1047)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ defmodule LivebookWeb.SessionLive.DeleteSectionComponent do
|
|||
delete_cells? = delete_cells == "true"
|
||||
|
||||
Livebook.Session.delete_section(
|
||||
socket.assigns.session_id,
|
||||
socket.assigns.session.pid,
|
||||
socket.assigns.section.id,
|
||||
delete_cells?
|
||||
)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do
|
|||
def handle_event("save", %{"disable_formatting" => disable_formatting}, socket) do
|
||||
disable_formatting = disable_formatting == "true"
|
||||
|
||||
Session.set_cell_attributes(socket.assigns.session_id, socket.assigns.cell.id, %{
|
||||
Session.set_cell_attributes(socket.assigns.session.pid, socket.assigns.cell.id, %{
|
||||
disable_formatting: disable_formatting
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
|
|||
alias Livebook.{Session, Runtime}
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"session_id" => session_id, "current_runtime" => current_runtime}, socket) do
|
||||
def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket, session_id: session_id, current_runtime: current_runtime, error_message: nil)}
|
||||
{:ok, assign(socket, session: session, current_runtime: current_runtime, error_message: nil)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -41,7 +40,7 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
|
|||
def handle_event("init", _params, socket) do
|
||||
case Runtime.ElixirStandalone.init() do
|
||||
{:ok, runtime} ->
|
||||
Session.connect_runtime(socket.assigns.session_id, runtime)
|
||||
Session.connect_runtime(socket.assigns.session.pid, runtime)
|
||||
{:noreply, assign(socket, error_message: nil)}
|
||||
|
||||
{:error, message} ->
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
|
|||
alias Livebook.{Session, Runtime}
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"session_id" => session_id}, socket) do
|
||||
{:ok, assign(socket, session_id: session_id)}
|
||||
def mount(_params, %{"session" => session}, socket) do
|
||||
{:ok, assign(socket, session: session)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -35,7 +35,7 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
|
|||
@impl true
|
||||
def handle_event("init", _params, socket) do
|
||||
{:ok, runtime} = Runtime.Embedded.init()
|
||||
Session.connect_runtime(socket.assigns.session_id, runtime)
|
||||
Session.connect_runtime(socket.assigns.session.pid, runtime)
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ defmodule LivebookWeb.SessionLive.ExportComponent do
|
|||
else
|
||||
# Note: we need to load the notebook, because the local data
|
||||
# has cell contents stripped out
|
||||
notebook = Session.get_notebook(socket.assigns.session_id)
|
||||
notebook = Session.get_notebook(socket.assigns.session.pid)
|
||||
assign(socket, :notebook, notebook)
|
||||
end
|
||||
|
||||
|
|
@ -32,13 +32,13 @@ defmodule LivebookWeb.SessionLive.ExportComponent do
|
|||
Here you can preview and directly export the notebook source.
|
||||
</p>
|
||||
<div class="tabs">
|
||||
<%= live_patch to: Routes.session_path(@socket, :export, @session_id, "livemd"),
|
||||
<%= live_patch to: Routes.session_path(@socket, :export, @session.id, "livemd"),
|
||||
class: "tab #{if(@tab == "livemd", do: "active")}" do %>
|
||||
<span class="font-medium">
|
||||
Live Markdown
|
||||
</span>
|
||||
<% end %>
|
||||
<%= live_patch to: Routes.session_path(@socket, :export, @session_id, "exs"),
|
||||
<%= live_patch to: Routes.session_path(@socket, :export, @session.id, "exs"),
|
||||
class: "tab #{if(@tab == "exs", do: "active")}" do %>
|
||||
<span class="font-medium">
|
||||
Elixir Script
|
||||
|
|
@ -46,11 +46,11 @@ defmodule LivebookWeb.SessionLive.ExportComponent do
|
|||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<%= live_component component_for_tab(@tab),
|
||||
id: "export-notebook-#{@tab}",
|
||||
session_id: @session_id,
|
||||
notebook: @notebook %>
|
||||
</div>
|
||||
<%= live_component component_for_tab(@tab),
|
||||
id: "export-notebook-#{@tab}",
|
||||
session: @session,
|
||||
notebook: @notebook %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ defmodule LivebookWeb.SessionLive.ExportElixirComponent do
|
|||
</span>
|
||||
<span class="tooltip left" aria-label="Download source">
|
||||
<a class="icon-button"
|
||||
href={Routes.session_path(@socket, :download_source, @session_id, "exs")}>
|
||||
href={Routes.session_path(@socket, :download_source, @session.id, "exs")}>
|
||||
<.remix_icon icon="download-2-line" class="text-lg" />
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do
|
|||
</span>
|
||||
<span class="tooltip left" aria-label="Download source">
|
||||
<a class="icon-button"
|
||||
href={Routes.session_path(@socket, :download_source, @session_id, "livemd", include_outputs: @include_outputs)}>
|
||||
href={Routes.session_path(@socket, :download_source, @session.id, "livemd", include_outputs: @include_outputs)}>
|
||||
<.remix_icon icon="download-2-line" class="text-lg" />
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
attrs
|
||||
end
|
||||
|
||||
Session.set_cell_attributes(socket.assigns.session_id, socket.assigns.cell.id, attrs)
|
||||
Session.set_cell_attributes(socket.assigns.session.pid, socket.assigns.cell.id, attrs)
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
|||
@type status :: :initial | :initializing | :finished
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"session_id" => session_id, "current_runtime" => current_runtime}, socket) do
|
||||
def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
session_id: session_id,
|
||||
session: session,
|
||||
status: :initial,
|
||||
current_runtime: current_runtime,
|
||||
file: initial_file(current_runtime),
|
||||
|
|
@ -84,7 +84,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
|||
{:noreply, add_output(socket, output)}
|
||||
|
||||
{:ok, runtime} ->
|
||||
Session.connect_runtime(socket.assigns.session_id, runtime)
|
||||
Session.connect_runtime(socket.assigns.session.pid, runtime)
|
||||
{:noreply, socket |> assign(status: :finished) |> add_output("Connected successfully")}
|
||||
|
||||
{:error, error} ->
|
||||
|
|
|
|||
|
|
@ -6,21 +6,21 @@ defmodule LivebookWeb.SessionLive.PersistenceLive do
|
|||
# the parent live view.
|
||||
use LivebookWeb, :live_view
|
||||
|
||||
alias Livebook.{Session, SessionSupervisor, LiveMarkdown, FileSystem}
|
||||
alias Livebook.{Sessions, Session, LiveMarkdown, FileSystem}
|
||||
|
||||
@impl true
|
||||
def mount(
|
||||
_params,
|
||||
%{
|
||||
"session_id" => session_id,
|
||||
"session" => session,
|
||||
"file" => file,
|
||||
"persist_outputs" => persist_outputs,
|
||||
"autosave_interval_s" => autosave_interval_s
|
||||
},
|
||||
socket
|
||||
) do
|
||||
session_summaries = SessionSupervisor.get_session_summaries()
|
||||
running_files = Enum.map(session_summaries, & &1.file)
|
||||
sessions = Sessions.list_sessions()
|
||||
running_files = Enum.map(sessions, & &1.file)
|
||||
|
||||
attrs = %{
|
||||
file: file,
|
||||
|
|
@ -30,7 +30,7 @@ defmodule LivebookWeb.SessionLive.PersistenceLive do
|
|||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
session_id: session_id,
|
||||
session: session,
|
||||
running_files: running_files,
|
||||
attrs: attrs,
|
||||
new_attrs: attrs
|
||||
|
|
@ -152,20 +152,20 @@ defmodule LivebookWeb.SessionLive.PersistenceLive do
|
|||
autosave_interval_s = assigns.new_attrs.autosave_interval_s
|
||||
|
||||
if file != assigns.attrs.file do
|
||||
Session.set_file(assigns.session_id, file)
|
||||
Session.set_file(assigns.session.pid, file)
|
||||
end
|
||||
|
||||
if autosave_interval_s != assigns.attrs.autosave_interval_s do
|
||||
Session.set_notebook_attributes(assigns.session_id, %{
|
||||
Session.set_notebook_attributes(assigns.session.pid, %{
|
||||
autosave_interval_s: autosave_interval_s
|
||||
})
|
||||
end
|
||||
|
||||
Session.set_notebook_attributes(assigns.session_id, %{
|
||||
Session.set_notebook_attributes(assigns.session.pid, %{
|
||||
persist_outputs: assigns.new_attrs.persist_outputs
|
||||
})
|
||||
|
||||
Session.save_sync(assigns.session_id)
|
||||
Session.save_sync(assigns.session.pid)
|
||||
|
||||
running_files =
|
||||
if file do
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
|||
<div>
|
||||
<%= live_render @socket, live_view_for_type(@type),
|
||||
id: "runtime-config-#{@type}",
|
||||
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
|
||||
session: %{"session" => @session, "current_runtime" => @runtime} %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -116,7 +116,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
|||
end
|
||||
|
||||
def handle_event("disconnect", _params, socket) do
|
||||
Session.disconnect_runtime(socket.assigns.session_id)
|
||||
Session.disconnect_runtime(socket.assigns.session.pid)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
defmodule Livebook.SessionSupervisorTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Livebook.SessionSupervisor
|
||||
|
||||
describe "create_session/0" do
|
||||
test "creates a new session process and returns its id" do
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
{:ok, pid} = SessionSupervisor.get_session_pid(id)
|
||||
|
||||
assert has_child_with_pid?(SessionSupervisor, pid)
|
||||
end
|
||||
|
||||
test "broadcasts a message" do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions")
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
|
||||
assert_receive {:session_created, ^id}
|
||||
end
|
||||
end
|
||||
|
||||
describe "close_session/1" do
|
||||
test "stops the session process identified by the given id" do
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
{:ok, pid} = SessionSupervisor.get_session_pid(id)
|
||||
ref = Process.monitor(pid)
|
||||
|
||||
SessionSupervisor.close_session(id)
|
||||
|
||||
assert_receive {:DOWN, ^ref, :process, _, _}
|
||||
refute has_child_with_pid?(SessionSupervisor, pid)
|
||||
end
|
||||
|
||||
test "broadcasts a message" do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions")
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
|
||||
SessionSupervisor.close_session(id)
|
||||
|
||||
assert_receive {:session_closed, ^id}
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_session_ids/0" do
|
||||
test "lists ids identifying sessions running under the supervisor" do
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
|
||||
assert id in SessionSupervisor.get_session_ids()
|
||||
end
|
||||
end
|
||||
|
||||
describe "session_exists?/1" do
|
||||
test "returns true if a session process with the given id exists" do
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
|
||||
assert SessionSupervisor.session_exists?(id)
|
||||
end
|
||||
|
||||
test "returns false if a session process with the given id does not exist" do
|
||||
refute SessionSupervisor.session_exists?("nonexistent")
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_session_pid/1" do
|
||||
test "returns pid if a session process with the given id is running" do
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
|
||||
assert {:ok, pid} = SessionSupervisor.get_session_pid(id)
|
||||
assert :global.whereis_name({:session, id}) == pid
|
||||
end
|
||||
|
||||
test "returns an error if a session process with the given id does not exist" do
|
||||
assert {:error, :nonexistent} = SessionSupervisor.get_session_pid("nonexistent")
|
||||
end
|
||||
end
|
||||
|
||||
defp has_child_with_pid?(supervisor, pid) do
|
||||
List.keymember?(DynamicSupervisor.which_children(supervisor), pid, 1)
|
||||
end
|
||||
end
|
||||
|
|
@ -10,101 +10,101 @@ defmodule Livebook.SessionTest do
|
|||
@evaluation_wait_timeout 3_000
|
||||
|
||||
setup do
|
||||
session_id = start_session()
|
||||
%{session_id: session_id}
|
||||
session = start_session()
|
||||
%{session: session}
|
||||
end
|
||||
|
||||
describe "set_notebook_attributes/2" do
|
||||
test "sends an attributes update to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends an attributes update to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
attrs = %{set_notebook_attributes: true}
|
||||
Session.set_notebook_attributes(session_id, attrs)
|
||||
Session.set_notebook_attributes(session.pid, attrs)
|
||||
assert_receive {:operation, {:set_notebook_attributes, ^pid, ^attrs}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "insert_section/2" do
|
||||
test "sends an insert opreation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends an insert opreation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
Session.insert_section(session_id, 0)
|
||||
Session.insert_section(session.pid, 0)
|
||||
assert_receive {:operation, {:insert_section, ^pid, 0, _id}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "insert_cell/4" do
|
||||
test "sends an insert opreation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends an insert opreation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
Session.insert_section(session_id, 0)
|
||||
Session.insert_section(session.pid, 0)
|
||||
assert_receive {:operation, {:insert_section, ^pid, 0, section_id}}
|
||||
|
||||
Session.insert_cell(session_id, section_id, 0, :elixir)
|
||||
Session.insert_cell(session.pid, section_id, 0, :elixir)
|
||||
assert_receive {:operation, {:insert_cell, ^pid, ^section_id, 0, :elixir, _id}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_section/3" do
|
||||
test "sends a delete opreation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a delete opreation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{section_id, _cell_id} = insert_section_and_cell(session_id)
|
||||
{section_id, _cell_id} = insert_section_and_cell(session.pid)
|
||||
|
||||
Session.delete_section(session_id, section_id, false)
|
||||
Session.delete_section(session.pid, section_id, false)
|
||||
assert_receive {:operation, {:delete_section, ^pid, ^section_id, false}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_cell/2" do
|
||||
test "sends a delete opreation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a delete opreation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
|
||||
Session.delete_cell(session_id, cell_id)
|
||||
Session.delete_cell(session.pid, cell_id)
|
||||
assert_receive {:operation, {:delete_cell, ^pid, ^cell_id}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "restore_cell/2" do
|
||||
test "sends a restore opreation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a restore opreation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
Session.delete_cell(session_id, cell_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
Session.delete_cell(session.pid, cell_id)
|
||||
|
||||
Session.restore_cell(session_id, cell_id)
|
||||
Session.restore_cell(session.pid, cell_id)
|
||||
assert_receive {:operation, {:restore_cell, ^pid, ^cell_id}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "queue_cell_evaluation/2" do
|
||||
test "sends a queue evaluation operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a queue evaluation operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
|
||||
Session.queue_cell_evaluation(session_id, cell_id)
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation, {:queue_cell_evaluation, ^pid, ^cell_id}},
|
||||
@evaluation_wait_timeout
|
||||
end
|
||||
|
||||
test "triggers evaluation and sends update operation once it finishes",
|
||||
%{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
%{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
|
||||
Session.queue_cell_evaluation(session_id, cell_id)
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, _,
|
||||
|
|
@ -114,14 +114,14 @@ defmodule Livebook.SessionTest do
|
|||
end
|
||||
|
||||
describe "cancel_cell_evaluation/2" do
|
||||
test "sends a cancel evaluation operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a cancel evaluation operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
Session.queue_cell_evaluation(session_id, cell_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
Session.cancel_cell_evaluation(session_id, cell_id)
|
||||
Session.cancel_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation, {:cancel_cell_evaluation, ^pid, ^cell_id}},
|
||||
@evaluation_wait_timeout
|
||||
|
|
@ -129,89 +129,89 @@ defmodule Livebook.SessionTest do
|
|||
end
|
||||
|
||||
describe "set_notebook_name/2" do
|
||||
test "sends a notebook name update operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a notebook name update operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
Session.set_notebook_name(session_id, "Cat's guide to life")
|
||||
Session.set_notebook_name(session.pid, "Cat's guide to life")
|
||||
assert_receive {:operation, {:set_notebook_name, ^pid, "Cat's guide to life"}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "set_section_name/3" do
|
||||
test "sends a section name update operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a section name update operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{section_id, _cell_id} = insert_section_and_cell(session_id)
|
||||
{section_id, _cell_id} = insert_section_and_cell(session.pid)
|
||||
|
||||
Session.set_section_name(session_id, section_id, "Chapter 1")
|
||||
Session.set_section_name(session.pid, section_id, "Chapter 1")
|
||||
assert_receive {:operation, {:set_section_name, ^pid, ^section_id, "Chapter 1"}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "apply_cell_delta/4" do
|
||||
test "sends a cell delta operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a cell delta operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
|
||||
delta = Delta.new() |> Delta.insert("cats")
|
||||
revision = 1
|
||||
|
||||
Session.apply_cell_delta(session_id, cell_id, delta, revision)
|
||||
Session.apply_cell_delta(session.pid, cell_id, delta, revision)
|
||||
assert_receive {:operation, {:apply_cell_delta, ^pid, ^cell_id, ^delta, ^revision}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "report_cell_revision/3" do
|
||||
test "sends a revision report operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a revision report operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
revision = 1
|
||||
|
||||
Session.report_cell_revision(session_id, cell_id, revision)
|
||||
Session.report_cell_revision(session.pid, cell_id, revision)
|
||||
assert_receive {:operation, {:report_cell_revision, ^pid, ^cell_id, ^revision}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "set_cell_attributes/3" do
|
||||
test "sends an attributes update operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends an attributes update operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
attrs = %{disable_formatting: true}
|
||||
|
||||
Session.set_cell_attributes(session_id, cell_id, attrs)
|
||||
Session.set_cell_attributes(session.pid, cell_id, attrs)
|
||||
assert_receive {:operation, {:set_cell_attributes, ^pid, ^cell_id, ^attrs}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "connect_runtime/2" do
|
||||
test "sends a runtime update operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a runtime update operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{:ok, runtime} = Livebook.Runtime.Embedded.init()
|
||||
Session.connect_runtime(session_id, runtime)
|
||||
Session.connect_runtime(session.pid, runtime)
|
||||
|
||||
assert_receive {:operation, {:set_runtime, ^pid, ^runtime}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "disconnect_runtime/1" do
|
||||
test "sends a runtime update operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
test "sends a runtime update operation to subscribers", %{session: session} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
{:ok, runtime} = Livebook.Runtime.Embedded.init()
|
||||
Session.connect_runtime(session_id, runtime)
|
||||
Session.connect_runtime(session.pid, runtime)
|
||||
|
||||
Session.disconnect_runtime(session_id)
|
||||
Session.disconnect_runtime(session.pid)
|
||||
|
||||
assert_receive {:operation, {:set_runtime, ^pid, nil}}
|
||||
end
|
||||
|
|
@ -220,41 +220,41 @@ defmodule Livebook.SessionTest do
|
|||
describe "set_file/1" do
|
||||
@tag :tmp_dir
|
||||
test "sends a file update operation to subscribers",
|
||||
%{session_id: session_id, tmp_dir: tmp_dir} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
%{session: session, tmp_dir: tmp_dir} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
pid = self()
|
||||
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
Session.set_file(session_id, file)
|
||||
Session.set_file(session.pid, file)
|
||||
|
||||
assert_receive {:operation, {:set_file, ^pid, ^file}}
|
||||
end
|
||||
|
||||
@tag :tmp_dir
|
||||
test "broadcasts an error if the path is already in use",
|
||||
%{session_id: session_id, tmp_dir: tmp_dir} do
|
||||
%{session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
start_session(file: file)
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
Session.set_file(session_id, file)
|
||||
Session.set_file(session.pid, file)
|
||||
|
||||
assert_receive {:error, "failed to set new file because it is already in use"}
|
||||
end
|
||||
|
||||
@tag :tmp_dir
|
||||
test "moves images to the new directory", %{session_id: session_id, tmp_dir: tmp_dir} do
|
||||
test "moves images to the new directory", %{session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
%{images_dir: images_dir} = Session.get_summary(session_id)
|
||||
%{images_dir: images_dir} = session
|
||||
|
||||
image_file = FileSystem.File.resolve(images_dir, "test.jpg")
|
||||
:ok = FileSystem.File.write(image_file, "")
|
||||
|
||||
file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
Session.set_file(session_id, file)
|
||||
Session.set_file(session.pid, file)
|
||||
|
||||
# Wait for the session to deal with the files
|
||||
Process.sleep(50)
|
||||
|
|
@ -267,23 +267,23 @@ defmodule Livebook.SessionTest do
|
|||
|
||||
@tag :tmp_dir
|
||||
test "does not remove images from the previous dir if not temporary",
|
||||
%{session_id: session_id, tmp_dir: tmp_dir} do
|
||||
%{session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
Session.set_file(session_id, file)
|
||||
Session.set_file(session.pid, file)
|
||||
|
||||
%{images_dir: images_dir} = Session.get_summary(session_id)
|
||||
%{images_dir: images_dir} = session
|
||||
image_file = FileSystem.File.resolve(images_dir, "test.jpg")
|
||||
:ok = FileSystem.File.write(image_file, "")
|
||||
|
||||
Session.set_file(session_id, nil)
|
||||
Session.set_file(session.pid, nil)
|
||||
|
||||
# Wait for the session to deal with the files
|
||||
Process.sleep(50)
|
||||
|
||||
assert {:ok, true} = FileSystem.File.exists?(image_file)
|
||||
|
||||
%{images_dir: new_images_dir} = Session.get_summary(session_id)
|
||||
%{images_dir: new_images_dir} = session
|
||||
|
||||
assert {:ok, true} =
|
||||
FileSystem.File.exists?(FileSystem.File.resolve(new_images_dir, "test.jpg"))
|
||||
|
|
@ -293,36 +293,36 @@ defmodule Livebook.SessionTest do
|
|||
describe "save/1" do
|
||||
@tag :tmp_dir
|
||||
test "persists the notebook to the associated file and notifies subscribers",
|
||||
%{session_id: session_id, tmp_dir: tmp_dir} do
|
||||
%{session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
Session.set_file(session_id, file)
|
||||
Session.set_file(session.pid, file)
|
||||
# Perform a change, so the notebook is dirty
|
||||
Session.set_notebook_name(session_id, "My notebook")
|
||||
Session.set_notebook_name(session.pid, "My notebook")
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
assert {:ok, false} = FileSystem.File.exists?(file)
|
||||
|
||||
Session.save(session_id)
|
||||
Session.save(session.pid)
|
||||
|
||||
assert_receive {:operation, {:mark_as_not_dirty, _}}
|
||||
assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file)
|
||||
end
|
||||
|
||||
@tag :tmp_dir
|
||||
test "creates nonexistent directories", %{session_id: session_id, tmp_dir: tmp_dir} do
|
||||
test "creates nonexistent directories", %{session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
file = FileSystem.File.resolve(tmp_dir, "nonexistent/dir/notebook.livemd")
|
||||
Session.set_file(session_id, file)
|
||||
Session.set_file(session.pid, file)
|
||||
# Perform a change, so the notebook is dirty
|
||||
Session.set_notebook_name(session_id, "My notebook")
|
||||
Session.set_notebook_name(session.pid, "My notebook")
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
assert {:ok, false} = FileSystem.File.exists?(file)
|
||||
|
||||
Session.save(session_id)
|
||||
Session.save(session.pid)
|
||||
|
||||
assert_receive {:operation, {:mark_as_not_dirty, _}}
|
||||
assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file)
|
||||
|
|
@ -332,32 +332,32 @@ defmodule Livebook.SessionTest do
|
|||
describe "close/1" do
|
||||
@tag :tmp_dir
|
||||
test "saves the notebook and notifies subscribers once the session is closed",
|
||||
%{session_id: session_id, tmp_dir: tmp_dir} do
|
||||
%{session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
Session.set_file(session_id, file)
|
||||
Session.set_file(session.pid, file)
|
||||
# Perform a change, so the notebook is dirty
|
||||
Session.set_notebook_name(session_id, "My notebook")
|
||||
Session.set_notebook_name(session.pid, "My notebook")
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
assert {:ok, false} = FileSystem.File.exists?(file)
|
||||
|
||||
Process.flag(:trap_exit, true)
|
||||
Session.close(session_id)
|
||||
Session.close(session.pid)
|
||||
|
||||
assert_receive :session_closed
|
||||
assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file)
|
||||
end
|
||||
|
||||
test "clears session temporary directory", %{session_id: session_id} do
|
||||
%{images_dir: images_dir} = Session.get_summary(session_id)
|
||||
test "clears session temporary directory", %{session: session} do
|
||||
%{images_dir: images_dir} = session
|
||||
:ok = FileSystem.File.create_dir(images_dir)
|
||||
|
||||
assert {:ok, true} = FileSystem.File.exists?(images_dir)
|
||||
|
||||
Process.flag(:trap_exit, true)
|
||||
Session.close(session_id)
|
||||
Session.close(session.pid)
|
||||
|
||||
# Wait for the session to deal with the files
|
||||
Process.sleep(50)
|
||||
|
|
@ -384,9 +384,8 @@ defmodule Livebook.SessionTest do
|
|||
image_file = FileSystem.File.resolve(tmp_dir, "image.jpg")
|
||||
:ok = FileSystem.File.write(image_file, "")
|
||||
|
||||
session_id = start_session(copy_images_from: tmp_dir)
|
||||
|
||||
%{images_dir: images_dir} = Session.get_summary(session_id)
|
||||
session = start_session(copy_images_from: tmp_dir)
|
||||
%{images_dir: images_dir} = session
|
||||
|
||||
assert {:ok, true} =
|
||||
FileSystem.File.exists?(FileSystem.File.resolve(images_dir, "image.jpg"))
|
||||
|
|
@ -395,9 +394,8 @@ defmodule Livebook.SessionTest do
|
|||
test "saves images when :images option is specified" do
|
||||
images = %{"image.jpg" => "binary content"}
|
||||
|
||||
session_id = start_session(images: images)
|
||||
|
||||
%{images_dir: images_dir} = Session.get_summary(session_id)
|
||||
session = start_session(images: images)
|
||||
%{images_dir: images_dir} = session
|
||||
|
||||
assert FileSystem.File.resolve(images_dir, "image.jpg") |> FileSystem.File.read() ==
|
||||
{:ok, "binary content"}
|
||||
|
|
@ -410,14 +408,13 @@ defmodule Livebook.SessionTest do
|
|||
# to verify session integrates well with it properly.
|
||||
|
||||
test "starts a standalone runtime upon first evaluation if there was none set explicitly" do
|
||||
session_id = Utils.random_id()
|
||||
{:ok, _} = Session.start_link(id: session_id)
|
||||
session = start_session()
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||
|
||||
Session.queue_cell_evaluation(session_id, cell_id)
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
# Give it a bit more time as this involves starting a system process.
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, _,
|
||||
|
|
@ -426,14 +423,13 @@ defmodule Livebook.SessionTest do
|
|||
end
|
||||
|
||||
test "if the runtime node goes down, notifies the subscribers" do
|
||||
session_id = Utils.random_id()
|
||||
{:ok, _} = Session.start_link(id: session_id)
|
||||
session = start_session()
|
||||
{:ok, runtime} = Runtime.ElixirStandalone.init()
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
# Wait for the runtime to be set
|
||||
Session.connect_runtime(session_id, runtime)
|
||||
Session.connect_runtime(session.pid, runtime)
|
||||
assert_receive {:operation, {:set_runtime, _, ^runtime}}
|
||||
|
||||
# Terminate the other node, the session should detect that
|
||||
|
|
@ -443,11 +439,11 @@ defmodule Livebook.SessionTest do
|
|||
assert_receive {:info, "runtime node terminated unexpectedly"}
|
||||
end
|
||||
|
||||
test "on user change sends an update operation subscribers", %{session_id: session_id} do
|
||||
test "on user change sends an update operation subscribers", %{session: session} do
|
||||
user = Livebook.Users.User.new()
|
||||
Session.register_client(session_id, self(), user)
|
||||
Session.register_client(session.pid, self(), user)
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
updated_user = %{user | name: "Jake Peralta"}
|
||||
Livebook.Users.broadcast_change(updated_user)
|
||||
|
|
@ -475,12 +471,12 @@ defmodule Livebook.SessionTest do
|
|||
]
|
||||
}
|
||||
|
||||
session_id = start_session(notebook: notebook)
|
||||
session = start_session(notebook: notebook)
|
||||
|
||||
cell_id = elixir_cell.id
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Session.queue_cell_evaluation(session_id, cell_id)
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, text_output},
|
||||
|
|
@ -505,12 +501,12 @@ defmodule Livebook.SessionTest do
|
|||
]
|
||||
}
|
||||
|
||||
session_id = start_session(notebook: notebook)
|
||||
session = start_session(notebook: notebook)
|
||||
|
||||
cell_id = elixir_cell.id
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Session.queue_cell_evaluation(session_id, cell_id)
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, text_output},
|
||||
|
|
@ -537,12 +533,12 @@ defmodule Livebook.SessionTest do
|
|||
]
|
||||
}
|
||||
|
||||
session_id = start_session(notebook: notebook)
|
||||
session = start_session(notebook: notebook)
|
||||
|
||||
cell_id = elixir_cell.id
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Session.queue_cell_evaluation(session_id, cell_id)
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, text_output},
|
||||
|
|
@ -555,14 +551,14 @@ defmodule Livebook.SessionTest do
|
|||
|
||||
defp start_session(opts \\ []) do
|
||||
session_id = Utils.random_id()
|
||||
{:ok, _} = Session.start_link(Keyword.merge(opts, id: session_id))
|
||||
session_id
|
||||
{:ok, pid} = Session.start_link(Keyword.merge([id: session_id], opts))
|
||||
Session.get_by_pid(pid)
|
||||
end
|
||||
|
||||
defp insert_section_and_cell(session_id) do
|
||||
Session.insert_section(session_id, 0)
|
||||
defp insert_section_and_cell(session_pid) do
|
||||
Session.insert_section(session_pid, 0)
|
||||
assert_receive {:operation, {:insert_section, _, 0, section_id}}
|
||||
Session.insert_cell(session_id, section_id, 0, :elixir)
|
||||
Session.insert_cell(session_pid, section_id, 0, :elixir)
|
||||
assert_receive {:operation, {:insert_cell, _, ^section_id, 0, :elixir, cell_id}}
|
||||
|
||||
{section_id, cell_id}
|
||||
|
|
|
|||
51
test/livebook/sessions_test.exs
Normal file
51
test/livebook/sessions_test.exs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
defmodule Livebook.SessionsTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Livebook.Sessions
|
||||
|
||||
describe "create_session/0" do
|
||||
test "starts a new session process under the sessions supervisor" do
|
||||
{:ok, session} = Sessions.create_session()
|
||||
assert has_child_with_pid?(Livebook.SessionSupervisor, session.pid)
|
||||
end
|
||||
|
||||
test "broadcasts a message to subscribers" do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions")
|
||||
{:ok, %{id: id}} = Sessions.create_session()
|
||||
assert_receive {:session_created, %{id: ^id}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_sessions/0" do
|
||||
test "lists all sessions" do
|
||||
{:ok, session} = Sessions.create_session()
|
||||
assert session in Sessions.list_sessions()
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_session/1" do
|
||||
test "returns an error if no session with the given id exists" do
|
||||
id = Livebook.Utils.random_node_aware_id()
|
||||
assert :error = Sessions.fetch_session(id)
|
||||
end
|
||||
|
||||
test "returns session matching the given id" do
|
||||
{:ok, session} = Sessions.create_session()
|
||||
assert {:ok, ^session} = Sessions.fetch_session(session.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_session/1" do
|
||||
test "broadcasts a message to subscribers" do
|
||||
{:ok, %{id: id} = session} = Sessions.create_session()
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions")
|
||||
updated_session = %{session | notebook_name: "New name"}
|
||||
Livebook.Sessions.update_session(updated_session)
|
||||
assert_receive {:session_updated, %{id: ^id, notebook_name: "New name"}}
|
||||
end
|
||||
end
|
||||
|
||||
defp has_child_with_pid?(supervisor, pid) do
|
||||
List.keymember?(DynamicSupervisor.which_children(supervisor), pid, 1)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,62 +1,64 @@
|
|||
defmodule LivebookWeb.SessionControllerTest do
|
||||
use LivebookWeb.ConnCase, async: true
|
||||
|
||||
alias Livebook.{SessionSupervisor, Session, Notebook, FileSystem}
|
||||
alias Livebook.{Sessions, Session, Notebook, FileSystem}
|
||||
|
||||
describe "show_image" do
|
||||
test "returns not found when the given session does not exist", %{conn: conn} do
|
||||
conn = get(conn, Routes.session_path(conn, :show_image, "nonexistent", "image.jpg"))
|
||||
id = Livebook.Utils.random_node_aware_id()
|
||||
conn = get(conn, Routes.session_path(conn, :show_image, id, "image.jpg"))
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "Not found"
|
||||
end
|
||||
|
||||
test "returns not found when the given image does not exist", %{conn: conn} do
|
||||
{:ok, session_id} = SessionSupervisor.create_session()
|
||||
{:ok, session} = Sessions.create_session()
|
||||
|
||||
conn = get(conn, Routes.session_path(conn, :show_image, session_id, "nonexistent.jpg"))
|
||||
conn = get(conn, Routes.session_path(conn, :show_image, session.id, "nonexistent.jpg"))
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "No such file or directory"
|
||||
|
||||
SessionSupervisor.close_session(session_id)
|
||||
Session.close(session.pid)
|
||||
end
|
||||
|
||||
test "returns the image when it does exist", %{conn: conn} do
|
||||
{:ok, session_id} = SessionSupervisor.create_session()
|
||||
%{images_dir: images_dir} = Session.get_summary(session_id)
|
||||
{:ok, session} = Sessions.create_session()
|
||||
%{images_dir: images_dir} = session
|
||||
:ok = FileSystem.File.resolve(images_dir, "test.jpg") |> FileSystem.File.write("")
|
||||
|
||||
conn = get(conn, Routes.session_path(conn, :show_image, session_id, "test.jpg"))
|
||||
conn = get(conn, Routes.session_path(conn, :show_image, session.id, "test.jpg"))
|
||||
|
||||
assert conn.status == 200
|
||||
assert get_resp_header(conn, "content-type") == ["image/jpeg"]
|
||||
|
||||
SessionSupervisor.close_session(session_id)
|
||||
Session.close(session.pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "download_source" do
|
||||
test "returns not found when the given session does not exist", %{conn: conn} do
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, "nonexistent", "livemd"))
|
||||
id = Livebook.Utils.random_node_aware_id()
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, id, "livemd"))
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "Not found"
|
||||
end
|
||||
|
||||
test "returns bad request when given an invalid format", %{conn: conn} do
|
||||
{:ok, session_id} = SessionSupervisor.create_session()
|
||||
{:ok, session} = Sessions.create_session()
|
||||
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, session_id, "invalid"))
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, session.id, "invalid"))
|
||||
|
||||
assert conn.status == 400
|
||||
assert conn.resp_body == "Invalid format, supported formats: livemd, exs"
|
||||
end
|
||||
|
||||
test "handles live markdown notebook source", %{conn: conn} do
|
||||
{:ok, session_id} = SessionSupervisor.create_session()
|
||||
{:ok, session} = Sessions.create_session()
|
||||
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, session_id, "livemd"))
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, session.id, "livemd"))
|
||||
|
||||
assert conn.status == 200
|
||||
assert get_resp_header(conn, "content-type") == ["text/plain"]
|
||||
|
|
@ -71,7 +73,7 @@ defmodule LivebookWeb.SessionControllerTest do
|
|||
```
|
||||
"""
|
||||
|
||||
SessionSupervisor.close_session(session_id)
|
||||
Session.close(session.pid)
|
||||
end
|
||||
|
||||
test "includes output in markdown when include_outputs parameter is set", %{conn: conn} do
|
||||
|
|
@ -95,10 +97,10 @@ defmodule LivebookWeb.SessionControllerTest do
|
|||
]
|
||||
}
|
||||
|
||||
{:ok, session_id} = SessionSupervisor.create_session(notebook: notebook)
|
||||
{:ok, session} = Sessions.create_session(notebook: notebook)
|
||||
|
||||
query = [include_outputs: "true"]
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, session_id, "livemd", query))
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, session.id, "livemd", query))
|
||||
|
||||
assert conn.status == 200
|
||||
assert get_resp_header(conn, "content-type") == ["text/plain"]
|
||||
|
|
@ -117,13 +119,13 @@ defmodule LivebookWeb.SessionControllerTest do
|
|||
```
|
||||
"""
|
||||
|
||||
SessionSupervisor.close_session(session_id)
|
||||
Session.close(session.pid)
|
||||
end
|
||||
|
||||
test "handles elixir notebook source", %{conn: conn} do
|
||||
{:ok, session_id} = SessionSupervisor.create_session()
|
||||
{:ok, session} = Sessions.create_session()
|
||||
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, session_id, "exs"))
|
||||
conn = get(conn, Routes.session_path(conn, :download_source, session.id, "exs"))
|
||||
|
||||
assert conn.status == 200
|
||||
assert get_resp_header(conn, "content-type") == ["text/plain"]
|
||||
|
|
@ -134,7 +136,7 @@ defmodule LivebookWeb.SessionControllerTest do
|
|||
# ── Section ──
|
||||
"""
|
||||
|
||||
SessionSupervisor.close_session(session_id)
|
||||
Session.close(session.pid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Livebook.{SessionSupervisor, Session}
|
||||
alias Livebook.{Sessions, Session}
|
||||
|
||||
test "disconnected and connected render", %{conn: conn} do
|
||||
{:ok, view, disconnected_html} = live(conn, "/")
|
||||
|
|
@ -113,34 +113,38 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
|
||||
describe "sessions list" do
|
||||
test "lists running sessions", %{conn: conn} do
|
||||
{:ok, id1} = SessionSupervisor.create_session()
|
||||
{:ok, id2} = SessionSupervisor.create_session()
|
||||
{:ok, session1} = Sessions.create_session()
|
||||
{:ok, session2} = Sessions.create_session()
|
||||
|
||||
{:ok, view, _} = live(conn, "/")
|
||||
|
||||
assert render(view) =~ id1
|
||||
assert render(view) =~ id2
|
||||
assert render(view) =~ session1.id
|
||||
assert render(view) =~ session2.id
|
||||
end
|
||||
|
||||
test "updates UI whenever a session is added or deleted", %{conn: conn} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions")
|
||||
|
||||
{:ok, view, _} = live(conn, "/")
|
||||
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
{:ok, %{id: id} = session} = Sessions.create_session()
|
||||
assert_receive {:session_created, %{id: ^id}}
|
||||
assert render(view) =~ id
|
||||
|
||||
SessionSupervisor.close_session(id)
|
||||
Session.close(session.pid)
|
||||
assert_receive {:session_closed, %{id: ^id}}
|
||||
refute render(view) =~ id
|
||||
end
|
||||
|
||||
test "allows forking existing session", %{conn: conn} do
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
Session.set_notebook_name(id, "My notebook")
|
||||
{:ok, session} = Sessions.create_session()
|
||||
Session.set_notebook_name(session.pid, "My notebook")
|
||||
|
||||
{:ok, view, _} = live(conn, "/")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|> element(~s{[data-test-session-id="#{id}"] button}, "Fork")
|
||||
|> element(~s{[data-test-session-id="#{session.id}"] button}, "Fork")
|
||||
|> render_click()
|
||||
|
||||
assert to =~ "/sessions/"
|
||||
|
|
@ -150,21 +154,21 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
end
|
||||
|
||||
test "allows closing session after confirmation", %{conn: conn} do
|
||||
{:ok, id} = SessionSupervisor.create_session()
|
||||
{:ok, session} = Sessions.create_session()
|
||||
|
||||
{:ok, view, _} = live(conn, "/")
|
||||
|
||||
assert render(view) =~ id
|
||||
assert render(view) =~ session.id
|
||||
|
||||
view
|
||||
|> element(~s{[data-test-session-id="#{id}"] a}, "Close")
|
||||
|> element(~s{[data-test-session-id="#{session.id}"] a}, "Close")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element(~s{button}, "Close session")
|
||||
|> render_click()
|
||||
|
||||
refute render(view) =~ id
|
||||
refute render(view) =~ session.id
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -185,7 +189,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
|
||||
describe "notebook import" do
|
||||
test "allows importing notebook directly from content", %{conn: conn} do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions")
|
||||
|
||||
{:ok, view, _} = live(conn, "/home/import/content")
|
||||
|
||||
|
|
@ -197,7 +201,7 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
|> element("form", "Import")
|
||||
|> render_submit(%{data: %{content: notebook_content}})
|
||||
|
||||
assert_receive {:session_created, id}
|
||||
assert_receive {:session_created, %{id: id}}
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{id}")
|
||||
assert render(view) =~ "My notebook"
|
||||
|
|
|
|||
|
|
@ -3,116 +3,116 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Livebook.{SessionSupervisor, Session, Delta, Runtime, Users, FileSystem}
|
||||
alias Livebook.{Sessions, Session, Delta, Runtime, Users, FileSystem}
|
||||
alias Livebook.Notebook.Cell
|
||||
alias Livebook.Users.User
|
||||
|
||||
setup do
|
||||
{:ok, session_id} = SessionSupervisor.create_session(notebook: Livebook.Notebook.new())
|
||||
%{session_id: session_id}
|
||||
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
|
||||
%{session: session}
|
||||
end
|
||||
|
||||
test "disconnected and connected render", %{conn: conn, session_id: session_id} do
|
||||
{:ok, view, disconnected_html} = live(conn, "/sessions/#{session_id}")
|
||||
test "disconnected and connected render", %{conn: conn, session: session} do
|
||||
{:ok, view, disconnected_html} = live(conn, "/sessions/#{session.id}")
|
||||
assert disconnected_html =~ "Untitled notebook"
|
||||
assert render(view) =~ "Untitled notebook"
|
||||
end
|
||||
|
||||
describe "asynchronous updates" do
|
||||
test "renders an updated notebook name", %{conn: conn, session_id: session_id} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
test "renders an updated notebook name", %{conn: conn, session: session} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
Session.set_notebook_name(session_id, "My notebook")
|
||||
wait_for_session_update(session_id)
|
||||
Session.set_notebook_name(session.pid, "My notebook")
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
assert render(view) =~ "My notebook"
|
||||
end
|
||||
|
||||
test "renders a newly inserted section", %{conn: conn, session_id: session_id} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
test "renders a newly inserted section", %{conn: conn, session: session} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
section_id = insert_section(session_id)
|
||||
section_id = insert_section(session.pid)
|
||||
|
||||
assert render(view) =~ section_id
|
||||
end
|
||||
|
||||
test "renders an updated section name", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
test "renders an updated section name", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
Session.set_section_name(session_id, section_id, "My section")
|
||||
wait_for_session_update(session_id)
|
||||
Session.set_section_name(session.pid, section_id, "My section")
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
assert render(view) =~ "My section"
|
||||
end
|
||||
|
||||
test "renders a newly inserted cell", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
test "renders a newly inserted cell", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
cell_id = insert_text_cell(session_id, section_id, :markdown)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :markdown)
|
||||
|
||||
assert render(view) =~ "cell-" <> cell_id
|
||||
end
|
||||
|
||||
test "un-renders a deleted cell", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_text_cell(session_id, section_id, :markdown)
|
||||
test "un-renders a deleted cell", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :markdown)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
Session.delete_cell(session_id, cell_id)
|
||||
wait_for_session_update(session_id)
|
||||
Session.delete_cell(session.pid, cell_id)
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
refute render(view) =~ "cell-" <> cell_id
|
||||
end
|
||||
end
|
||||
|
||||
describe "UI events update the shared state" do
|
||||
test "adding a new section", %{conn: conn, session_id: session_id} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
test "adding a new section", %{conn: conn, session: session} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("button", "New section")
|
||||
|> render_click()
|
||||
|
||||
assert %{notebook: %{sections: [_section]}} = Session.get_data(session_id)
|
||||
assert %{notebook: %{sections: [_section]}} = Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
test "adding a new cell", %{conn: conn, session_id: session_id} do
|
||||
Session.insert_section(session_id, 0)
|
||||
test "adding a new cell", %{conn: conn, session: session} do
|
||||
Session.insert_section(session.pid, 0)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("button", "+ Markdown")
|
||||
|> render_click()
|
||||
|
||||
assert %{notebook: %{sections: [%{cells: [%Cell.Markdown{}]}]}} =
|
||||
Session.get_data(session_id)
|
||||
Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
test "queueing cell evaluation", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_text_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
||||
test "queueing cell evaluation", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir, "Process.sleep(10)")
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
|
||||
|
||||
assert %{cell_infos: %{^cell_id => %{evaluation_status: :evaluating}}} =
|
||||
Session.get_data(session_id)
|
||||
Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
test "cancelling cell evaluation", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_text_cell(session_id, section_id, :elixir, "Process.sleep(2000)")
|
||||
test "cancelling cell evaluation", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir, "Process.sleep(2000)")
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|
|
@ -123,71 +123,71 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|> render_hook("cancel_cell_evaluation", %{"cell_id" => cell_id})
|
||||
|
||||
assert %{cell_infos: %{^cell_id => %{evaluation_status: :ready}}} =
|
||||
Session.get_data(session_id)
|
||||
Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
test "inserting a cell below the given cell", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_text_cell(session_id, section_id, :elixir)
|
||||
test "inserting a cell below the given cell", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|> render_hook("insert_cell_below", %{"cell_id" => cell_id, "type" => "markdown"})
|
||||
|
||||
assert %{notebook: %{sections: [%{cells: [_first_cell, %Cell.Markdown{}]}]}} =
|
||||
Session.get_data(session_id)
|
||||
Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
test "inserting a cell above the given cell", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_text_cell(session_id, section_id, :elixir)
|
||||
test "inserting a cell above the given cell", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|> render_hook("insert_cell_above", %{"cell_id" => cell_id, "type" => "markdown"})
|
||||
|
||||
assert %{notebook: %{sections: [%{cells: [%Cell.Markdown{}, _first_cell]}]}} =
|
||||
Session.get_data(session_id)
|
||||
Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
test "deleting the given cell", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_text_cell(session_id, section_id, :elixir)
|
||||
test "deleting the given cell", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|> render_hook("delete_cell", %{"cell_id" => cell_id})
|
||||
|
||||
assert %{notebook: %{sections: [%{cells: []}]}} = Session.get_data(session_id)
|
||||
assert %{notebook: %{sections: [%{cells: []}]}} = Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
test "newlines in input values are normalized", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_input_cell(session_id, section_id)
|
||||
test "newlines in input values are normalized", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_input_cell(session.pid, section_id)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element(~s/form[phx-change="set_cell_value"]/)
|
||||
|> render_change(%{"value" => "line\r\nline"})
|
||||
|
||||
assert %{notebook: %{sections: [%{cells: [%{id: ^cell_id, value: "line\nline"}]}]}} =
|
||||
Session.get_data(session_id)
|
||||
Session.get_data(session.pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "runtime settings" do
|
||||
test "connecting to elixir standalone updates connect button to reconnect",
|
||||
%{conn: conn, session_id: session_id} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}/settings/runtime")
|
||||
%{conn: conn, session: session} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/settings/runtime")
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
[elixir_standalone_view] = live_children(view)
|
||||
|
||||
|
|
@ -207,8 +207,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
describe "persistence settings" do
|
||||
@tag :tmp_dir
|
||||
test "saving to file shows the newly created file",
|
||||
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}/settings/file")
|
||||
%{conn: conn, session: session, tmp_dir: tmp_dir} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/settings/file")
|
||||
|
||||
assert view = find_live_child(view, "persistence")
|
||||
|
||||
|
|
@ -233,8 +233,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
@tag :tmp_dir
|
||||
test "changing output persistence updates data",
|
||||
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}/settings/file")
|
||||
%{conn: conn, session: session, tmp_dir: tmp_dir} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/settings/file")
|
||||
|
||||
assert view = find_live_child(view, "persistence")
|
||||
|
||||
|
|
@ -256,17 +256,17 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|> element(~s{button[phx-click="save"]}, "Save")
|
||||
|> render_click()
|
||||
|
||||
assert %{notebook: %{persist_outputs: true}} = Session.get_data(session_id)
|
||||
assert %{notebook: %{persist_outputs: true}} = Session.get_data(session.pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "completion" do
|
||||
test "replies with nil completion reference when no runtime is started",
|
||||
%{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_text_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
||||
%{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir, "Process.sleep(10)")
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|
|
@ -280,14 +280,14 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end
|
||||
|
||||
test "replies with completion reference and then sends asynchronous response",
|
||||
%{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_text_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
||||
%{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :elixir, "Process.sleep(10)")
|
||||
|
||||
{:ok, runtime} = Livebook.Runtime.Embedded.init()
|
||||
Session.connect_runtime(session_id, runtime)
|
||||
Session.connect_runtime(session.pid, runtime)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|
|
@ -307,11 +307,11 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "forking the session", %{conn: conn, session_id: session_id} do
|
||||
Session.set_notebook_name(session_id, "My notebook")
|
||||
wait_for_session_update(session_id)
|
||||
test "forking the session", %{conn: conn, session: session} do
|
||||
Session.set_notebook_name(session.pid, "My notebook")
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|
|
@ -325,19 +325,19 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end
|
||||
|
||||
describe "connected users" do
|
||||
test "lists connected users", %{conn: conn, session_id: session_id} do
|
||||
test "lists connected users", %{conn: conn, session: session} do
|
||||
user1 = create_user_with_name("Jake Peralta")
|
||||
|
||||
client_pid =
|
||||
spawn_link(fn ->
|
||||
Session.register_client(session_id, self(), user1)
|
||||
Session.register_client(session.pid, self(), user1)
|
||||
|
||||
receive do
|
||||
:stop -> :ok
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
assert render(view) =~ "Jake Peralta"
|
||||
|
||||
|
|
@ -345,16 +345,16 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end
|
||||
|
||||
test "updates users list whenever a user joins or leaves",
|
||||
%{conn: conn, session_id: session_id} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
%{conn: conn, session: session} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
user1 = create_user_with_name("Jake Peralta")
|
||||
|
||||
client_pid =
|
||||
spawn_link(fn ->
|
||||
Session.register_client(session_id, self(), user1)
|
||||
Session.register_client(session.pid, self(), user1)
|
||||
|
||||
receive do
|
||||
:stop -> :ok
|
||||
|
|
@ -370,21 +370,21 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end
|
||||
|
||||
test "updates users list whenever a user changes his data",
|
||||
%{conn: conn, session_id: session_id} do
|
||||
%{conn: conn, session: session} do
|
||||
user1 = create_user_with_name("Jake Peralta")
|
||||
|
||||
client_pid =
|
||||
spawn_link(fn ->
|
||||
Session.register_client(session_id, self(), user1)
|
||||
Session.register_client(session.pid, self(), user1)
|
||||
|
||||
receive do
|
||||
:stop -> :ok
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
assert render(view) =~ "Jake Peralta"
|
||||
|
||||
|
|
@ -399,11 +399,11 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end
|
||||
|
||||
describe "input cell settings" do
|
||||
test "setting input cell attributes updates data", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_input_cell(session_id, section_id)
|
||||
test "setting input cell attributes updates data", %{conn: conn, session: session} do
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_input_cell(session.pid, section_id)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}/cell-settings/#{cell_id}")
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/cell-settings/#{cell_id}")
|
||||
|
||||
form_selector = ~s/[role="dialog"] form/
|
||||
|
||||
|
|
@ -439,28 +439,28 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
}
|
||||
]
|
||||
}
|
||||
} = Session.get_data(session_id)
|
||||
} = Session.get_data(session.pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "relative paths" do
|
||||
test "renders an info message when the path doesn't have notebook extension",
|
||||
%{conn: conn, session_id: session_id} do
|
||||
session_path = "/sessions/#{session_id}"
|
||||
%{conn: conn, session: session} do
|
||||
session_path = "/sessions/#{session.id}"
|
||||
|
||||
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||
result = live(conn, "/sessions/#{session_id}/document.pdf")
|
||||
result = live(conn, "/sessions/#{session.id}/document.pdf")
|
||||
|
||||
{:ok, view, _} = follow_redirect(result, conn)
|
||||
assert render(view) =~ "Got unrecognised session path: document.pdf"
|
||||
end
|
||||
|
||||
test "renders an info message when the session has neither original url nor path",
|
||||
%{conn: conn, session_id: session_id} do
|
||||
session_path = "/sessions/#{session_id}"
|
||||
%{conn: conn, session: session} do
|
||||
session_path = "/sessions/#{session.id}"
|
||||
|
||||
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||
result = live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||
result = live(conn, "/sessions/#{session.id}/notebook.livemd")
|
||||
|
||||
{:ok, view, _} = follow_redirect(result, conn)
|
||||
|
||||
|
|
@ -470,18 +470,18 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
@tag :tmp_dir
|
||||
test "renders an error message when the relative notebook does not exist",
|
||||
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||
%{conn: conn, session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
index_file = FileSystem.File.resolve(tmp_dir, "index.livemd")
|
||||
notebook_file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
|
||||
Session.set_file(session_id, index_file)
|
||||
wait_for_session_update(session_id)
|
||||
Session.set_file(session.pid, index_file)
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
session_path = "/sessions/#{session_id}"
|
||||
session_path = "/sessions/#{session.id}"
|
||||
|
||||
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||
result = live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||
result = live(conn, "/sessions/#{session.id}/notebook.livemd")
|
||||
|
||||
{:ok, view, _} = follow_redirect(result, conn)
|
||||
|
||||
|
|
@ -491,24 +491,25 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
@tag :tmp_dir
|
||||
test "opens a relative notebook if it exists",
|
||||
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||
%{conn: conn, session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
index_file = FileSystem.File.resolve(tmp_dir, "index.livemd")
|
||||
notebook_file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
|
||||
Session.set_file(session_id, index_file)
|
||||
wait_for_session_update(session_id)
|
||||
Session.set_file(session.pid, index_file)
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
:ok = FileSystem.File.write(notebook_file, "# Sibling notebook")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: new_session_path}}} =
|
||||
result = live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||
result = live(conn, "/sessions/#{session.id}/notebook.livemd")
|
||||
|
||||
{:ok, view, _} = follow_redirect(result, conn)
|
||||
assert render(view) =~ "Sibling notebook"
|
||||
|
||||
"/sessions/" <> session_id = new_session_path
|
||||
data = Session.get_data(session_id)
|
||||
{:ok, session} = Sessions.fetch_session(session_id)
|
||||
data = Session.get_data(session.pid)
|
||||
assert data.file == notebook_file
|
||||
end
|
||||
|
||||
|
|
@ -519,76 +520,77 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
index_file = FileSystem.File.resolve(tmp_dir, "index.livemd")
|
||||
notebook_file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
|
||||
{:ok, session_id} = SessionSupervisor.create_session(origin: {:file, index_file})
|
||||
{:ok, session} = Sessions.create_session(origin: {:file, index_file})
|
||||
|
||||
:ok = FileSystem.File.write(notebook_file, "# Sibling notebook")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: new_session_path}}} =
|
||||
result = live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||
result = live(conn, "/sessions/#{session.id}/notebook.livemd")
|
||||
|
||||
{:ok, view, _} = follow_redirect(result, conn)
|
||||
assert render(view) =~ "Sibling notebook - fork"
|
||||
|
||||
"/sessions/" <> session_id = new_session_path
|
||||
data = Session.get_data(session_id)
|
||||
{:ok, session} = Sessions.fetch_session(session_id)
|
||||
data = Session.get_data(session.pid)
|
||||
assert data.file == nil
|
||||
assert data.origin == {:file, notebook_file}
|
||||
end
|
||||
|
||||
@tag :tmp_dir
|
||||
test "if the notebook is already open, redirects to the session",
|
||||
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||
%{conn: conn, session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
index_file = FileSystem.File.resolve(tmp_dir, "index.livemd")
|
||||
notebook_file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||
|
||||
Session.set_file(session_id, index_file)
|
||||
wait_for_session_update(session_id)
|
||||
Session.set_file(session.pid, index_file)
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
:ok = FileSystem.File.write(notebook_file, "# Sibling notebook")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: session_path}}} =
|
||||
live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||
live(conn, "/sessions/#{session.id}/notebook.livemd")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||
live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||
live(conn, "/sessions/#{session.id}/notebook.livemd")
|
||||
end
|
||||
|
||||
@tag :tmp_dir
|
||||
test "handles nested paths", %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||
test "handles nested paths", %{conn: conn, session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
parent_file = FileSystem.File.resolve(tmp_dir, "parent.livemd")
|
||||
child_dir = FileSystem.File.resolve(tmp_dir, "dir/")
|
||||
child_file = FileSystem.File.resolve(child_dir, "child.livemd")
|
||||
|
||||
Session.set_file(session_id, parent_file)
|
||||
wait_for_session_update(session_id)
|
||||
Session.set_file(session.pid, parent_file)
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
:ok = FileSystem.File.write(child_file, "# Child notebook")
|
||||
|
||||
{:ok, view, _} =
|
||||
conn
|
||||
|> live("/sessions/#{session_id}/dir/child.livemd")
|
||||
|> live("/sessions/#{session.id}/dir/child.livemd")
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert render(view) =~ "Child notebook"
|
||||
end
|
||||
|
||||
@tag :tmp_dir
|
||||
test "handles parent paths", %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||
test "handles parent paths", %{conn: conn, session: session, tmp_dir: tmp_dir} do
|
||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||
parent_file = FileSystem.File.resolve(tmp_dir, "parent.livemd")
|
||||
child_dir = FileSystem.File.resolve(tmp_dir, "dir/")
|
||||
child_file = FileSystem.File.resolve(child_dir, "child.livemd")
|
||||
|
||||
Session.set_file(session_id, child_file)
|
||||
wait_for_session_update(session_id)
|
||||
Session.set_file(session.pid, child_file)
|
||||
wait_for_session_update(session.pid)
|
||||
|
||||
:ok = FileSystem.File.write(parent_file, "# Parent notebook")
|
||||
|
||||
{:ok, view, _} =
|
||||
conn
|
||||
|> live("/sessions/#{session_id}/__parent__/parent.livemd")
|
||||
|> live("/sessions/#{session.id}/__parent__/parent.livemd")
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert render(view) =~ "Parent notebook"
|
||||
|
|
@ -604,11 +606,11 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end)
|
||||
|
||||
index_url = url(bypass.port) <> "/index.livemd"
|
||||
{:ok, session_id} = SessionSupervisor.create_session(origin: {:url, index_url})
|
||||
{:ok, session} = Sessions.create_session(origin: {:url, index_url})
|
||||
|
||||
{:ok, view, _} =
|
||||
conn
|
||||
|> live("/sessions/#{session_id}/notebook.livemd")
|
||||
|> live("/sessions/#{session.id}/notebook.livemd")
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert render(view) =~ "My notebook"
|
||||
|
|
@ -623,12 +625,12 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
index_url = url(bypass.port) <> "/index.livemd"
|
||||
|
||||
{:ok, session_id} = SessionSupervisor.create_session(origin: {:url, index_url})
|
||||
{:ok, session} = Sessions.create_session(origin: {:url, index_url})
|
||||
|
||||
session_path = "/sessions/#{session_id}"
|
||||
session_path = "/sessions/#{session.id}"
|
||||
|
||||
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||
result = live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||
result = live(conn, "/sessions/#{session.id}/notebook.livemd")
|
||||
|
||||
{:ok, view, _} = follow_redirect(result, conn)
|
||||
assert render(view) =~ "Cannot navigate, failed to download notebook from the given URL"
|
||||
|
|
@ -639,13 +641,13 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
index_url = "http://example.com/#{test}/index.livemd"
|
||||
notebook_url = "http://example.com/#{test}/notebook.livemd"
|
||||
|
||||
{:ok, index_session_id} = SessionSupervisor.create_session(origin: {:url, index_url})
|
||||
{:ok, notebook_session_id} = SessionSupervisor.create_session(origin: {:url, notebook_url})
|
||||
{:ok, index_session} = Sessions.create_session(origin: {:url, index_url})
|
||||
{:ok, notebook_session} = Sessions.create_session(origin: {:url, notebook_url})
|
||||
|
||||
notebook_session_path = "/sessions/#{notebook_session_id}"
|
||||
notebook_session_path = "/sessions/#{notebook_session.id}"
|
||||
|
||||
assert {:error, {:live_redirect, %{to: ^notebook_session_path}}} =
|
||||
live(conn, "/sessions/#{index_session_id}/notebook.livemd")
|
||||
live(conn, "/sessions/#{index_session.id}/notebook.livemd")
|
||||
end
|
||||
|
||||
test "renders an error message if there are already multiple session imported from the relative URL",
|
||||
|
|
@ -653,18 +655,14 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
index_url = "http://example.com/#{test}/index.livemd"
|
||||
notebook_url = "http://example.com/#{test}/notebook.livemd"
|
||||
|
||||
{:ok, index_session_id} = SessionSupervisor.create_session(origin: {:url, index_url})
|
||||
{:ok, index_session} = Sessions.create_session(origin: {:url, index_url})
|
||||
{:ok, _notebook_session1} = Sessions.create_session(origin: {:url, notebook_url})
|
||||
{:ok, _notebook_session2} = Sessions.create_session(origin: {:url, notebook_url})
|
||||
|
||||
{:ok, _notebook_session_id1} =
|
||||
SessionSupervisor.create_session(origin: {:url, notebook_url})
|
||||
|
||||
{:ok, _notebook_session_id2} =
|
||||
SessionSupervisor.create_session(origin: {:url, notebook_url})
|
||||
|
||||
index_session_path = "/sessions/#{index_session_id}"
|
||||
index_session_path = "/sessions/#{index_session.id}"
|
||||
|
||||
assert {:error, {:live_redirect, %{to: ^index_session_path}}} =
|
||||
result = live(conn, "/sessions/#{index_session_id}/notebook.livemd")
|
||||
result = live(conn, "/sessions/#{index_session.id}/notebook.livemd")
|
||||
|
||||
{:ok, view, _} = follow_redirect(result, conn)
|
||||
|
||||
|
|
@ -675,41 +673,41 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
# Helpers
|
||||
|
||||
defp wait_for_session_update(session_id) do
|
||||
defp wait_for_session_update(session_pid) do
|
||||
# This call is synchronous, so it gives the session time
|
||||
# for handling the previously sent change messages.
|
||||
Session.get_data(session_id)
|
||||
Session.get_data(session_pid)
|
||||
:ok
|
||||
end
|
||||
|
||||
# Utils for sending session requests, waiting for the change to be applied
|
||||
# and retrieving new ids if applicable.
|
||||
|
||||
defp insert_section(session_id) do
|
||||
Session.insert_section(session_id, 0)
|
||||
%{notebook: %{sections: [section]}} = Session.get_data(session_id)
|
||||
defp insert_section(session_pid) do
|
||||
Session.insert_section(session_pid, 0)
|
||||
%{notebook: %{sections: [section]}} = Session.get_data(session_pid)
|
||||
section.id
|
||||
end
|
||||
|
||||
defp insert_text_cell(session_id, section_id, type, content \\ "") do
|
||||
Session.insert_cell(session_id, section_id, 0, type)
|
||||
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_id)
|
||||
defp insert_text_cell(session_pid, section_id, type, content \\ "") do
|
||||
Session.insert_cell(session_pid, section_id, 0, type)
|
||||
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid)
|
||||
|
||||
# We need to register ourselves as a client to start submitting cell deltas
|
||||
user = Livebook.Users.User.new()
|
||||
Session.register_client(session_id, self(), user)
|
||||
Session.register_client(session_pid, self(), user)
|
||||
|
||||
delta = Delta.new(insert: content)
|
||||
Session.apply_cell_delta(session_id, cell.id, delta, 1)
|
||||
Session.apply_cell_delta(session_pid, cell.id, delta, 1)
|
||||
|
||||
wait_for_session_update(session_id)
|
||||
wait_for_session_update(session_pid)
|
||||
|
||||
cell.id
|
||||
end
|
||||
|
||||
defp insert_input_cell(session_id, section_id) do
|
||||
Session.insert_cell(session_id, section_id, 0, :input)
|
||||
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_id)
|
||||
defp insert_input_cell(session_pid, section_id) do
|
||||
Session.insert_cell(session_pid, section_id, 0, :input)
|
||||
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid)
|
||||
cell.id
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue