diff --git a/frontend/components/BottomRightPanel.js b/frontend/components/BottomRightPanel.js index 40fe5ff245..a3272e06aa 100644 --- a/frontend/components/BottomRightPanel.js +++ b/frontend/components/BottomRightPanel.js @@ -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" @@ -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} diff --git a/frontend/components/ErrorMessage.js b/frontend/components/ErrorMessage.js index 883a80efe7..91ae58bb13 100644 --- a/frontend/components/ErrorMessage.js +++ b/frontend/components/ErrorMessage.js @@ -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("#==#") @@ -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`

The package ${package_name}.jl could not load because it failed to initialize.

+

That's not nice! Things you could try:

+ +

You might find useful information in the package installation log:

+ <${PkgTerminalView} value=${pkg_terminal_value} />` + }, + show_stacktrace: () => false, + }, default_rewriter, ] diff --git a/frontend/components/NotifyWhenDone.js b/frontend/components/NotifyWhenDone.js index e9fe347461..e6a510a3c5 100644 --- a/frontend/components/NotifyWhenDone.js +++ b/frontend/components/NotifyWhenDone.js @@ -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" diff --git a/frontend/components/ProcessTab.js b/frontend/components/StatusTab.js similarity index 98% rename from frontend/components/ProcessTab.js rename to frontend/components/StatusTab.js index 0d28d01a12..1bc411f81e 100644 --- a/frontend/components/ProcessTab.js +++ b/frontend/components/StatusTab.js @@ -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`
<${StatusItem} @@ -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]) => { diff --git a/frontend/editor.css b/frontend/editor.css index c21b9f8d05..bc150d3e85 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -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 { diff --git a/src/packages/IOListener.jl b/src/packages/IOListener.jl index 50ec615438..ed4f1d03d6 100644 --- a/src/packages/IOListener.jl +++ b/src/packages/IOListener.jl @@ -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) @@ -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...) \ No newline at end of file +_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 + diff --git a/src/packages/Packages.jl b/src/packages/Packages.jl index 1e91480b4c..647def9ebd 100644 --- a/src/packages/Packages.jl +++ b/src/packages/Packages.jl @@ -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 @@ -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) @@ -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) @@ -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 @@ -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) @@ -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 @@ -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 @@ -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) diff --git a/src/packages/precompile_isolated.jl b/src/packages/precompile_isolated.jl index b1619181ce..341cbd3d30 100644 --- a/src/packages/precompile_isolated.jl +++ b/src/packages/precompile_isolated.jl @@ -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 diff --git a/src/webserver/Status.jl b/src/webserver/Status.jl index c84dd74c18..4c37c15de8 100644 --- a/src/webserver/Status.jl +++ b/src/webserver/Status.jl @@ -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