Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐡 Better error message when package fails to load #2925

Merged
merged 2 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/components/BottomRightPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { html, useState, useRef, useEffect, useMemo } from "../imports/Preact.js
import { cl } from "../common/ClassTable.js"

import { LiveDocsTab } from "./LiveDocsTab.js"
import { is_finished, ProcessTab, total_done, total_tasks, useStatusItem } from "./ProcessTab.js"
import { is_finished, StatusTab, total_done, total_tasks, useStatusItem } from "./StatusTab.js"
import { useMyClockIsAheadBy } from "../common/clock sync.js"
import { BackendLaunchPhase } from "../common/Binder.js"
import { useEventListener } from "../common/useEventListener.js"
Expand Down Expand Up @@ -140,7 +140,7 @@ export let BottomRightPanel = ({
sanitize_html=${sanitize_html}
/>`
: open_tab === "process"
? html`<${ProcessTab}
? html`<${StatusTab}
notebook=${notebook}
backend_launch_logs=${backend_launch_logs}
my_clock_is_ahead_by=${my_clock_is_ahead_by}
Expand Down
21 changes: 21 additions & 0 deletions frontend/components/ErrorMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { html, useContext, useEffect, useLayoutEffect, useRef, useState } from "
import { pluto_syntax_colors } from "./CellInput.js"
import { highlight } from "./CellOutput.js"
import { Editor } from "./Editor.js"
import { PkgTerminalView } from "./PkgTerminalView.js"

const extract_cell_id = (/** @type {string} */ file) => {
const sep_index = file.indexOf("#==#")
Expand Down Expand Up @@ -299,6 +300,26 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id }) => {
return Object.keys(erred_upstreams).length === 0
},
},
{
pattern: /^ArgumentError: Package (.*) not found in current path/,
display: (/** @type{string} */ x) => {
const match = x.match(/^ArgumentError: Package (.*) not found in current path/)
const package_name = (match?.[1] ?? "").replaceAll("`", "")

const pkg_terminal_value = pluto_actions.get_notebook()?.nbpkg?.terminal_outputs?.[package_name]

return html`<p>The package <strong>${package_name}.jl</strong> could not load because it failed to initialize.</p>
<p>That's not nice! Things you could try:</p>
<ul>
<li>Restart the notebook.</li>
<li>Try a different Julia version.</li>
<li>Contact the developers of ${package_name}.jl about this error.</li>
</ul>
<p>You might find useful information in the package installation log:</p>
<${PkgTerminalView} value=${pkg_terminal_value} />`
},
show_stacktrace: () => false,
},
default_rewriter,
]

Expand Down
2 changes: 1 addition & 1 deletion frontend/components/NotifyWhenDone.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { html, useEffect, useState } from "../imports/Preact.js"

import { cl } from "../common/ClassTable.js"
import { is_finished, total_done } from "./ProcessTab.js"
import { is_finished, total_done } from "./StatusTab.js"
import { useDelayedTruth } from "./BottomRightPanel.js"
import { url_logo_small } from "./Editor.js"
import { open_pluto_popup } from "../common/open_pluto_popup.js"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { NotifyWhenDone } from "./NotifyWhenDone.js"
* my_clock_is_ahead_by: number,
* }} props
*/
export let ProcessTab = ({ status, notebook, backend_launch_logs, my_clock_is_ahead_by }) => {
export const StatusTab = ({ status, notebook, backend_launch_logs, my_clock_is_ahead_by }) => {
return html`
<section>
<${StatusItem}
Expand Down Expand Up @@ -123,14 +123,14 @@ const StatusItem = ({ status_tree, path, my_clock_is_ahead_by, nbpkg, backend_la
const busy_time = Math.max(local_busy_time, mytime - start - (mystatus.timing === "local" ? 0 : my_clock_is_ahead_by))

useEffect(() => {
if (busy) {
if (busy || mystatus.success === false) {
let handle = setTimeout(() => {
set_is_open(true)
}, Math.max(100, 500 - path.length * 200))

return () => clearTimeout(handle)
}
}, [busy])
}, [busy || mystatus.success === false])

useEffectWithPrevious(
([old_finished]) => {
Expand Down
5 changes: 4 additions & 1 deletion frontend/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -2095,12 +2095,15 @@ pkg-terminal > .scroller {
width: 100%;
}

pkg-terminal pre {
body pkg-terminal:not(.asdf) pre:not(.asdf) {
white-space: pre-wrap;
word-break: break-all;
font-size: 0.6rem;
font-family: "Space Mono", monospace;
font-variant-ligatures: none;
margin: 0;
color: inherit;
background: none;
}

pkg-terminal .make-me-spin {
Expand Down
12 changes: 10 additions & 2 deletions src/packages/IOListener.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Base.@kwdef struct IOListener
end

function trigger(listener::IOListener)
if isreadable(listener.buffer)
if !eof(listener.buffer) && isreadable(listener.buffer)
newdata = readavailable(listener.buffer)
isempty(newdata) && return
s = String(newdata)
Expand Down Expand Up @@ -53,4 +53,12 @@ end
freeze_loading_spinners(s::AbstractString) = _replaceall(s, '◑' => '◐', '◒' => '◐', '◓' => '◐')

_replaceall(s, p) = replace(s, p)
_replaceall(s, p, ps...) = @static VERSION >= v"1.7" ? replace(s, p, ps...) : _replaceall(replace(s, p), ps...)
_replaceall(s, p, ps...) = @static VERSION >= v"1.7" ? replace(s, p, ps...) : _replaceall(replace(s, p), ps...)

phasemessage(iolistener, phase::String) = phasemessage(iolistener.buffer, phase)
function phasemessage(io::IO, phase::String)
ioc = IOContext(io, :color=>true)
printstyled(ioc, "\n$phase...\n"; bold=true)
printstyled(ioc, "===\n"; color=:light_black)
end

18 changes: 10 additions & 8 deletions src/packages/Packages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function sync_nbpkg_core(
reg = !PkgCompat._updated_registries_compat[]

# Print something in the terminal logs
println(iolistener.buffer, "Waiting for $(reg ? "the package registry to update" : "other notebooks to finish Pkg operations")...")
phasemessage(iolistener, "Waiting for $(reg ? "the package registry to update" : "other notebooks to finish Pkg operations")")
trigger(iolistener) # manual trigger because we did not start listening yet

# Create a business item
Expand Down Expand Up @@ -225,7 +225,7 @@ function sync_nbpkg_core(
Status.report_business_started!(pkg_status, :add)
start_time = time_ns()
with_io_setup(notebook, iolistener) do
println(iolistener.buffer, "\nAdding packages...")
phasemessage(iolistener, "Adding packages")

# We temporarily clear the "semver-compatible" [deps] entries, because Pkg already respects semver, unless it doesn't, in which case we don't want to force it.
PkgCompat.clear_auto_compat_entries!(notebook.nbpkg_ctx)
Expand Down Expand Up @@ -380,10 +380,11 @@ function sync_nbpkg(session, notebook, old_topology::NotebookTopology, new_topol
# TODO: send to user
showerror(stderr, e, bt)

error_text = sprint(showerror, e, bt)

error_text = e isa PrecompilationFailedException ? e.msg : sprint(showerror, e, bt)
for p in notebook.nbpkg_busy_packages
old = get(notebook.nbpkg_terminal_outputs, p, "")
notebook.nbpkg_terminal_outputs[p] = old * "\n\n\nPkg error!\n\n" * error_text
notebook.nbpkg_terminal_outputs[p] = old * "\n\n\e[1mPkg error!\e[22m\n" * error_text
end
notebook.nbpkg_busy_packages = String[]
update_nbpkg_cache!(notebook)
Expand Down Expand Up @@ -423,7 +424,7 @@ end
function _instantiate(notebook::Notebook, iolistener::IOListener)
start_time = time_ns()
with_io_setup(notebook, iolistener) do
println(iolistener.buffer, "\nInstantiating...")
phasemessage(iolistener, "Instantiating")
@debug "PlutoPkg: Instantiating" notebook.path

# update registries if this is the first time
Expand Down Expand Up @@ -453,7 +454,7 @@ end
function _precompile(notebook::Notebook, iolistener::IOListener, compiler_options::CompilerOptions)
start_time = time_ns()
with_io_setup(notebook, iolistener) do
println(iolistener.buffer, "\nPrecompiling...")
phasemessage(iolistener, "Precompiling")
@debug "PlutoPkg: Precompiling" notebook.path

env_dir = PkgCompat.env_dir(notebook.nbpkg_ctx)
Expand All @@ -468,7 +469,7 @@ end
function _resolve(notebook::Notebook, iolistener::IOListener)
startlistening(iolistener)
with_io_setup(notebook, iolistener) do
println(iolistener.buffer, "\nResolving...")
phasemessage(iolistener, "Resolving")
@debug "PlutoPkg: Resolving" notebook.path
Pkg.resolve(notebook.nbpkg_ctx)
end
Expand Down Expand Up @@ -555,7 +556,7 @@ function update_nbpkg_core(
cleanup_iolistener[] = () -> stoplistening(iolistener)

if !isready(pkg_token)
println(iolistener.buffer, "Waiting for other notebooks to finish Pkg operations...")
phasemessage(iolistener, "Waiting for other notebooks to finish Pkg operations")
trigger(iolistener)
end

Expand All @@ -575,6 +576,7 @@ function update_nbpkg_core(
end

with_io_setup(notebook, iolistener) do
phasemessage(iolistener, "Updating packages")
# We temporarily clear the "semver-compatible" [deps] entries, because it is difficult to update them after the update 🙈. TODO
PkgCompat.clear_auto_compat_entries!(notebook.nbpkg_ctx)

Expand Down
46 changes: 43 additions & 3 deletions src/packages/precompile_isolated.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,51 @@ function precompile_isolated(
"""

cmd = `$(Base.julia_cmd()[1]) $(flags) -e $(code)`

stderr_buffer = IOBuffer()
stderr_capture = tee_io(stderr, stderr_buffer) # not to io because any stderr content will be shown eventually by the `error`.

Base.run(pipeline(
cmd; stdout=io, #dont capture stderr because we want it to show in the server terminal when something goes wrong
))
try
Base.run(pipeline(
cmd; stdout=io, stderr=stderr_capture.io,
))
catch e
if e isa ProcessFailedException
throw(PrecompilationFailedException("Precompilation failed\n\n$(String(take!(stderr_buffer)))"))
else
rethrow(e)
end
finally
stderr_capture.close()
end

# In the future we could allow interrupting the precompilation process (e.g. when the notebook is shut down)
# by running this code using Malt.jl
end

struct PrecompilationFailedException <: Exception
msg::String
end

# Create a new IO object that redirects all writes to the given capture IOs. It's like the `tee` linux command. Return a named tuple with the IO object and a function to close it which you should not forget to call.
function tee_io(captures...)
bs = Base.BufferStream()

t = @async begin
while !eof(bs)
data = readavailable(bs)
isempty(data) && continue

for s in captures
write(s, data)
end
end
end

function closeme()
close(bs)
wait(t)
end

return (io=bs, close=closeme)
end
14 changes: 9 additions & 5 deletions src/webserver/Status.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,15 @@ report_business_started!(parent::Business, name::Symbol) = get_child(parent, nam
report_business_planned!(parent::Business, name::Symbol) = get_child(parent, name)


report_business!(f::Function, parent::Business, args...) = try
report_business_started!(parent, args...)
f()
finally
report_business_finished!(parent, args...)
function report_business!(f::Function, parent::Business, name::Symbol)
local success = false
try
report_business_started!(parent, name)
f()
success = true
finally
report_business_finished!(parent, name, success)
end
end

delete_business!(business::Business, name::Symbol) = lock(business.lock) do
Expand Down
Loading