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:
+
+ - Restart the notebook.
+ - Try a different Julia version.
+ - Contact the developers of ${package_name}.jl about this error.
+
+ 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