Skip to content

Commit

Permalink
🔥 Hard cell stop function
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsp committed Apr 2, 2020
1 parent ebb94fb commit ad60d95
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 28 deletions.
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ name = "Pluto"
uuid = "c3e4b0f8-55cb-11ea-2926-15256bba5781"
license = "MIT"
authors = ["Fons van der Plas <[email protected]>", "Mikołaj Bochenski <[email protected]>"]
version = "0.5.3"
version = "0.5.4"

[deps]
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
Expand Down
12 changes: 11 additions & 1 deletion assets/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,13 @@ document.addEventListener("DOMContentLoaded", () => {
requestDeleteRemoteCell(newCellNode.id)
}
newCellNode.querySelector(".runcell").onclick = (e) => {
requestChangeRemoteCell(newCellNode.id)
if(newCellNode.classList.contains("running"))
{
newCellNode.classList.add("error")
requestInterruptRemote()
} else {
requestChangeRemoteCell(newCellNode.id)
}
}

return newCellNode
Expand Down Expand Up @@ -300,6 +306,10 @@ document.addEventListener("DOMContentLoaded", () => {
client.send("runall", {})
}

function requestInterruptRemote() {
client.send("interruptall", {})
}

// Indexing works as if a new cell is added.
// e.g. if the third cell (at js-index 2) of [0, 1, 2, 3, 4]
// is moved to the end, that would be new js-index = 5
Expand Down
2 changes: 1 addition & 1 deletion assets/light.css
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ runarea>button.runcell>span::after {
}

cell.running>runarea>button.runcell>span::after {
background-image: url(https://unpkg.com/[email protected]/dist/svg/ellipsis-horizontal-circle-outline.svg);
background-image: url(https://unpkg.com/[email protected]/dist/svg/stop-circle-outline.svg);
}

cellinput>button.deletecell {
Expand Down
Binary file added demo/cellstopper.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 19 additions & 13 deletions src/react/Run.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,18 @@ function run_reactive!(initiator, notebook::Notebook, cells::Array{Cell, 1})
WorkspaceManager.delete_vars(workspace, to_delete_vars)
WorkspaceManager.delete_funcs(workspace, to_delete_funcs)

local any_interrupted = false
for cell in to_run
deleted_refs = cell.symstate.references workspace.deleted_vars
if length(deleted_refs) > 0
relay_reactivity_error!(cell, deleted_refs |> first |> UndefVarError)
if any_interrupted
relay_reactivity_error!(cell, InterruptException())
else
run_single!(initiator, notebook, cell)
deleted_refs = cell.symstate.references workspace.deleted_vars
if length(deleted_refs) > 0
relay_reactivity_error!(cell, deleted_refs |> first |> UndefVarError)
else
any_interrupted |= run_single!(initiator, notebook, cell)
end
end

putnotebookupdates!(notebook, clientupdate_cell_output(initiator, notebook, cell))
end

Expand All @@ -80,21 +84,23 @@ end
run_reactive!(initiator, notebook::Notebook, cell::Cell) = run_reactive!(initiator, notebook, [cell])
run_reactive_async!(initiator, notebook::Notebook, cell::Cell) = run_reactive_async!(initiator, notebook, [cell])

"Run a single cell non-reactively."
function run_single!(initiator, notebook::Notebook, cell::Cell)
"Run a single cell non-reactively, return whether the run was Interrupted."
function run_single!(initiator, notebook::Notebook, cell::Cell)::Bool
starttime = time_ns()
output, errored = WorkspaceManager.eval_fetch_in_workspace(notebook, cell.parsedcode)
run = WorkspaceManager.eval_fetch_in_workspace(notebook, cell.parsedcode)
cell.runtime = time_ns() - starttime

if errored
if run.errored
cell.output_repr = nothing
cell.error_repr = output[1]
cell.repr_mime = output[2]
cell.error_repr = run.output_formatted[1]
cell.repr_mime = run.output_formatted[2]
else
cell.output_repr = output[1]
cell.output_repr = run.output_formatted[1]
cell.error_repr = nothing
cell.repr_mime = output[2]
cell.repr_mime = run.output_formatted[2]
WorkspaceManager.undelete_vars(notebook, cell.symstate.assignments)
end

return run.interrupted
# TODO: capture stdout and display it somehwere, but let's keep using the actual terminal for now
end
149 changes: 138 additions & 11 deletions src/react/WorkspaceManager.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@ end

mutable struct ProcessWorkspace <: AbstractWorkspace
workspace_pid::Int64
dowork_token::Channel{Nothing}
deleted_vars::Set{Symbol}
end

ProcessWorkspace(workspace_pid::Int64) = let
t = Channel{Nothing}(1)
put!(t, nothing)
ProcessWorkspace(workspace_pid, t, Set{Symbol}())
end

default_workspace_method = ProcessWorkspace

"The workspace method to be used for all future workspace creations. ModuleWorkspace` is lightest, `ProcessWorkspace` can always terminate."
function set_default_workspace_method(method)
function set_default_workspace_method(method::Type{<:AbstractWorkspace})
global default_workspace_method = method
end


"These expressions get executed whenever a new workspace is created."
workspace_preamble = [:(using Markdown), :(ENV["GKSwstype"] = "nul")]

Expand Down Expand Up @@ -64,12 +70,22 @@ end
function make_workspace(notebook::Notebook, ::Val{ProcessWorkspace})::ProcessWorkspace
pid = Distributed.addprocs(1) |> first

workspace = ProcessWorkspace(pid, Set{Symbol}())
workspace = ProcessWorkspace(pid)
workspaces[notebook.uuid] = workspace

eval_in_workspace.([workspace], workspace_preamble)
# TODO: we could also import Pluto
eval_in_workspace(workspace, :(include($(joinpath(PKG_ROOT_DIR, "src", "notebookserver", "FormatOutput.jl")))))
# For Windows?
eval_in_workspace(workspace, :(ccall(:jl_exit_on_sigint, Cvoid, (Cint,), 0)))

# so that we NEVER break the workspace with an interrupt 🤕
@async eval_in_workspace(workspace,
:(while true
try
wait()
catch end
end))

workspace
end
Expand Down Expand Up @@ -99,12 +115,12 @@ function get_workspace(notebook::Notebook)::AbstractWorkspace
end
end

"Evaluate expression inside the workspace - output is fetched and formatted, errors are caught and formatted. Returns formatted output and errored? flag."
function eval_fetch_in_workspace(notebook::Notebook, expr)::Tuple{Tuple{String, MIME}, Bool}
"Evaluate expression inside the workspace - output is fetched and formatted, errors are caught and formatted. Returns formatted output and error flags."
function eval_fetch_in_workspace(notebook::Notebook, expr)::NamedTuple{(:output_formatted, :errored, :interrupted),Tuple{Tuple{String, MIME},Bool,Bool}}
eval_fetch_in_workspace(get_workspace(notebook), expr)
end

function eval_fetch_in_workspace(workspace::ProcessWorkspace, expr)::Tuple{Tuple{String, MIME}, Bool}
function eval_fetch_in_workspace(workspace::ProcessWorkspace, expr)::NamedTuple{(:output_formatted, :errored, :interrupted),Tuple{Tuple{String, MIME},Bool,Bool}}
# We wrap the expression in a try-catch block, because we want to capture and format the exception on the worker itself.
wrapped = :(ans = try
# We want to eval `expr` in the global scope, try introduced a local scope.
Expand All @@ -113,16 +129,48 @@ function eval_fetch_in_workspace(workspace::ProcessWorkspace, expr)::Tuple{Tuple
bt = stacktrace(catch_backtrace())
CapturedException(ex, bt)
end)

# run the code 🏃‍♀️
# we use [pid] instead of pid to prevent fetching output
Distributed.remotecall_eval(Main, [workspace.workspace_pid], wrapped)

# another try block to catch an InterruptException
token = take!(workspace.dowork_token)
try
Distributed.remotecall_eval(Main, [workspace.workspace_pid], wrapped)
put!(workspace.dowork_token, token)
catch exs
# We don't use a `finally` because the token needs to be back asap
put!(workspace.dowork_token, token)
try
@assert exs isa CompositeException
ex = exs.exceptions |> first
@assert ex isa Distributed.RemoteException
@assert ex.pid == workspace.workspace_pid
@assert ex.captured.ex isa InterruptException

return (output_formatted=format_output(InterruptException()), errored=true, interrupted=true)
catch assertionerr
showerror(stderr, exs)
return (output_formatted=format_output(exs), errored=true, interrupted=true)
end
end

# instead of fetching the output value (which might not make sense in our context, since the user can define structs, types, functions, etc), we format the cell output on the worker, and fetch the formatted output.
# This also means that very big objects are not duplicated in RAM.
Distributed.remotecall_eval(Main, workspace.workspace_pid, :(format_output(ans), isa(ans, CapturedException)))
fetcher = :((output_formatted=format_output(ans), errored=isa(ans, CapturedException), interrupted=false))

# token = take!(workspace.dowork_token)
try
result = Distributed.remotecall_eval(Main, workspace.workspace_pid, fetcher)
# put!(workspace.dowork_token, token)
return result
catch ex
# put!(workspace.dowork_token, token)
rethrow(ex)
end
end

function eval_fetch_in_workspace(workspace::ModuleWorkspace, expr)::Tuple{Tuple{String, MIME}, Bool}
function eval_fetch_in_workspace(workspace::ModuleWorkspace, expr)::NamedTuple{(:output_formatted, :errored, :interrupted),Tuple{Tuple{String, MIME},Bool,Bool}}
ans = try
Core.eval(workspace.workspace_module, expr)
catch ex
Expand All @@ -133,13 +181,20 @@ function eval_fetch_in_workspace(workspace::ModuleWorkspace, expr)::Tuple{Tuple{
format_output(ans), isa(ans, CapturedException)
end

"Evaluate expression inside the workspace - output is not fetched, errors are rethrown."
"Evaluate expression inside the workspace - output is not fetched, errors are rethrown. For internal use."
function eval_in_workspace(notebook::Notebook, expr)
eval_in_workspace(get_workspace(notebook), expr)
end

function eval_in_workspace(workspace::ProcessWorkspace, expr)
Distributed.remotecall_eval(Main, [workspace.workspace_pid], expr)
# token = take!(workspace.dowork_token)
try
Distributed.remotecall_eval(Main, [workspace.workspace_pid], expr)
# put!(workspace.dowork_token, token)
catch ex
# put!(workspace.dowork_token, token)
rethrow(ex)
end
nothing
end

Expand All @@ -148,6 +203,78 @@ function eval_in_workspace(workspace::ModuleWorkspace, expr)
nothing
end

# "Interrupt (Ctrl+C) a workspace, return whether succesful."
# function interrupt_workspace(initiator, notebook::Notebook)::Bool
# interrupt_workspace(initiator, WorkspaceManager.get_workspace(notebook))
# end

# function interrupt_workspace(initiator, workspace::ModuleWorkspace)
# @warn "Unfortunately, a `ModuleWorkspace` can't be interrupted. Use a `ProcessWorkspace` instead."
# false
# end

# function interrupt_workspace(initiator, workspace::ProcessWorkspace)
# if Sys.iswindows()
# @warn "Unfortunately, stopping cells is currently not supported on Windows.
# Maybe the Windows Subsystem for Linux is right for you:
# https://docs.microsoft.com/en-us/windows/wsl"
# return false
# end
# println("Sending interrupt to $(workspace.workspace_pid)")
# Distributed.interrupt(workspace.workspace_pid)
# true
# end

"Force interrupt (SIGINT) a workspace, return whether succesful"
function kill_workspace(initiator, notebook::Notebook)::Bool
kill_workspace(initiator, WorkspaceManager.get_workspace(notebook))
end

function kill_workspace(initiator, workspace::ModuleWorkspace)
@warn "Unfortunately, a `ModuleWorkspace` can't be interrupted. Use a `ProcessWorkspace` instead."
false
end

function kill_workspace(initiator, workspace::ProcessWorkspace)
if Sys.iswindows()
@warn "Unfortunately, stopping cells is currently not supported on Windows :(
Maybe the Windows Subsystem for Linux is right for you:
https://docs.microsoft.com/en-us/windows/wsl"
return false
end
# You can force kill a julia process by pressing Ctrl+C four times 🙃
# But this is not very consistent, so we will just keep pressing Ctrl+C until the workspace isn't running anymore.
# TODO: this will also kill "pending" evaluations, and any evaluations started within 100ms of the kill. A global "evaluation count" would fix this.
# TODO: listen for the final words of the remote process on stdout/stderr:

@info "Sending interrupt to process $(workspace.workspace_pid)"
Distributed.interrupt(workspace.workspace_pid)

delay = 5.0 # seconds
parts = 100

for _ in 1:parts
sleep(delay/parts)
if isready(workspace.dowork_token)
println("Cell interrupted!")
return true
end
end

println("Still running... starting sequence")
while !isready(workspace.dowork_token)
print(" 🔥 ")
for _ in 1:1
Distributed.interrupt(workspace.workspace_pid)
sleep(0.001)
end
sleep(0.2)
end
println()
println("Cell interrupted!")
true
end


"Delete all methods of the functions from the workspace."
function delete_funcs(notebook::Notebook, to_delete::Set{Symbol})
Expand Down
5 changes: 5 additions & 0 deletions src/webserver/Dynamic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,9 @@ end

responses[:getallnotebooks] = (initiator::Client, body, notebook=nothing) -> begin
putplutoupdates!(clientupdate_notebook_list(initiator, values(notebooks)))
end

responses[:interruptall] = (initiator::Client, body, notebook::Notebook) -> begin
success = WorkspaceManager.kill_workspace(initiator, notebook)
# TODO: notify user whether interrupt was successful (i.e. whether they are using a `ProcessWorkspace`)
end
2 changes: 1 addition & 1 deletion src/webserver/WebServer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ function run(port = 1234, launchbrowser = false)
end
catch e
if isa(e, InterruptException)
println("\nClosing Pluto... Bye! 🎈")
println("\nClosing Pluto... Have a nice day! 🎈")
close(serversocket)
for (uuid, ws) in WorkspaceManager.workspaces
WorkspaceManager.unmake_workspace(ws)
Expand Down

5 comments on commit ad60d95

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on ad60d95 Apr 2, 2020

Choose a reason for hiding this comment

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

@JuliaRegistrator register()

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on ad60d95 Apr 2, 2020

Choose a reason for hiding this comment

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

#42

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on ad60d95 Apr 2, 2020

Choose a reason for hiding this comment

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

#12

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

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

Registration pull request created: JuliaRegistries/General/11979

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.5.4 -m "<description of version>" ad60d950f558b7f81f6e360a04d124067863fab7
git push origin v0.5.4

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on ad60d95 Apr 2, 2020

Choose a reason for hiding this comment

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

cell stop demo

Please sign in to comment.