livebook/lib/live_book/session/file_guard.ex
Jonatan Kłosko 0925ec77cd
Implement notebook persistence and import (#44)
* Basic filesystem navigation

* Add file picker modal

* Implement autosave when dirty and show the status

* Add hompage link in the session view

* Improve file picker and use in both places

* Move session list to homepage

* Some refactoring

* Show import messages if any

* Fix and extend tests

* Show a message when there are no sessions running

* Rename import to fork and make that clear in notebook name

* Fix old route

* Show info when no file is connected to the given session

* Show runtime type next to filename

* Show button for joining session when a running path is selected

* Move modal components to SessionLive namespace

* Add FileGuard to lock files used for notebook persistence

* Use radio for specifying persistence type

* Don't lock nil path

* Simplify FileGuard implementation

* Test notebook persistence

* Fix typo

* Further simplify FileGuard

* Improve file listing

* Don't show parent dir when there's a basename being typed

* Add path component tests
2021-02-21 16:54:44 +01:00

76 lines
1.8 KiB
Elixir

defmodule LiveBook.Session.FileGuard do
@moduledoc false
# Serves as a locking mechanism for notebook files.
#
# Every session process willing to persist notebook
# should turn to `FileGuard` to make sure the path
# is not already used by another session.
use GenServer
@type state :: %{
path_with_owner_ref: %{String.t() => reference()}
}
@name __MODULE__
def start_link(_opts \\ []) do
GenServer.start_link(__MODULE__, [], name: @name)
end
def stop() do
GenServer.stop(@name)
end
@doc """
Locks the given file associating it with the given process.
If the owner process dies the file is automatically unlocked.
"""
@spec lock(String.t(), pid()) :: :ok | {:error, :already_in_use}
def lock(path, owner_pid) do
GenServer.call(@name, {:lock, path, owner_pid})
end
@doc """
Unlocks the given file.
"""
@spec unlock(String.t()) :: :ok
def unlock(path) do
GenServer.cast(@name, {:unlock, path})
end
# Callbacks
@impl true
def init(_opts) do
{:ok, %{path_with_owner_ref: %{}}}
end
@impl true
def handle_call({:lock, path, owner_pid}, _from, state) do
if Map.has_key?(state.path_with_owner_ref, path) do
{:reply, {:error, :already_in_use}, state}
else
monitor_ref = Process.monitor(owner_pid)
state = put_in(state.path_with_owner_ref[path], monitor_ref)
{:reply, :ok, state}
end
end
@impl true
def handle_cast({:unlock, path}, state) do
{maybe_ref, state} = pop_in(state.path_with_owner_ref[path])
maybe_ref && Process.demonitor(maybe_ref, [:flush])
{:noreply, state}
end
@impl true
def handle_info({:DOWN, ref, :process, _, _}, state) do
{path, ^ref} = Enum.find(state.path_with_owner_ref, &(elem(&1, 1) == ref))
{_, state} = pop_in(state.path_with_owner_ref[path])
{:noreply, state}
end
end