Skip to content

Commit

Permalink
Add ability to interrupt running Julia tasks from Emacs.
Browse files Browse the repository at this point in the history
  • Loading branch information
gcv committed Aug 4, 2023
1 parent b51f8ce commit a88755d
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

- Looking up completions now works even if there is a running computational task in the REPL ([#123](https://github.com/gcv/julia-snail/issues/123)).


### Added

- Support for Eat as an alternative to vterm.
- `julia-snail-interrupt-task`: a new interface function which allows interrupting a running computational task in the Julia REPL ([#104](https://github.com/gcv/julia-snail/issues/104)).


### Changed
Expand Down
79 changes: 67 additions & 12 deletions JuliaSnail.jl
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,28 @@ end
end


### --- task handling code

module Tasks

import Printf

active_tasks_lock = ReentrantLock()
active_tasks = Dict{String, Task}()

function interrupt(reqid)
if haskey(active_tasks, reqid)
task = active_tasks[reqid]
schedule(task, InterruptException(), error=true)
return Printf.@sprintf("Interrupt scheduled for Julia reqid %s", reqid)
else
return Printf.@sprintf("Unknown reqid %s on the Julia side", reqid)
end
end

end


### --- server code

running = false
Expand Down Expand Up @@ -869,31 +891,64 @@ function start(port=10011; addr="127.0.0.1")
@async while Sockets.isopen(client) && !eof(client)
command = readline(client, keep=true)
input = nothing
expr = nothing
current_reqid = nothing
try
input = eval(Meta.parse(command))
expr = Meta.parse(input.code)
result = eval_in_module(input.ns, expr)
# report successful evaluation back to client
current_reqid = input.reqid
catch err
# probably a parsing error
resp = elexpr([
Symbol("julia-snail--response-success"),
Symbol("julia-snail--response-failure"),
input.reqid,
result
sprint(showerror, err),
tuple(string.(stacktrace(catch_backtrace()))...)
])
send_to_client(resp, client)
catch err
continue
end
active_task = @task begin # process input
try
result = eval_in_module(input.ns, expr)
# report successful evaluation back to client
resp = elexpr([
Symbol("julia-snail--response-failure"),
Symbol("julia-snail--response-success"),
input.reqid,
sprint(showerror, err),
tuple(string.(stacktrace(catch_backtrace()))...)
result
])
send_to_client(resp, client)
catch err2
# internal Snail error or unexpected IO behavior..?
println("JuliaSnail: something broke: ", sprint(showerror, err2))
end
catch err
if isa(err, InterruptException)
resp = elexpr([
Symbol("julia-snail--response-interrupt"),
input.reqid
])
send_to_client(resp, client)
else
try
resp = elexpr([
Symbol("julia-snail--response-failure"),
input.reqid,
sprint(showerror, err),
tuple(string.(stacktrace(catch_backtrace()))...)
])
send_to_client(resp, client)
catch err2
# internal Snail error or unexpected IO behavior..?
println(stderr, "JuliaSnail: something broke in reqid: ", input.reqid, "; ", sprint(showerror, err2))
end
end
finally
lock(Tasks.active_tasks_lock) do
delete!(Tasks.active_tasks, current_reqid)
end
end # process input
end
lock(Tasks.active_tasks_lock) do
Tasks.active_tasks[current_reqid] = active_task
end
schedule(active_task)
end # async while loop for client connection
end
close(server_socket)
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ https://user-images.githubusercontent.com/10327/128589405-7368bb50-0ef3-4003-b5d

## Installation

Julia versions >1.2.0 should all work with Snail.
Julia versions >1.6.0 should all work with Snail.

Snail’s Julia-side dependencies will automatically be installed when it starts, and will stay out of your way using Julia’s [`LOAD_PATH` mechanism](https://docs.julialang.org/en/v1/base/constants/#Base.LOAD_PATH).

Expand Down Expand Up @@ -229,6 +229,8 @@ In addition, most `xref` commands are available (except `xref-find-references`).

Completion also works. Emacs built-in completion features, as well as `company-complete`, will do a reasonable job of finding the right completions in the context of the current module (though will not pick up local variables). Completion is current-module aware.

**Experimental feature:** To interrupt a Julia task started from the Emacs side (e.g. a long-running computation started with `julia-snail-send-line`), use `julia-snail-interrupt-task`. When only one task is running, Snail will simply try to terminate it. With multiple tasks, the user will be prompted for a request ID. _This is currently an opaque identifier, and the interface will be improved in the future._


### Multiple Julia versions

Expand Down
29 changes: 29 additions & 0 deletions julia-snail.el
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,10 @@ evaluated in the context of MODULE."
(funcall callback-failure request-info))))
(julia-snail--response-base reqid))

(defun julia-snail--response-interrupt (reqid)
"Snail task interruption response handler for REQID."
(julia-snail--response-base reqid))


;;; --- CST parser interface

Expand Down Expand Up @@ -1889,6 +1893,31 @@ autocompletion aware of the available modules."
(message "Caches updated: parent module %s"
(julia-snail--construct-module-path module))))

(defun julia-snail-interrupt-task ()
"Try to interrupt a Julia computation task which was started on the Emacs side."
(interactive)
(let* ((running-reqids (hash-table-keys julia-snail--requests))
;; TODO: Filter these reqids for valid ones (e.g. ones with valid REPL buffers?)
(reqid (cond ((= 0 (length running-reqids))
(message "No Julia tasks currently running (that the Emacs side knows about)")
nil)
((= 1 (length running-reqids))
(-first-item running-reqids))
(t
(completing-read "Select id of Julia request to interrupt"
;; TODO: This needs to include metadata and annotations for what
;; computational task each reqid corresponds to (like a line number
;; or the actual code).
running-reqids)))))
(when reqid
(let ((repl-buf (get-buffer julia-snail-repl-buffer)))
(message
(julia-snail--send-to-server
'("JuliaSnail" "Tasks")
(format "interrupt(\"%s\")" reqid)
:repl-buf repl-buf
:async nil))))))


;;; --- keymaps

Expand Down

0 comments on commit a88755d

Please sign in to comment.