beatrina 0.8.6 → 0.9.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICES +1 -1
- package/README.md +6 -0
- package/beatrina_V0.9.43.html +1885 -0
- package/beatrina_V0.9.43.html.inputs.json +1 -0
- package/bin/beatrina.mjs +156 -26
- package/bin/browser.mjs +31 -0
- package/bin/cli.mjs +64 -5
- package/bin/doctor-rows.mjs +31 -0
- package/bin/failsafe.mjs +8 -3
- package/bin/finder.mjs +74 -0
- package/bin/lsquery.swift +55 -0
- package/bin/pages.mjs +81 -0
- package/bin/python-setup.mjs +207 -0
- package/bin/runtime-dirs.mjs +27 -0
- package/bin/sessions.mjs +30 -10
- package/bin/shortcut.mjs +271 -35
- package/bin/update-check.mjs +2 -2
- package/build-info.json +1 -1
- package/engines/js/worker.mjs +6 -3
- package/engines/python/adapter.py +14 -13
- package/engines/python/analyze.py +4 -2
- package/engines/python/bootstrap.py +32 -31
- package/engines/python/debugger.py +10 -6
- package/engines/python/engine.json +1 -1
- package/engines/python/handoff.py +4 -2
- package/engines/python/worker.py +39 -10
- package/engines/r/engine.json +1 -1
- package/engines/r/handoff.R +4 -3
- package/failsafe/ai-policy.R +16 -16
- package/failsafe/ai-store.R +6 -6
- package/failsafe/cite.R +11 -11
- package/failsafe/journal.R +11 -11
- package/failsafe/plugins.R +27 -27
- package/failsafe/serve.R +364 -239
- package/failsafe/session-documents.R +104 -0
- package/host/ai-authority.mjs +55 -0
- package/host/ai-policy.mjs +48 -26
- package/host/bundle.mjs +194 -0
- package/host/deployment.mjs +25 -18
- package/host/engine-js.mjs +16 -17
- package/host/engine-pool.mjs +56 -8
- package/host/engine-python.mjs +80 -31
- package/host/engine-r.mjs +38 -32
- package/host/engine-stdio.mjs +45 -12
- package/host/env-names.mjs +148 -0
- package/host/gateway-token.mjs +51 -0
- package/host/journal-store.mjs +10 -9
- package/host/main.mjs +216 -76
- package/host/payload.mjs +131 -0
- package/host/planes/README.md +1 -1
- package/host/planes/ai-store.mjs +6 -5
- package/host/planes/ai.mjs +49 -29
- package/host/planes/analyze.mjs +6 -5
- package/host/planes/bundle.mjs +344 -0
- package/host/planes/choose.mjs +404 -0
- package/host/planes/cite.mjs +18 -17
- package/host/planes/files.mjs +0 -0
- package/host/planes/jobs.mjs +43 -20
- package/host/planes/latex.mjs +41 -5
- package/host/planes/mcp.mjs +80 -91
- package/host/planes/pair.mjs +24 -24
- package/host/planes/pipe-term.mjs +2 -2
- package/host/planes/plugins.mjs +1 -1
- package/host/planes/recent-documents.mjs +170 -0
- package/host/planes/sessions.mjs +230 -82
- package/host/planes/settings.mjs +4 -4
- package/host/planes/terminal.mjs +13 -11
- package/host/planes/test-file.mjs +2 -2
- package/host/planes/update.mjs +6 -13
- package/host/plugin-store.mjs +35 -52
- package/host/recent-documents.mjs +124 -0
- package/host/runtime-dir.mjs +47 -0
- package/host/server.mjs +123 -21
- package/host/session-keep.mjs +70 -0
- package/host/settings.mjs +85 -37
- package/host/update-record.mjs +3 -2
- package/host/user-dirs.mjs +60 -18
- package/host/which.mjs +39 -0
- package/host/windows-runtime.mjs +6 -3
- package/host/worker-plane.mjs +80 -7
- package/host/ws.mjs +9 -2
- package/host/zip.mjs +237 -0
- package/kernel/analyze.R +1 -1
- package/{check → kernel/check}/acceptance.mjs +1 -1
- package/kernel/check/knit-file.mjs +19639 -0
- package/{check → kernel/check}/session.mjs +49 -8
- package/kernel/deployment.R +20 -20
- package/kernel/examples/NOTICE.md +1 -1
- package/kernel/fileio.R +6 -6
- package/kernel/index.html +2 -2
- package/kernel/job-run.R +83 -22
- package/kernel/jobs.R +28 -12
- package/kernel/kernel-version +1 -1
- package/kernel/kernel.R +24 -21
- package/kernel/knitr-run.R +50 -9
- package/kernel/latex.R +429 -30
- package/kernel/mcp/{carmar-mcp.mjs → beatrina-mcp.mjs} +249 -59
- package/kernel/notebook-page.R +7 -7
- package/kernel/project.R +10 -10
- package/kernel/settings.R +105 -56
- package/kernel/sniff.R +4 -4
- package/kernel/typst.R +121 -0
- package/kernel/worker.R +602 -123
- package/kernel/workspace-keep.R +188 -0
- package/lib/agent-authoring-contract.js +28 -19
- package/lib/cell-kinds.js +3 -3
- package/lib/engine-labels.js +4 -4
- package/menu/Beatrina Menu.app/Contents/Info.plist +14 -0
- package/menu/Beatrina Menu.app/Contents/MacOS/Beatrina Menu +0 -0
- package/menu/Beatrina Menu.app/Contents/_CodeSignature/CodeResources +115 -0
- package/package.json +4 -3
- package/carmar_V0.8.6.html +0 -1310
package/kernel/worker.R
CHANGED
|
@@ -63,9 +63,9 @@ local({
|
|
|
63
63
|
# interactive worker MUST read through R's console (stdin()), because that is
|
|
64
64
|
# the one reader a native browser() prompt shares — a second buffered reader
|
|
65
65
|
# on the same fd would steal bytes from the debugger.
|
|
66
|
-
WORKER_MODE <- Sys.getenv("
|
|
66
|
+
WORKER_MODE <- Sys.getenv("BEATRINA_WORKER_MODE", "batch")
|
|
67
67
|
sentinel <- if (identical(WORKER_MODE, "interactive")) {
|
|
68
|
-
Sys.getenv("
|
|
68
|
+
Sys.getenv("BEATRINA_SENTINEL", "")
|
|
69
69
|
} else {
|
|
70
70
|
args <- commandArgs(trailingOnly = TRUE)
|
|
71
71
|
if (length(args) >= 1L) args[1] else ""
|
|
@@ -75,13 +75,89 @@ stopifnot(nzchar(sentinel))
|
|
|
75
75
|
# mode — a comment, so a line that ever reached R's raw top level would be
|
|
76
76
|
# inert. The prefix is stripped here before parsing.
|
|
77
77
|
CMD_PREFIX <- local({
|
|
78
|
-
tag <- Sys.getenv("
|
|
78
|
+
tag <- Sys.getenv("BEATRINA_CMD_TAG", "")
|
|
79
79
|
if (nzchar(tag)) paste0("#", tag, " ") else ""
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
PLOT_WIDTH <- 900L
|
|
83
83
|
PLOT_HEIGHT <- 620L
|
|
84
84
|
PLOT_RES <- 110L
|
|
85
|
+
|
|
86
|
+
#' knitr's OWN figure defaults, captured before any user code runs.
|
|
87
|
+
#'
|
|
88
|
+
#' The baseline that lets knitr_dim_defaults() tell "this document asked for a
|
|
89
|
+
#' 10-inch figure" from "knitr answered with its own 7-inch default". Read at
|
|
90
|
+
#' load time on purpose: after a setup chunk has run it is no longer pristine.
|
|
91
|
+
KNITR_FIG_BASE <- local({
|
|
92
|
+
if (!requireNamespace("knitr", quietly = TRUE)) return(NULL)
|
|
93
|
+
tryCatch(knitr::opts_chunk$get(c("fig.width", "fig.height", "dpi")),
|
|
94
|
+
error = function(e) NULL)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
#' The document's own figure defaults, from knitr.
|
|
98
|
+
#'
|
|
99
|
+
#' `knitr::opts_chunk$set(fig.width = 10, dpi = 150)` in a setup chunk is how
|
|
100
|
+
#' every R author states what size their figures are, and until 2026-09-18 an
|
|
101
|
+
#' interactive run ignored it completely: the call looked like configuration
|
|
102
|
+
#' and did nothing, and the PNGs came back at this file's constants whatever
|
|
103
|
+
#' the document said. knitr sizes in INCHES and carries the resolution
|
|
104
|
+
#' separately, which is exactly a raster device's width = inches * dpi.
|
|
105
|
+
#'
|
|
106
|
+
#' Precedence, widest to narrowest: what the page sent for THIS run (the
|
|
107
|
+
#' chunk's own fig.width, or the editor's device settings) wins, because it is
|
|
108
|
+
#' more specific than a document default; then these; then the constants.
|
|
109
|
+
#'
|
|
110
|
+
#' @return A list with any of width/height/res in pixels, or an empty list.
|
|
111
|
+
knitr_dim_defaults <- function() {
|
|
112
|
+
if (!requireNamespace("knitr", quietly = TRUE)) return(list())
|
|
113
|
+
got <- tryCatch(knitr::opts_chunk$get(c("fig.width", "fig.height", "dpi")),
|
|
114
|
+
error = function(e) NULL)
|
|
115
|
+
if (!is.list(got)) return(list())
|
|
116
|
+
# Only what the DOCUMENT set. knitr always answers with something — its own
|
|
117
|
+
# defaults are 7in by 7in at 72 dpi — so honouring whatever it says would
|
|
118
|
+
# shrink every plot in every notebook that never mentioned figures, from 900
|
|
119
|
+
# pixels to 504. KNITR_FIG_BASE is knitr's pristine answer, captured at boot
|
|
120
|
+
# before a line of anyone's code has run; a value equal to it was not asked
|
|
121
|
+
# for by this document and is ignored.
|
|
122
|
+
base <- KNITR_FIG_BASE
|
|
123
|
+
if (is.list(base)) {
|
|
124
|
+
for (key in c("fig.width", "fig.height", "dpi")) {
|
|
125
|
+
if (!is.null(base[[key]]) && !is.null(got[[key]]) && isTRUE(all.equal(base[[key]], got[[key]]))) got[[key]] <- NULL
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
num <- function(x) if (is.numeric(x) && length(x) == 1L && is.finite(x) && x > 0) as.numeric(x) else NULL
|
|
129
|
+
dpi <- num(got$dpi)
|
|
130
|
+
w <- num(got$fig.width)
|
|
131
|
+
h <- num(got$fig.height)
|
|
132
|
+
out <- list()
|
|
133
|
+
if (!is.null(dpi)) out$res <- as.integer(round(dpi))
|
|
134
|
+
# Inches become pixels only when there is a resolution to multiply by; with
|
|
135
|
+
# no dpi set, knitr's own default of 72 is what a figure of that width means.
|
|
136
|
+
scale <- if (!is.null(dpi)) dpi else 72
|
|
137
|
+
if (!is.null(w)) out$width <- as.integer(round(w * scale))
|
|
138
|
+
if (!is.null(h)) out$height <- as.integer(round(h * scale))
|
|
139
|
+
out
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#' The document's chosen display precision, or NULL if it never chose one.
|
|
143
|
+
#'
|
|
144
|
+
#' R's factory default is 7. A document that says `options(digits = 3)` means
|
|
145
|
+
#' its tables to read at three significant digits, and says it once, in a setup
|
|
146
|
+
#' chunk — so the value travels with every frame rather than being asked for
|
|
147
|
+
#' per table.
|
|
148
|
+
display_digits <- function() {
|
|
149
|
+
d <- getOption("digits", 7L)
|
|
150
|
+
if (!is.numeric(d) || length(d) != 1L || !is.finite(d) || d == 7L) return(NULL)
|
|
151
|
+
as.integer(max(1L, min(22L, round(d))))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#' One dimension, resolved: this run's value, else the document's, else ours.
|
|
155
|
+
dim_or <- function(value, key) {
|
|
156
|
+
if (!is.null(value)) return(value)
|
|
157
|
+
doc <- knitr_dim_defaults()
|
|
158
|
+
if (!is.null(doc[[key]])) return(doc[[key]])
|
|
159
|
+
switch(key, width = PLOT_WIDTH, height = PLOT_HEIGHT, res = PLOT_RES)
|
|
160
|
+
}
|
|
85
161
|
# Hard ceilings for the data viewer. Server-side because the client is one
|
|
86
162
|
# `limit: 1e9` typo away from asking for everything; the reply reports what was
|
|
87
163
|
# clamped, so the UI never has to guess what it actually received.
|
|
@@ -114,13 +190,13 @@ try(utils::rc.settings(ipck = TRUE, func = TRUE, args = TRUE, files = TRUE),
|
|
|
114
190
|
# is started by three different launchers (kernel.R, the Chrome bridge host,
|
|
115
191
|
# and the packaged inst/app/kernel) and none of them guarantee a cwd.
|
|
116
192
|
import_sources <- local({
|
|
117
|
-
env_dir <- Sys.getenv("
|
|
193
|
+
env_dir <- Sys.getenv("BEATRINA_WORKER_DIR", "")
|
|
118
194
|
file_arg <- grep("^--file=", commandArgs(FALSE), value = TRUE)
|
|
119
195
|
here <- if (nzchar(env_dir)) env_dir
|
|
120
196
|
else if (length(file_arg)) {
|
|
121
197
|
dirname(normalizePath(sub("^--file=", "", file_arg[1L]), mustWork = FALSE))
|
|
122
198
|
} else getwd()
|
|
123
|
-
file.path(here, c("fileio.R", "sniff.R", "project.R"))
|
|
199
|
+
file.path(here, c("fileio.R", "sniff.R", "project.R", "workspace-keep.R", "deployment.R"))
|
|
124
200
|
})
|
|
125
201
|
# environment(), not parent.frame(): inside a nested local() the parent frame
|
|
126
202
|
# is the eval machinery's, not this file's private scope, and the functions
|
|
@@ -142,9 +218,9 @@ invisible(lapply(Filter(file.exists, import_sources),
|
|
|
142
218
|
# user already set: their .Rprofile loads here (we deliberately do not use
|
|
143
219
|
# --vanilla), and it is where r-universe and institutional mirrors live.
|
|
144
220
|
local({
|
|
145
|
-
managed_mirror <- trimws(Sys.getenv("
|
|
221
|
+
managed_mirror <- trimws(Sys.getenv("BEATRINA_CRAN_MIRROR", ""))
|
|
146
222
|
if (nzchar(managed_mirror) && !grepl("^https://", managed_mirror)) {
|
|
147
|
-
stop("
|
|
223
|
+
stop("BEATRINA_CRAN_MIRROR must be an HTTPS URL.")
|
|
148
224
|
}
|
|
149
225
|
repos <- getOption("repos")
|
|
150
226
|
cran <- if (is.null(repos)) NA_character_ else unname(repos["CRAN"])
|
|
@@ -191,7 +267,7 @@ R_KEYWORDS <- c("if", "else", "repeat", "while", "function", "for", "in",
|
|
|
191
267
|
#' CAIRO IS TRIED BEFORE QUARTZ, and that ORDER is a hang fix, not a
|
|
192
268
|
#' preference. `png(type="quartz")` pulls in the macOS Aqua/AppKit graphics
|
|
193
269
|
#' backend, which spawns a Cocoa event loop (an NSEventThread plus grDevices'
|
|
194
|
-
#' own ELThread) the first time it opens.
|
|
270
|
+
#' own ELThread) the first time it opens. Beatrina's worker is a BackgroundOnly
|
|
195
271
|
#' process with no window-server access, so when R pumps that event loop
|
|
196
272
|
#' mid-evaluation — which it does from inside a long R-level loop, e.g. the
|
|
197
273
|
#' permutation loop in `markov_order_test` — `ReceiveNextEventCommon` blocks
|
|
@@ -391,7 +467,10 @@ sys.source(file.path(here, "knitr-run.R"), envir = environment())
|
|
|
391
467
|
# code and every package keep seeing base's own function, and removing the
|
|
392
468
|
# attached frame restores the original behaviour exactly. The delegation is the
|
|
393
469
|
# whole implementation — announce, then let R do what it already did.
|
|
394
|
-
|
|
470
|
+
# The console reader itself, captured BEFORE the namespaces are patched below:
|
|
471
|
+
# the announcing wrapper must call the real one, never itself.
|
|
472
|
+
BASE_READLINE <- if (is.null(attr(base::readline, "beatrina"))) base::readline else attr(base::readline, "beatrina")
|
|
473
|
+
beatrina_readline <- function(prompt = "") {
|
|
395
474
|
text <- tryCatch(as.character(prompt)[[1]], error = function(e) "")
|
|
396
475
|
if (!length(text) || is.na(text)) text <- ""
|
|
397
476
|
# The question belongs to the RUN that asked it, and says so.
|
|
@@ -413,7 +492,7 @@ carmar_readline <- function(prompt = "") {
|
|
|
413
492
|
# id by finding the first `"id":…` in the worker's own bytes, so a prompt
|
|
414
493
|
# containing that text would otherwise be rewritten instead of the id.
|
|
415
494
|
emit(list(type = "input_request", id = RUN_STATE$id, prompt = text))
|
|
416
|
-
answer <-
|
|
495
|
+
answer <- BASE_READLINE(prompt)
|
|
417
496
|
# Symmetry matters more than it looks: a page that opened an input row on the
|
|
418
497
|
# request must be told to close it, INCLUDING when the answer arrived by some
|
|
419
498
|
# other route (an interrupt, a second page). Without this the prompt row
|
|
@@ -438,7 +517,7 @@ carmar_readline <- function(prompt = "") {
|
|
|
438
517
|
# RUN_STATE carries the current run's id from run_cell to the shadow.
|
|
439
518
|
RUN_STATE <- new.env(parent = emptyenv())
|
|
440
519
|
RUN_STATE$id <- NULL
|
|
441
|
-
|
|
520
|
+
beatrina_print <- function(x, ...) {
|
|
442
521
|
if (is.null(RUN_STATE$id) || length(list(...)) || sink.number() > (RUN_STATE$capture_depth %||% 0L)) {
|
|
443
522
|
return(base::print(x, ...))
|
|
444
523
|
}
|
|
@@ -457,8 +536,8 @@ carmar_print <- function(x, ...) {
|
|
|
457
536
|
}
|
|
458
537
|
base::print(x, ...)
|
|
459
538
|
}
|
|
460
|
-
INPUT_SHADOW <- "
|
|
461
|
-
|
|
539
|
+
INPUT_SHADOW <- "beatrina:input"
|
|
540
|
+
beatrina_flush_console <- function() {
|
|
462
541
|
if (!is.null(KNIT_CAPTURE)) evaluate::flush_console()
|
|
463
542
|
else utils::flush.console()
|
|
464
543
|
invisible(NULL)
|
|
@@ -467,10 +546,136 @@ if (!(INPUT_SHADOW %in% search())) {
|
|
|
467
546
|
# warn.conflicts = FALSE: masking `readline` (and `print`) is the entire
|
|
468
547
|
# point, and a startup warning about it would be printed into the user's
|
|
469
548
|
# first cell.
|
|
470
|
-
attach(list(readline =
|
|
549
|
+
attach(list(readline = beatrina_readline, print = beatrina_print, flush.console = beatrina_flush_console),
|
|
471
550
|
name = INPUT_SHADOW, warn.conflicts = FALSE)
|
|
472
551
|
}
|
|
473
552
|
|
|
553
|
+
# Console reads R's OWN helpers make are announced too (2026-09-19). The shadow
|
|
554
|
+
# above only answers for readline() typed in the user's code: askYesNo(),
|
|
555
|
+
# menu(), select.list(), scan("") and every package reach base and utils
|
|
556
|
+
# through their NAMESPACES, where a search-path shadow is invisible — measured:
|
|
557
|
+
# menu() printed its choices and the run sat blocked with no prompt on the
|
|
558
|
+
# page, and so did askYesNo, select.list and scan. So the readers are announced
|
|
559
|
+
# where everything funnels through them:
|
|
560
|
+
# - base::readline is the announcing reader in base's namespace and package
|
|
561
|
+
# env, so askYesNo() and every package's readline() ride it;
|
|
562
|
+
# - utils::menu's console loop is re-expressed over it (the original reads
|
|
563
|
+
# the console from C, C_menu, which nothing at R level can observe);
|
|
564
|
+
# - scan() from the console collects its lines through it and hands them to
|
|
565
|
+
# the real scan() as `text`.
|
|
566
|
+
# Each READ is announced, so a menu that re-asks after a bad answer opens a
|
|
567
|
+
# fresh prompt instead of having its answer refused by the one-line gate.
|
|
568
|
+
# The originals are kept (as the "beatrina" attribute, BASE_*), and the
|
|
569
|
+
# patching is idempotent.
|
|
570
|
+
|
|
571
|
+
#' Put `value` in place of `name` in a package's namespace and its attached env.
|
|
572
|
+
#' @return Invisibly, whether every present binding was replaced.
|
|
573
|
+
beatrina_patch <- function(name, value, pkg) {
|
|
574
|
+
envs <- Filter(Negate(is.null), list(asNamespace(pkg),
|
|
575
|
+
if (paste0("package:", pkg) %in% search()) as.environment(paste0("package:", pkg)) else NULL))
|
|
576
|
+
done <- vapply(envs, function(env) {
|
|
577
|
+
if (!exists(name, envir = env, inherits = FALSE)) return(TRUE)
|
|
578
|
+
locked <- bindingIsLocked(name, env)
|
|
579
|
+
if (locked) unlockBinding(name, env)
|
|
580
|
+
on.exit(if (locked) lockBinding(name, env), add = TRUE)
|
|
581
|
+
assign(name, value, envir = env)
|
|
582
|
+
TRUE
|
|
583
|
+
}, logical(1))
|
|
584
|
+
invisible(all(done))
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#' menu() over the announced reader, faithful to utils::menu's console path:
|
|
588
|
+
#' the same layout, a number or an exact choice, 0 to exit, and the same
|
|
589
|
+
#' sentence on a bad answer. The graphics path is left to the original.
|
|
590
|
+
#' @return The chosen index, or 0.
|
|
591
|
+
BASE_MENU <- if (is.null(attr(utils::menu, "beatrina"))) utils::menu else attr(utils::menu, "beatrina")
|
|
592
|
+
beatrina_menu <- function(choices, graphics = FALSE, title = NULL) {
|
|
593
|
+
if (!interactive()) stop("menu() cannot be used non-interactively")
|
|
594
|
+
if (isTRUE(graphics)) return(BASE_MENU(choices, graphics = graphics, title = title))
|
|
595
|
+
choices <- as.character(choices)
|
|
596
|
+
nc <- length(choices)
|
|
597
|
+
if (length(title) && nzchar(title[1L])) cat(title[1L], "\n")
|
|
598
|
+
op <- paste0(format(seq_len(nc)), ": ", choices)
|
|
599
|
+
if (nc > 10L) {
|
|
600
|
+
fop <- format(op)
|
|
601
|
+
nw <- nchar(fop[1L], "w") + 2L
|
|
602
|
+
ncol <- getOption("width") %/% nw
|
|
603
|
+
if (ncol > 1L) op <- paste0(fop, c(rep.int(" ", min(nc, ncol) - 1L), "\n"), collapse = "")
|
|
604
|
+
}
|
|
605
|
+
cat("", op, "", sep = "\n")
|
|
606
|
+
# Recursion, not a loop: ask until the answer names an item (R's own menu
|
|
607
|
+
# repeats the same way; a person typing nonsense is the only way round).
|
|
608
|
+
ask <- function() {
|
|
609
|
+
ans <- trimws(beatrina_readline("Selection: "))
|
|
610
|
+
ind <- if (grepl("^[0-9]", ans)) suppressWarnings(as.integer(sub("^([0-9]+).*$", "\\1", ans)))
|
|
611
|
+
else match(ans, choices, nomatch = nc + 1L)
|
|
612
|
+
if (!is.na(ind) && ind <= nc) return(ind)
|
|
613
|
+
# gettext() trims the trailing newline off the message; it is said apart.
|
|
614
|
+
cat(gettext("Enter an item from the menu, or 0 to exit", domain = "R-utils"), "\n", sep = "")
|
|
615
|
+
ask()
|
|
616
|
+
}
|
|
617
|
+
ask()
|
|
618
|
+
}
|
|
619
|
+
attr(beatrina_menu, "beatrina") <- BASE_MENU
|
|
620
|
+
|
|
621
|
+
#' scan() from the console, through the announced reader. Lines are read the
|
|
622
|
+
#' way the console scan reads them — one prompt per line, numbered by the next
|
|
623
|
+
#' item, until a blank line, `nlines` lines, or `n`/`nmax` items — then the real
|
|
624
|
+
#' scan() parses them as `text`, so every argument keeps its meaning.
|
|
625
|
+
BASE_SCAN <- if (is.null(attr(base::scan, "beatrina"))) base::scan else attr(base::scan, "beatrina")
|
|
626
|
+
beatrina_scan <- BASE_SCAN
|
|
627
|
+
body(beatrina_scan) <- quote({
|
|
628
|
+
call <- match.call()
|
|
629
|
+
call[[1L]] <- BASE_SCAN
|
|
630
|
+
from_console <- is.character(file) && identical(file, "") && missing(text)
|
|
631
|
+
if (!from_console) return(eval(call, parent.frame()))
|
|
632
|
+
per_line <- function(line) {
|
|
633
|
+
parts <- if (nzchar(sep)) strsplit(line, sep, fixed = TRUE)[[1L]] else strsplit(trimws(line), "[ \t]+")[[1L]]
|
|
634
|
+
length(parts[nzchar(parts)])
|
|
635
|
+
}
|
|
636
|
+
want <- max(c(n, nmax, 0L)[c(n, nmax, 0L) > 0L][1L], 0L, na.rm = TRUE)
|
|
637
|
+
collect <- function(lines, items) {
|
|
638
|
+
if (nlines > 0L && length(lines) >= nlines) return(lines)
|
|
639
|
+
if (want > 0L && items >= want) return(lines)
|
|
640
|
+
line <- beatrina_readline(sprintf("%d: ", items + 1L))
|
|
641
|
+
if (!nzchar(trimws(line))) return(lines)
|
|
642
|
+
collect(c(lines, line), items + per_line(line))
|
|
643
|
+
}
|
|
644
|
+
call$file <- NULL
|
|
645
|
+
call$text <- paste(collect(character(), 0L), collapse = "\n")
|
|
646
|
+
eval(call, parent.frame())
|
|
647
|
+
})
|
|
648
|
+
attr(beatrina_scan, "beatrina") <- BASE_SCAN
|
|
649
|
+
attr(beatrina_readline, "beatrina") <- BASE_READLINE
|
|
650
|
+
environment(beatrina_scan) <- environment()
|
|
651
|
+
|
|
652
|
+
beatrina_patch("readline", beatrina_readline, "base")
|
|
653
|
+
beatrina_patch("scan", beatrina_scan, "base")
|
|
654
|
+
beatrina_patch("menu", beatrina_menu, "utils")
|
|
655
|
+
|
|
656
|
+
#' Never ask "Hit <Return> to see next plot".
|
|
657
|
+
#'
|
|
658
|
+
#' This worker is interactive, so a plot that turns page prompting on —
|
|
659
|
+
#' tna's plot() for cliques does `par(ask = TRUE)` by default — makes the
|
|
660
|
+
#' graphics engine read the console before each new page. That read is not
|
|
661
|
+
#' readline(), so nothing announces it: the chunk spun forever and every
|
|
662
|
+
#' chunk behind it queued (test/lab-rmd.e2e, the lab's `plot(cliques_of_two,
|
|
663
|
+
#' 4)`). Every page is captured as its own figure here, so there is no page to
|
|
664
|
+
#' wait for. The hooks run before the engine's check on every new page, base
|
|
665
|
+
#' graphics and grid alike; a package's own `par(ask = TRUE)` is left in place
|
|
666
|
+
#' and simply never gets to ask.
|
|
667
|
+
#'
|
|
668
|
+
#' @return Invisibly NULL.
|
|
669
|
+
beatrina_no_page_prompt <- function() {
|
|
670
|
+
if (isTRUE(grDevices::devAskNewPage())) grDevices::devAskNewPage(FALSE)
|
|
671
|
+
invisible(NULL)
|
|
672
|
+
}
|
|
673
|
+
local({
|
|
674
|
+
already <- function(hook) any(vapply(getHook(hook), identical, logical(1), beatrina_no_page_prompt))
|
|
675
|
+
if (!already("before.plot.new")) setHook("before.plot.new", beatrina_no_page_prompt)
|
|
676
|
+
if (!already("before.grid.newpage")) setHook("before.grid.newpage", beatrina_no_page_prompt)
|
|
677
|
+
})
|
|
678
|
+
|
|
474
679
|
#' A function's formals as one display string, defaults included.
|
|
475
680
|
#'
|
|
476
681
|
#' `formals()` is NULL for primitives like `sum`; `args()` still knows their
|
|
@@ -651,9 +856,9 @@ emit_struct <- function(id, name = NULL, path = NULL) {
|
|
|
651
856
|
child_get(o, k)
|
|
652
857
|
}
|
|
653
858
|
node <- tryCatch(Reduce(step, keys, init = get(name, envir = globalenv())),
|
|
654
|
-
error = function(e) structure(class = "
|
|
859
|
+
error = function(e) structure(class = "beatrina_fail",
|
|
655
860
|
list(msg = conditionMessage(e))))
|
|
656
|
-
if (inherits(node, "
|
|
861
|
+
if (inherits(node, "beatrina_fail")) {
|
|
657
862
|
emit(list(type = "struct", id = id, name = name, path = as.list(keys),
|
|
658
863
|
error = node$msg))
|
|
659
864
|
return(invisible(NULL))
|
|
@@ -955,6 +1160,88 @@ emit_colstats <- function(id, name, column, query = NULL, filters = NULL) {
|
|
|
955
1160
|
)
|
|
956
1161
|
}
|
|
957
1162
|
|
|
1163
|
+
#' The grid's copy door, R's half: one column, every row, as text.
|
|
1164
|
+
#'
|
|
1165
|
+
#' `Copy Column (all rows, through R)` (lib/panel.js `columnValues`,
|
|
1166
|
+
#' lib/command-system.js): the page holds one window of rows, so a whole
|
|
1167
|
+
#' column has to come from here. Same object resolution, same query and
|
|
1168
|
+
#' filters as `view` and `colstats`, so what is copied is the column of the
|
|
1169
|
+
#' rows the grid is showing — every one of them, not one page. Written the way
|
|
1170
|
+
#' `write.csv` writes it (header quoted, text quoted, numbers at full
|
|
1171
|
+
#' precision, NA bare, no row names) to a temporary file and read back: no
|
|
1172
|
+
#' sink, nothing evaluated but the name. Read-only.
|
|
1173
|
+
#'
|
|
1174
|
+
#' Bounded at COLUMN_VALUES_MAX values and refused above it with a sentence:
|
|
1175
|
+
#' a twenty-million-line clipboard is not a copy, it is an export, and the
|
|
1176
|
+
#' Export door sits one row down in the same menu.
|
|
1177
|
+
#'
|
|
1178
|
+
#' @param id Request id.
|
|
1179
|
+
#' @param name Object name or expression, as for `view`.
|
|
1180
|
+
#' @param column The column to copy.
|
|
1181
|
+
#' @param query,filters The viewer's active search and column filters.
|
|
1182
|
+
COLUMN_VALUES_MAX <- 1000000L
|
|
1183
|
+
|
|
1184
|
+
emit_column_values <- function(id, name, column, query = NULL, filters = NULL) {
|
|
1185
|
+
column <- if (is.character(column) && length(column)) column[[1L]] else ""
|
|
1186
|
+
reply <- function(...) emit(list(type = "column_values", id = id, name = name,
|
|
1187
|
+
column = column, ...))
|
|
1188
|
+
if (!is.character(name) || length(name) != 1L || is.na(name) || !nzchar(name)) {
|
|
1189
|
+
reply(error = "bad name")
|
|
1190
|
+
return(invisible(NULL))
|
|
1191
|
+
}
|
|
1192
|
+
obj <- tryCatch(eval(parse(text = name), globalenv()), error = function(e) NULL)
|
|
1193
|
+
# NULL is checked BEFORE coercion: as.data.frame(NULL) is an empty frame,
|
|
1194
|
+
# not NULL, and a missing object would otherwise read as "no such column".
|
|
1195
|
+
if (is.null(obj)) {
|
|
1196
|
+
reply(error = "not found")
|
|
1197
|
+
return(invisible(NULL))
|
|
1198
|
+
}
|
|
1199
|
+
if (!is.data.frame(obj)) obj <- tryCatch(as.data.frame(obj, stringsAsFactors = FALSE),
|
|
1200
|
+
error = function(e) NULL)
|
|
1201
|
+
if (is.null(obj) || !nzchar(column)) {
|
|
1202
|
+
reply(error = "not found")
|
|
1203
|
+
return(invisible(NULL))
|
|
1204
|
+
}
|
|
1205
|
+
total <- nrow(obj)
|
|
1206
|
+
narrowed <- view_filter(obj, query, filters)
|
|
1207
|
+
obj <- narrowed$obj
|
|
1208
|
+
hit <- match(column, names(obj))
|
|
1209
|
+
if (is.na(hit)) hit <- match(column, substr(names(obj), 1L, MAX_VIEW_LABEL_CHARS))
|
|
1210
|
+
if (is.na(hit)) {
|
|
1211
|
+
reply(error = "no such column")
|
|
1212
|
+
return(invisible(NULL))
|
|
1213
|
+
}
|
|
1214
|
+
n <- nrow(obj)
|
|
1215
|
+
if (n > COLUMN_VALUES_MAX) {
|
|
1216
|
+
reply(error = sprintf("%s has %s values; copying through R stops at %s. Export the table instead.",
|
|
1217
|
+
column, format(n, big.mark = ","),
|
|
1218
|
+
format(COLUMN_VALUES_MAX, big.mark = ",")),
|
|
1219
|
+
rows = n, totalRows = total)
|
|
1220
|
+
return(invisible(NULL))
|
|
1221
|
+
}
|
|
1222
|
+
# A temporary file, not a textConnection: the connection grows its vector a
|
|
1223
|
+
# line at a time and took 16 s for 100,000 rows where the file takes 50 ms
|
|
1224
|
+
# (measured 2026-09-17, identical text). UTF-8 both ways, so Windows' native
|
|
1225
|
+
# code page never enters the text. as.data.frame() first: a tibble or
|
|
1226
|
+
# data.table has its own `[`, and this must be base's one-column frame.
|
|
1227
|
+
tf <- tempfile("column-values-", fileext = ".csv")
|
|
1228
|
+
on.exit(unlink(tf), add = TRUE)
|
|
1229
|
+
written <- tryCatch({
|
|
1230
|
+
utils::write.csv(as.data.frame(obj)[hit], tf, row.names = FALSE, fileEncoding = "UTF-8")
|
|
1231
|
+
TRUE
|
|
1232
|
+
}, interrupt = function(i) FALSE, error = function(e) e)
|
|
1233
|
+
if (isTRUE(written)) {
|
|
1234
|
+
reply(text = paste(readLines(tf, encoding = "UTF-8", warn = FALSE), collapse = "\n"),
|
|
1235
|
+
rows = n, totalRows = total,
|
|
1236
|
+
filtered = nzchar(narrowed$query) || narrowed$count > 0L)
|
|
1237
|
+
} else if (isFALSE(written)) {
|
|
1238
|
+
reply(error = "interrupted")
|
|
1239
|
+
} else {
|
|
1240
|
+
reply(error = conditionMessage(written))
|
|
1241
|
+
}
|
|
1242
|
+
invisible(NULL)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
958
1245
|
#' Autocomplete via R's own engine — the one behind TAB in the console,
|
|
959
1246
|
#' RStudio and Jupyter. Not hand-rolled: the engine already understands `$`
|
|
960
1247
|
#' and `@` access, argument names inside a call, `::` namespaces, library()
|
|
@@ -1051,15 +1338,125 @@ emit_complete <- function(id, line = NULL, cursor = NULL, max_items = MAX_COMPLE
|
|
|
1051
1338
|
}
|
|
1052
1339
|
list(value = v, kind = "variable", detail = detail)
|
|
1053
1340
|
}
|
|
1341
|
+
comps <- c(st$comps, case_blind_extras(st$token, st$comps, line, st$start, quoted))
|
|
1342
|
+
# R answers a `df$xyz` that matches nothing with the bare stub `df$`. As a
|
|
1343
|
+
# completion that is worse than none: accepting it REPLACES the token, so it
|
|
1344
|
+
# deletes the `xyz` the user typed and leaves incomplete syntax behind. It is
|
|
1345
|
+
# dropped only when the token already carries the separator — offering `x$`
|
|
1346
|
+
# for a token of `x`, or `stats::` for `stat`, is the useful "there is more
|
|
1347
|
+
# inside this" hint RStudio gives, and those stay. `pkg::` behaves the same
|
|
1348
|
+
# way as `$` and `@` here, so all three are one rule.
|
|
1349
|
+
SEP_END <- "([$@]|:::?)$"
|
|
1350
|
+
if (grepl("([$@]|::)", st$token) && !grepl(SEP_END, st$token)) {
|
|
1351
|
+
comps <- comps[!grepl(SEP_END, comps)]
|
|
1352
|
+
}
|
|
1054
1353
|
frame <- list(type = "complete", id = id, start = st$start, end = cursor,
|
|
1055
1354
|
token = st$token,
|
|
1056
|
-
items = lapply(utils::head(
|
|
1057
|
-
truncated = length(
|
|
1355
|
+
items = lapply(utils::head(comps, max_items), describe_item),
|
|
1356
|
+
truncated = length(comps) > max_items)
|
|
1058
1357
|
frame$args <- I(completion_call_args(fn))
|
|
1059
1358
|
frame$columns <- completion_columns(data, max_items)
|
|
1060
1359
|
emit(frame)
|
|
1061
1360
|
}
|
|
1062
1361
|
|
|
1362
|
+
#' Names that match the typed token when case is ignored.
|
|
1363
|
+
#'
|
|
1364
|
+
#' R's own completion engine (`utils:::.completeToken`) matches case-SENSITIVELY
|
|
1365
|
+
#' and has no option not to: typing `rda` offers nothing at all for an object
|
|
1366
|
+
#' called `Rdata`, and `myv` offers nothing for `myVar`. The page's own
|
|
1367
|
+
#' candidates — the names it parses out of the notebook's source — have always
|
|
1368
|
+
#' matched case-blind (`lib/completion-merge.js`), so the same keystroke
|
|
1369
|
+
#' completed a name written in a chunk and did NOT complete the same name once
|
|
1370
|
+
#' it existed in the session. One editor, two rules, decided by where the name
|
|
1371
|
+
#' happened to live.
|
|
1372
|
+
#'
|
|
1373
|
+
#' Case is ignored for EVERY token, `myV` as much as `rda`. A first shape of
|
|
1374
|
+
#' this left mixed-case tokens to R's exact matching on the theory that a
|
|
1375
|
+
#' capital typed mid-word was deliberate — but the page's parsed candidates
|
|
1376
|
+
#' never made that distinction, so it would have rebuilt the same split this
|
|
1377
|
+
#' function exists to close, one spelling further along. Completion is case
|
|
1378
|
+
#' insensitive; there is no second rule.
|
|
1379
|
+
#'
|
|
1380
|
+
#' These are EXTRAS, appended: an exact-case match is still found the way it
|
|
1381
|
+
#' always was, and `lib/completion-merge.js` already sorts a prefix match that
|
|
1382
|
+
#' differs in case below one that does not, so nothing moves in a list that was
|
|
1383
|
+
#' already right.
|
|
1384
|
+
#'
|
|
1385
|
+
#' Refused where a dotted or quoted context means the names are not the search
|
|
1386
|
+
#' path's: `df$`, `obj@`, `pkg::` and a path inside a string are R's to answer,
|
|
1387
|
+
#' and sweeping globals into them would offer names that cannot be used there.
|
|
1388
|
+
#'
|
|
1389
|
+
#' Measured at 2.4 ms over a 2,482-name search path — one sweep per keystroke
|
|
1390
|
+
#' that reaches the kernel, against a round trip that costs more than that.
|
|
1391
|
+
#'
|
|
1392
|
+
#' Every context R answers is covered, because "autocomplete is case
|
|
1393
|
+
#' insensitive" with an exception is two rules again: a bare name against the
|
|
1394
|
+
#' search path, `df$col`, `obj@slot`, and `pkg::name`. Each gets its OWN
|
|
1395
|
+
#' candidate set — the object's names, the object's slots, the namespace's
|
|
1396
|
+
#' exports — so nothing is offered where it could not be used.
|
|
1397
|
+
#'
|
|
1398
|
+
#' NOTHING IS EVALUATED to answer a keystroke. The head of a `$`/`@`/`::` is
|
|
1399
|
+
#' accepted only as a plain name and looked up with `get0`, and a namespace is
|
|
1400
|
+
#' read only when it is ALREADY loaded — the same rule
|
|
1401
|
+
#' `completion_call_args()` follows, for the same reason.
|
|
1402
|
+
#'
|
|
1403
|
+
#' @param token The token R guessed, as typed. R reports the WHOLE token for a
|
|
1404
|
+
#' dotted context (`x$al`, not `al`), which is what makes splitting it here
|
|
1405
|
+
#' the right place rather than re-deriving the head from the line.
|
|
1406
|
+
#' @param comps What R already matched, so nothing is offered twice.
|
|
1407
|
+
#' @param line The whole line, for the character before the token.
|
|
1408
|
+
#' @param start R's own start offset for the token (1-based, before the token).
|
|
1409
|
+
#' @param quoted Whether the token sits inside a string.
|
|
1410
|
+
#' @return Character vector of extra completions, spelled as R spells them
|
|
1411
|
+
#' (prefix included for a dotted context); empty when the context is one this
|
|
1412
|
+
#' cannot answer.
|
|
1413
|
+
case_blind_extras <- function(token, comps, line, start, quoted) {
|
|
1414
|
+
if (isTRUE(quoted)) return(character(0))
|
|
1415
|
+
if (!is.character(token) || length(token) != 1L || is.na(token)) return(character(0))
|
|
1416
|
+
# A prefix is safe to splice back only because `is_completion_name` vets the
|
|
1417
|
+
# head; the one regex metacharacter either half can carry is then the dot.
|
|
1418
|
+
starts_with <- function(pool, tail) {
|
|
1419
|
+
if (!length(pool)) return(character(0))
|
|
1420
|
+
pattern <- paste0("^", gsub(".", "\\.", tail, fixed = TRUE))
|
|
1421
|
+
pool[grepl(pattern, pool, ignore.case = TRUE)]
|
|
1422
|
+
}
|
|
1423
|
+
# `x$al` / `obj@sl` / `pkg::na` — R hands the whole thing over as one token.
|
|
1424
|
+
dotted <- regmatches(token, regexec("^(.*?)(\\$|@|:::?)([^$@:]*)$", token))[[1L]]
|
|
1425
|
+
if (length(dotted) == 4L) {
|
|
1426
|
+
head <- dotted[[2L]]; sep <- dotted[[3L]]; tail <- dotted[[4L]]
|
|
1427
|
+
if (!is_completion_name(head)) return(character(0))
|
|
1428
|
+
pool <- if (sep %in% c("::", ":::")) {
|
|
1429
|
+
if (head %in% loadedNamespaces())
|
|
1430
|
+
tryCatch(ls(asNamespace(head)), error = function(...) character(0))
|
|
1431
|
+
else character(0)
|
|
1432
|
+
} else {
|
|
1433
|
+
obj <- tryCatch(get0(head, envir = globalenv()), error = function(...) NULL)
|
|
1434
|
+
if (is.null(obj)) character(0)
|
|
1435
|
+
else if (identical(sep, "@")) tryCatch(methods::slotNames(obj), error = function(...) character(0))
|
|
1436
|
+
else if (is.environment(obj)) tryCatch(ls(obj), error = function(...) character(0))
|
|
1437
|
+
else tryCatch(names(obj), error = function(...) character(0))
|
|
1438
|
+
}
|
|
1439
|
+
matched <- starts_with(pool, tail)
|
|
1440
|
+
# `paste0("x", "$", character(0))` is "x$", not nothing — paste recycles a
|
|
1441
|
+
# zero-length argument as "". Without this the empty answer for an object
|
|
1442
|
+
# that does not exist came back as a completion to the bare prefix.
|
|
1443
|
+
if (!length(matched)) return(character(0))
|
|
1444
|
+
hits <- paste0(head, sep, matched)
|
|
1445
|
+
return(setdiff(hits, c(comps, sub("\\($", "", comps))))
|
|
1446
|
+
}
|
|
1447
|
+
if (!is_completion_name(token)) return(character(0))
|
|
1448
|
+
# A bare tail sitting right after a separator is a shape this cannot place —
|
|
1449
|
+
# the head is not in the token, so refuse rather than sweep in globals that
|
|
1450
|
+
# could not be used there.
|
|
1451
|
+
before <- if (start > 1L) substr(line, start - 1L, start - 1L) else ""
|
|
1452
|
+
if (before %in% c("$", "@", ":")) return(character(0))
|
|
1453
|
+
names <- unique(unlist(lapply(search(), function(e)
|
|
1454
|
+
tryCatch(ls(e), error = function(...) character(0)))))
|
|
1455
|
+
# Drop what R already offered, in either of the two spellings it ships
|
|
1456
|
+
# (`name` and `name(` for a function).
|
|
1457
|
+
setdiff(starts_with(names, token), c(comps, sub("\\($", "", comps)))
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1063
1460
|
#' A bare R name, or `pkg::name` — the only shape a completion context may
|
|
1064
1461
|
#' name. Anything else (a call, a subset, an expression) is refused, because
|
|
1065
1462
|
#' these names are looked up, and a lookup must never become an evaluation.
|
|
@@ -1110,6 +1507,57 @@ completion_columns <- function(data, max_items = MAX_COMPLETIONS) {
|
|
|
1110
1507
|
lapply(cols, function(nm) list(value = nm, type = class(df[[nm]])[1L]))
|
|
1111
1508
|
}
|
|
1112
1509
|
|
|
1510
|
+
#' A progress callback for keep_save()/keep_resume() that emits id-less
|
|
1511
|
+
#' `keep_progress` frames — the supervisor broadcasts an id-less frame to every
|
|
1512
|
+
#' page, which is what lets the page that asked AND a page that follows a
|
|
1513
|
+
#' handoff both draw it. Throttled to four a second; the last one always goes.
|
|
1514
|
+
#' @param phase "save" or "restore".
|
|
1515
|
+
#' @return function(done, total, bytes, name).
|
|
1516
|
+
keep_progress_emitter <- function(phase) {
|
|
1517
|
+
last <- 0
|
|
1518
|
+
function(done, total, bytes = 0, name = "") {
|
|
1519
|
+
now <- as.numeric(Sys.time())
|
|
1520
|
+
if (done < total && now - last < 0.25) return(invisible(NULL))
|
|
1521
|
+
last <<- now
|
|
1522
|
+
emit(list(type = "keep_progress", phase = phase, done = done, total = total,
|
|
1523
|
+
bytes = as.numeric(bytes %||% 0), name = as.character(name)[1L]))
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
#' "Restart, keep variables", step one: save the global environment
|
|
1528
|
+
#' (spike/workspace-keep.R). No deadline — Stop (the supervisor's
|
|
1529
|
+
#' suspend_cancel) interrupts it, and the partial copy is removed.
|
|
1530
|
+
#' @param id Request id.
|
|
1531
|
+
#' @return Invisibly NULL. Emits one `suspend` frame: token, saved, bytes,
|
|
1532
|
+
#' skipped — or cancelled, or error.
|
|
1533
|
+
emit_suspend <- function(id) {
|
|
1534
|
+
out <- tryCatch(keep_save(globalenv(), progress = keep_progress_emitter("save")),
|
|
1535
|
+
error = function(e) list(error = conditionMessage(e)),
|
|
1536
|
+
interrupt = function(i) list(cancelled = TRUE))
|
|
1537
|
+
emit(c(list(type = "suspend", id = id), out))
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
#' "Restart, keep variables", step two, in the NEW worker: load the kept
|
|
1541
|
+
#' objects back and restore the working directory. Asked by the supervisor
|
|
1542
|
+
#' only, never by a page (it is not in FORWARDED), with a token the save minted.
|
|
1543
|
+
#' @param id Request id.
|
|
1544
|
+
#' @param token The kept workspace's token.
|
|
1545
|
+
#' @return Invisibly NULL. Emits one `resume` frame: restored, failed, skipped,
|
|
1546
|
+
#' cwd — or error.
|
|
1547
|
+
emit_resume <- function(id, token) {
|
|
1548
|
+
out <- tryCatch(keep_resume(token, globalenv(), progress = keep_progress_emitter("restore")),
|
|
1549
|
+
error = function(e) list(error = conditionMessage(e)),
|
|
1550
|
+
interrupt = function(i) list(error = "The restore was interrupted."))
|
|
1551
|
+
# The directory R was in comes back too — unless it is gone (the old R's
|
|
1552
|
+
# tempdir() dies with the old R), which the report then says in words.
|
|
1553
|
+
if (is.character(out$wd) && nzchar(out$wd)) {
|
|
1554
|
+
moved <- dir.exists(out$wd) && !inherits(try(setwd(out$wd), silent = TRUE), "try-error")
|
|
1555
|
+
if (!moved) out$wd_missing <- out$wd
|
|
1556
|
+
}
|
|
1557
|
+
out$wd <- NULL
|
|
1558
|
+
emit(c(list(type = "resume", id = id, cwd = getwd()), out))
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1113
1561
|
#' Coerce a wire-supplied count, falling back rather than erroring: a malformed
|
|
1114
1562
|
#' offset from a client must degrade to the default, not kill the pane.
|
|
1115
1563
|
#'
|
|
@@ -1397,14 +1845,14 @@ emit_sniff <- function(id, path = NULL, opts = NULL) {
|
|
|
1397
1845
|
if (!file.exists(p) || dir.exists(p)) return(fail("not a readable file"))
|
|
1398
1846
|
}
|
|
1399
1847
|
if (!exists("sniff_file", inherits = TRUE)) {
|
|
1400
|
-
return(fail("this kernel is too old for the import wizard - restart
|
|
1848
|
+
return(fail("this kernel is too old for the import wizard - restart Beatrina"))
|
|
1401
1849
|
}
|
|
1402
1850
|
res <- tryCatch(
|
|
1403
1851
|
sniff_file(p, if (is.list(opts)) opts else list()),
|
|
1404
|
-
error = function(e) structure(class = "
|
|
1405
|
-
interrupt = function(i) structure(class = "
|
|
1852
|
+
error = function(e) structure(class = "beatrina_fail", list(msg = conditionMessage(e))),
|
|
1853
|
+
interrupt = function(i) structure(class = "beatrina_fail", list(msg = "interrupted"))
|
|
1406
1854
|
)
|
|
1407
|
-
if (inherits(res, "
|
|
1855
|
+
if (inherits(res, "beatrina_fail")) return(fail(res$msg))
|
|
1408
1856
|
emit(c(list(type = "sniff", id = id), res))
|
|
1409
1857
|
}
|
|
1410
1858
|
|
|
@@ -1443,10 +1891,10 @@ emit_import <- function(id, path = NULL, name = NULL) {
|
|
|
1443
1891
|
}
|
|
1444
1892
|
obj <- tryCatch(
|
|
1445
1893
|
if (requireNamespace("rio", quietly = TRUE)) rio::import(p) else read_by_ext(p),
|
|
1446
|
-
error = function(e) structure(class = "
|
|
1447
|
-
interrupt = function(i) structure(class = "
|
|
1894
|
+
error = function(e) structure(class = "beatrina_fail", list(msg = conditionMessage(e))),
|
|
1895
|
+
interrupt = function(i) structure(class = "beatrina_fail", list(msg = "interrupted"))
|
|
1448
1896
|
)
|
|
1449
|
-
if (inherits(obj, "
|
|
1897
|
+
if (inherits(obj, "beatrina_fail")) return(fail(obj$msg))
|
|
1450
1898
|
nm <- if (is.character(name) && length(name) == 1L && nzchar(name)) name
|
|
1451
1899
|
else make.names(tools::file_path_sans_ext(basename(p)))
|
|
1452
1900
|
assign(nm, obj, envir = globalenv())
|
|
@@ -1709,14 +2157,14 @@ emit_package_action <- function(id, action, name, lib = NULL) {
|
|
|
1709
2157
|
|
|
1710
2158
|
#' Project and renv state, without paths or lockfile contents on the wire.
|
|
1711
2159
|
emit_project_status <- function(id) {
|
|
1712
|
-
result <- tryCatch(
|
|
2160
|
+
result <- tryCatch(beatrina_project_status(), error = function(e)
|
|
1713
2161
|
list(error = conditionMessage(e)))
|
|
1714
2162
|
emit(c(list(type = "project_status", id = id), result))
|
|
1715
2163
|
}
|
|
1716
2164
|
|
|
1717
2165
|
#' One explicit, visible environment action: install renv or restore its lock.
|
|
1718
2166
|
emit_project_action <- function(id, action) {
|
|
1719
|
-
result <- tryCatch(
|
|
2167
|
+
result <- tryCatch(beatrina_project_action(action), error = function(e)
|
|
1720
2168
|
list(ok = FALSE, error = conditionMessage(e)))
|
|
1721
2169
|
emit(c(list(type = "project_action", id = id, action = action), result))
|
|
1722
2170
|
}
|
|
@@ -1777,7 +2225,7 @@ emit_help <- function(id, topic) {
|
|
|
1777
2225
|
} else {
|
|
1778
2226
|
txt <- paste(utils::capture.output(base::print(paths)), collapse = "\n")
|
|
1779
2227
|
if (!nzchar(trimws(txt))) NULL
|
|
1780
|
-
else paste0("<pre class=\"
|
|
2228
|
+
else paste0("<pre class=\"beatrina-help-text\">",
|
|
1781
2229
|
gsub("<", "<", gsub("&", "&", txt), fixed = TRUE), "</pre>")
|
|
1782
2230
|
}
|
|
1783
2231
|
}, error = function(e) NULL)
|
|
@@ -1927,7 +2375,7 @@ emit_wd <- function(id, path = NULL) {
|
|
|
1927
2375
|
|
|
1928
2376
|
# The file system lives in fileio.R (shared with the supervisor). Each op is
|
|
1929
2377
|
# `emit(fs_x(...))`: fileio.R returns the frame, this process sends it. The
|
|
1930
|
-
# worker still answers them when the supervisor forwards (
|
|
2378
|
+
# worker still answers them when the supervisor forwards (BEATRINA_FILE_OPS=worker,
|
|
1931
2379
|
# the transition flag), so nothing here may diverge from fileio.R.
|
|
1932
2380
|
emit_files <- function(id, path = NULL, all = FALSE) emit(fs_files(id, path, all, wd = getwd()))
|
|
1933
2381
|
emit_mkdir <- function(id, path = NULL) emit(fs_mkdir(id, path, wd = getwd()))
|
|
@@ -1940,7 +2388,7 @@ emit_writefile <- function(id, path = NULL, text = "", expected = NULL, encoding
|
|
|
1940
2388
|
emit(fs_writefile(id, path, text, expected, encoding, base64, wd = getwd()))
|
|
1941
2389
|
emit_writefiles_atomic <- function(id, files = NULL) emit(fs_writefiles_atomic(id, files, wd = getwd()))
|
|
1942
2390
|
|
|
1943
|
-
#' Check whether
|
|
2391
|
+
#' Check whether Beatrina's current R session is usable without collecting user
|
|
1944
2392
|
#' material.
|
|
1945
2393
|
#'
|
|
1946
2394
|
#' This reply is deliberately a FACT ALLOW-LIST. It never contains getwd(),
|
|
@@ -1954,12 +2402,12 @@ emit_writefiles_atomic <- function(id, files = NULL) emit(fs_writefiles_atomic(i
|
|
|
1954
2402
|
emit_doctor <- function(id) {
|
|
1955
2403
|
can_create_in <- function(dir) {
|
|
1956
2404
|
if (!is.character(dir) || length(dir) != 1L || !dir.exists(dir)) return(FALSE)
|
|
1957
|
-
probe <- tempfile(".
|
|
2405
|
+
probe <- tempfile(".beatrina-doctor-", tmpdir = dir)
|
|
1958
2406
|
on.exit(unlink(probe, force = TRUE), add = TRUE)
|
|
1959
2407
|
isTRUE(tryCatch(file.create(probe, showWarnings = FALSE), error = function(e) FALSE))
|
|
1960
2408
|
}
|
|
1961
2409
|
|
|
1962
|
-
q_configured <- trimws(Sys.getenv("
|
|
2410
|
+
q_configured <- trimws(Sys.getenv("BEATRINA_QUARTO", ""))
|
|
1963
2411
|
q_path <- if (nzchar(q_configured)) q_configured else unname(Sys.which("quarto"))
|
|
1964
2412
|
q_available <- is.character(q_path) && length(q_path) == 1L && nzchar(q_path) && file.exists(q_path)
|
|
1965
2413
|
q_version <- ""
|
|
@@ -2005,16 +2453,26 @@ emit_doctor <- function(id) {
|
|
|
2005
2453
|
version = q_version
|
|
2006
2454
|
),
|
|
2007
2455
|
required_packages = required,
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2456
|
+
# ASKED, not re-derived. Doctor's own copy of the posture rule had drifted:
|
|
2457
|
+
# `!identical(Sys.getenv("BEATRINA_REQUIRE_ORIGIN", "1"), "0")` is TRUE
|
|
2458
|
+
# whenever the variable is unset, while the real rule (deployment.R, and
|
|
2459
|
+
# host/deployment.mjs, which agree) is `== "1" || !loopback` — FALSE on a
|
|
2460
|
+
# default desktop session. So Doctor reported an origin check the host was
|
|
2461
|
+
# not enforcing, and lib/doctor.js computes its managed-remote verdict from
|
|
2462
|
+
# exactly these fields. The loopback list had drifted too, missing "[::1]".
|
|
2463
|
+
deployment = local({
|
|
2464
|
+
d <- beatrina_deployment(as.integer(Sys.getenv("BEATRINA_PORT", "0")))
|
|
2465
|
+
list(
|
|
2466
|
+
loopback = isTRUE(d$loopback),
|
|
2467
|
+
confinement = nzchar(Sys.getenv("BEATRINA_ROOT", "")),
|
|
2468
|
+
audit = nzchar(Sys.getenv("BEATRINA_LOG", "")),
|
|
2469
|
+
ai_local_only = identical(Sys.getenv("BEATRINA_AI_LOCAL_ONLY", ""), "1"),
|
|
2470
|
+
ai_policy = nzchar(Sys.getenv("BEATRINA_AI_POLICY", "")) || nzchar(Sys.getenv("BEATRINA_AI_PROVIDERS", "")),
|
|
2471
|
+
origin_required = isTRUE(d$require_origin),
|
|
2472
|
+
trusted_proxy = isTRUE(d$trust_proxy),
|
|
2473
|
+
unauthenticated = isTRUE(d$allow_unauthenticated)
|
|
2474
|
+
)
|
|
2475
|
+
}),
|
|
2018
2476
|
proxy = list(
|
|
2019
2477
|
http = nzchar(Sys.getenv("HTTP_PROXY", "")) || nzchar(Sys.getenv("http_proxy", "")),
|
|
2020
2478
|
https = nzchar(Sys.getenv("HTTPS_PROXY", "")) || nzchar(Sys.getenv("https_proxy", "")),
|
|
@@ -2033,9 +2491,9 @@ emit_doctor <- function(id) {
|
|
|
2033
2491
|
#' @param seq Index of the top-level expression, to keep filenames unique.
|
|
2034
2492
|
#' @return Invisibly NULL.
|
|
2035
2493
|
open_plot_device <- function(dir, seq, dims = NULL) {
|
|
2036
|
-
width <-
|
|
2037
|
-
height <-
|
|
2038
|
-
res <-
|
|
2494
|
+
width <- dim_or(dims$width, "width")
|
|
2495
|
+
height <- dim_or(dims$height, "height")
|
|
2496
|
+
res <- dim_or(dims$res, "res")
|
|
2039
2497
|
if (plot_is_svg(dims)) {
|
|
2040
2498
|
# A vector device is sized in INCHES and has no resolution at all — which
|
|
2041
2499
|
# is the whole point: its cost does not grow with dpi. The client sends
|
|
@@ -2088,9 +2546,9 @@ emit_plot_frame <- function(id, f, dims = NULL) {
|
|
|
2088
2546
|
# older client would have rendered nothing at all.
|
|
2089
2547
|
svg <- grepl("\\.svg$", f)
|
|
2090
2548
|
emit(list(type = "plot", id = id, mime = if (svg) "image/svg+xml" else "image/png",
|
|
2091
|
-
width =
|
|
2092
|
-
height =
|
|
2093
|
-
res =
|
|
2549
|
+
width = dim_or(dims$width, "width"),
|
|
2550
|
+
height = dim_or(dims$height, "height"),
|
|
2551
|
+
res = dim_or(dims$res, "res"),
|
|
2094
2552
|
data = jsonlite::base64_enc(readBin(f, "raw", file.info(f)$size))))
|
|
2095
2553
|
invisible(NULL)
|
|
2096
2554
|
}
|
|
@@ -2121,9 +2579,9 @@ emit_plot_frame <- function(id, f, dims = NULL) {
|
|
|
2121
2579
|
.blank_png_cache <- new.env(parent = emptyenv())
|
|
2122
2580
|
is_blank_plot <- function(f, dims) {
|
|
2123
2581
|
if (!identical(RASTER_DEVICE, "ragg")) return(FALSE)
|
|
2124
|
-
w <-
|
|
2125
|
-
h <-
|
|
2126
|
-
r <-
|
|
2582
|
+
w <- dim_or(dims$width, "width")
|
|
2583
|
+
h <- dim_or(dims$height, "height")
|
|
2584
|
+
r <- dim_or(dims$res, "res")
|
|
2127
2585
|
key <- paste(w, h, r, sep = "x")
|
|
2128
2586
|
blank <- .blank_png_cache[[key]]
|
|
2129
2587
|
if (is.null(blank)) {
|
|
@@ -2315,6 +2773,16 @@ emit_dataframe <- function(id, df, source = NULL) {
|
|
|
2315
2773
|
# jsonlite drops a NULL, so a frame with no name is byte-identical to what
|
|
2316
2774
|
# this emitted before the field existed.
|
|
2317
2775
|
source = source,
|
|
2776
|
+
# How many significant digits this document asked its numbers to be SHOWN
|
|
2777
|
+
# with. The values on the wire stay full precision — the page exports and
|
|
2778
|
+
# copies from them, and rounding here would write 0 for 1.2e-5 the way
|
|
2779
|
+
# jsonlite's own digits = 4 once did. So `options(digits = 3)` becomes a
|
|
2780
|
+
# display instruction the table renderer honours, instead of a call that
|
|
2781
|
+
# looks like configuration and changes nothing: auto-printing a data.frame
|
|
2782
|
+
# never passes through print.data.frame, which is what would have applied
|
|
2783
|
+
# it. NULL (dropped by jsonlite) whenever the document left R's default of
|
|
2784
|
+
# 7 alone, so an untouched notebook's frames are unchanged.
|
|
2785
|
+
digits = display_digits(),
|
|
2318
2786
|
rows = head_df
|
|
2319
2787
|
))
|
|
2320
2788
|
invisible(NULL)
|
|
@@ -2383,7 +2851,7 @@ widget_standalone_html <- function(w, fit = FALSE) {
|
|
|
2383
2851
|
"<style>html,body{margin:0;padding:0;height:100%;}</style>"
|
|
2384
2852
|
}
|
|
2385
2853
|
fit_js <- if (fit) paste0(
|
|
2386
|
-
"<script>(function(){var r=function(){try{parent.postMessage({
|
|
2854
|
+
"<script>(function(){var r=function(){try{parent.postMessage({beatrinaHtmlHeight:",
|
|
2387
2855
|
"Math.ceil(document.documentElement.getBoundingClientRect().height)},'*')}catch(e){}};",
|
|
2388
2856
|
"window.addEventListener('load',r);if(window.ResizeObserver){new ResizeObserver(r)",
|
|
2389
2857
|
".observe(document.documentElement)}setTimeout(r,0);setTimeout(r,250)})()</script>") else ""
|
|
@@ -2411,7 +2879,7 @@ widget_standalone_html <- function(w, fit = FALSE) {
|
|
|
2411
2879
|
#' when `v` is not HTML.
|
|
2412
2880
|
rich_html_of <- function(v) {
|
|
2413
2881
|
if (inherits(v, "htmlwidget")) return(list(x = v, kind = "widget"))
|
|
2414
|
-
if (inherits(v, "
|
|
2882
|
+
if (inherits(v, "beatrina_html")) return(list(x = htmltools::HTML(unclass(v)), kind = "html"))
|
|
2415
2883
|
if (inherits(v, c("shiny.tag", "shiny.tag.list", "html"))) return(list(x = v, kind = "html"))
|
|
2416
2884
|
if (inherits(v, "knit_asis")) return(list(x = htmltools::HTML(paste(as.character(v), collapse = "\n")), kind = "html"))
|
|
2417
2885
|
if (inherits(v, "knitr_kable") && identical(attr(v, "format"), "html")) {
|
|
@@ -2593,7 +3061,7 @@ apply_fn_breaks <- function() {
|
|
|
2593
3061
|
suppressMessages(trace(
|
|
2594
3062
|
h$name,
|
|
2595
3063
|
tracer = bquote({
|
|
2596
|
-
.
|
|
3064
|
+
.beatrina_debug_entered(.(file), .(line), "breakpoint")
|
|
2597
3065
|
browser()
|
|
2598
3066
|
}),
|
|
2599
3067
|
at = h$at, where = h$env, print = FALSE))
|
|
@@ -2657,7 +3125,7 @@ debug_stack <- function(calls, frames, max_frames = 40L) {
|
|
|
2657
3125
|
# frame's environment holds the TRACER EXPRESSION as an unforced promise —
|
|
2658
3126
|
# mget() forces what it reads, so describing that frame re-entered the
|
|
2659
3127
|
# tracer recursively (a second paused frame, a browser "Called from: mget").
|
|
2660
|
-
ours <- "^(\\.
|
|
3128
|
+
ours <- "^(\\.beatrina_debug_entered|\\.beatrina_debug_where|\\.doTrace|browser\\(|eval\\(expr|eval\\.parent|Reduce\\(|\\{)"
|
|
2661
3129
|
while (to >= from && grepl(ours, texts[[to]])) to <- to - 1L
|
|
2662
3130
|
if (to < from) return(list())
|
|
2663
3131
|
idx <- seq.int(from, to)
|
|
@@ -2684,7 +3152,7 @@ debug_stack <- function(calls, frames, max_frames = 40L) {
|
|
|
2684
3152
|
#'
|
|
2685
3153
|
#' THE HARNESS IS TRIMMED. The bottom frames are this file's own
|
|
2686
3154
|
#' (`run_cell`, `withCallingHandlers`, the `lapply` over expressions) and
|
|
2687
|
-
#' showing them teaches the reader that their error came from
|
|
3155
|
+
#' showing them teaches the reader that their error came from Beatrina. Frames
|
|
2688
3156
|
#' are dropped up to and including the last `eval(e, globalenv())`, which is
|
|
2689
3157
|
#' the boundary between our code and theirs.
|
|
2690
3158
|
#'
|
|
@@ -2748,7 +3216,7 @@ capture_trace <- function(max_frames = 40L) {
|
|
|
2748
3216
|
#' The narrow message fallback covers older R releases. In both cases the
|
|
2749
3217
|
#' allow-list is deliberately the same one used by `package_action`, so an
|
|
2750
3218
|
#' error message can never become code or an arbitrary install target.
|
|
2751
|
-
|
|
3219
|
+
beatrina_missing_package <- function(error) {
|
|
2752
3220
|
valid <- function(value) {
|
|
2753
3221
|
is.character(value) && length(value) == 1L && !is.na(value) &&
|
|
2754
3222
|
grepl("^[A-Za-z][A-Za-z0-9.]*$", value)
|
|
@@ -2776,12 +3244,12 @@ run_cell <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
2776
3244
|
# one established outside the cell's own tryCatch, so the cell's exiting
|
|
2777
3245
|
# handlers, being inner, are still the ones that take a real Stop.
|
|
2778
3246
|
withCallingHandlers(run_cell_impl(id, source, dims, srcname),
|
|
2779
|
-
interrupt = function(i) if (
|
|
3247
|
+
interrupt = function(i) if (beatrina_interrupt_is_stray(id)) beatrina_resume_if_stray(i))
|
|
2780
3248
|
}
|
|
2781
3249
|
run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
2782
3250
|
stopifnot(is.character(source), length(source) == 1L)
|
|
2783
3251
|
# First: a signal that arrived between commands must not end THIS cell.
|
|
2784
|
-
|
|
3252
|
+
beatrina_drain_stray_interrupt(id)
|
|
2785
3253
|
# The srcname is the chunk's identity in the debugger: functions defined
|
|
2786
3254
|
# here carry it in their srcrefs, which is what lets findLineNum resolve
|
|
2787
3255
|
# "chunk:<id>#line" to a function and a step. Wire-supplied; anything not a
|
|
@@ -2818,7 +3286,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
2818
3286
|
status <- "ok"
|
|
2819
3287
|
detail <- NULL
|
|
2820
3288
|
missing_package <- NULL
|
|
2821
|
-
plot_dir <- tempfile("
|
|
3289
|
+
plot_dir <- tempfile("beatrina-plots-")
|
|
2822
3290
|
dir.create(plot_dir)
|
|
2823
3291
|
seen <- character(0)
|
|
2824
3292
|
# Asked for vector, cannot give it: SAY SO. Falling back to a raster the
|
|
@@ -2829,7 +3297,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
2829
3297
|
emit(list(type = "stream", id = id, kind = "warning",
|
|
2830
3298
|
text = paste("Vector output is not available in this R —",
|
|
2831
3299
|
"install the svglite package (or XQuartz, for Cairo)",
|
|
2832
|
-
"and restart. Drawing at", dims$res
|
|
3300
|
+
"and restart. Drawing at", dim_or(dims$res, "res"),
|
|
2833
3301
|
"dpi instead.\n")))
|
|
2834
3302
|
}
|
|
2835
3303
|
|
|
@@ -2876,9 +3344,9 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
2876
3344
|
} else {
|
|
2877
3345
|
NA_integer_
|
|
2878
3346
|
}
|
|
2879
|
-
# Stop, read at the boundary (
|
|
3347
|
+
# Stop, read at the boundary (beatrina_stop_requested): the line after a
|
|
2880
3348
|
# busy call that no signal could interrupt does not run.
|
|
2881
|
-
|
|
3349
|
+
beatrina_stop_if_requested(id)
|
|
2882
3350
|
# Where R is, for the page's progress bar (RStudio's green line
|
|
2883
3351
|
# down the chunk): one frame per top-level expression, BEFORE it
|
|
2884
3352
|
# runs, naming the lines it spans — so a 40-second model fit shows
|
|
@@ -2896,7 +3364,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
2896
3364
|
res <- if (!is.na(at_line) && at_line %in% bp_lines) {
|
|
2897
3365
|
wrapped <- as.call(list(
|
|
2898
3366
|
as.name("{"),
|
|
2899
|
-
bquote(.
|
|
3367
|
+
bquote(.beatrina_debug_entered(.(srcname), .(at_line), "breakpoint")),
|
|
2900
3368
|
quote(browser()),
|
|
2901
3369
|
e))
|
|
2902
3370
|
withVisible(eval(wrapped, globalenv()))
|
|
@@ -2934,11 +3402,11 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
2934
3402
|
}
|
|
2935
3403
|
}))
|
|
2936
3404
|
# …and after the last expression: a Stop that arrived while it ran ends the cell interrupted.
|
|
2937
|
-
|
|
3405
|
+
beatrina_stop_if_requested(id)
|
|
2938
3406
|
},
|
|
2939
3407
|
# The stray-signal rule (see run_cell), established INSIDE the cell's own
|
|
2940
3408
|
# tryCatch so it is asked before the exiting handler below can unwind.
|
|
2941
|
-
interrupt = function(i) if (
|
|
3409
|
+
interrupt = function(i) if (beatrina_interrupt_is_stray(id)) beatrina_resume_if_stray(i),
|
|
2942
3410
|
warning = function(w) {
|
|
2943
3411
|
emit(list(type = "stream", id = id, kind = "warning",
|
|
2944
3412
|
text = conditionMessage(w)))
|
|
@@ -2961,7 +3429,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
2961
3429
|
# dispatch loop and zombie the worker; invoking this restart instead
|
|
2962
3430
|
# unwinds the browser and the rest of the cell, lands here, and the cell
|
|
2963
3431
|
# reports itself stopped like any interrupt.
|
|
2964
|
-
|
|
3432
|
+
beatrina_abort_cell = function() {
|
|
2965
3433
|
status <<- "interrupted"
|
|
2966
3434
|
detail <<- "Stopped from the debugger"
|
|
2967
3435
|
}
|
|
@@ -2969,7 +3437,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
2969
3437
|
error = function(e) {
|
|
2970
3438
|
status <<- "error"
|
|
2971
3439
|
detail <<- conditionMessage(e)
|
|
2972
|
-
missing_package <<-
|
|
3440
|
+
missing_package <<- beatrina_missing_package(e)
|
|
2973
3441
|
# A parse error has no stack and no expression index; its position comes
|
|
2974
3442
|
# from the message instead, exactly as the analyzer reads it.
|
|
2975
3443
|
emit(list(type = "traceback", id = id,
|
|
@@ -3008,13 +3476,13 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
|
|
|
3008
3476
|
#' on to the next line unless a second signal happens to land in the instant
|
|
3009
3477
|
#' between system() returning and that line starting — on Linux it never did
|
|
3010
3478
|
#' (`beatrina check`, 2026-09-16). So the supervisor also writes a flag file,
|
|
3011
|
-
#' `stop-<id>` in
|
|
3479
|
+
#' `stop-<id>` in BEATRINA_STOP_DIR, and the cell reads it at every top-level
|
|
3012
3480
|
#' expression boundary. The flag is removed when read; no directory, no flag.
|
|
3013
3481
|
#'
|
|
3014
3482
|
#' @param id The wire id of the running cell.
|
|
3015
3483
|
#' @return TRUE when a stop was requested (and the flag consumed), else FALSE.
|
|
3016
|
-
|
|
3017
|
-
dir <- Sys.getenv("
|
|
3484
|
+
beatrina_stop_requested <- function(id) {
|
|
3485
|
+
dir <- Sys.getenv("BEATRINA_STOP_DIR", "")
|
|
3018
3486
|
if (!nzchar(dir) || !is.character(id) || length(id) != 1L || is.na(id)) return(FALSE)
|
|
3019
3487
|
flag <- file.path(dir, paste0("stop-", id))
|
|
3020
3488
|
if (!file.exists(flag)) return(FALSE)
|
|
@@ -3026,8 +3494,8 @@ carmar_stop_requested <- function(id) {
|
|
|
3026
3494
|
#' through the same `interrupt` handler a signal reaches.
|
|
3027
3495
|
#' @param id The wire id of the running cell.
|
|
3028
3496
|
#' @return NULL, invisibly, when no stop was requested.
|
|
3029
|
-
|
|
3030
|
-
if (
|
|
3497
|
+
beatrina_stop_if_requested <- function(id) {
|
|
3498
|
+
if (beatrina_stop_requested(id)) {
|
|
3031
3499
|
signalCondition(structure(class = c("interrupt", "condition"),
|
|
3032
3500
|
list(message = "Execution interrupted", call = NULL)))
|
|
3033
3501
|
}
|
|
@@ -3037,23 +3505,23 @@ carmar_stop_if_requested <- function(id) {
|
|
|
3037
3505
|
#' Is a stop flag for run `id` on disk? Read without consuming it.
|
|
3038
3506
|
#' @param id The wire id of the running cell.
|
|
3039
3507
|
#' @return TRUE when the supervisor has asked this run to stop.
|
|
3040
|
-
|
|
3041
|
-
dir <- Sys.getenv("
|
|
3508
|
+
beatrina_stop_pending <- function(id) {
|
|
3509
|
+
dir <- Sys.getenv("BEATRINA_STOP_DIR", "")
|
|
3042
3510
|
if (!nzchar(dir) || !is.character(id) || length(id) != 1L || is.na(id)) return(FALSE)
|
|
3043
3511
|
file.exists(file.path(dir, paste0("stop-", id)))
|
|
3044
3512
|
}
|
|
3045
3513
|
|
|
3046
3514
|
#' Is an interrupt raised inside run `id` NOT this run's Stop?
|
|
3047
3515
|
#'
|
|
3048
|
-
#' Under a supervisor that writes stop flags (
|
|
3516
|
+
#' Under a supervisor that writes stop flags (BEATRINA_STOP_DIR set), every real
|
|
3049
3517
|
#' Stop writes `stop-<id>` BEFORE it sends a signal, so a signal-borne interrupt
|
|
3050
3518
|
#' with no flag for this run was sent for another — one that has already ended.
|
|
3051
3519
|
#' Without a flag folder (an older supervisor) nothing can be told apart, and
|
|
3052
3520
|
#' every interrupt is this run's, as it always was.
|
|
3053
3521
|
#' @param id The wire id of the running cell.
|
|
3054
3522
|
#' @return TRUE only when a flag folder exists and holds no flag for `id`.
|
|
3055
|
-
|
|
3056
|
-
nzchar(Sys.getenv("
|
|
3523
|
+
beatrina_interrupt_is_stray <- function(id) {
|
|
3524
|
+
nzchar(Sys.getenv("BEATRINA_STOP_DIR", "")) && !beatrina_stop_pending(id)
|
|
3057
3525
|
}
|
|
3058
3526
|
|
|
3059
3527
|
#' Resume from an interrupt that belongs to a run that has already ended.
|
|
@@ -3081,7 +3549,7 @@ carmar_interrupt_is_stray <- function(id) {
|
|
|
3081
3549
|
#'
|
|
3082
3550
|
#' @param i The interrupt condition.
|
|
3083
3551
|
#' @return Does not return when a `resume` restart exists; NULL otherwise.
|
|
3084
|
-
|
|
3552
|
+
beatrina_resume_if_stray <- function(i) {
|
|
3085
3553
|
if (!is.null(findRestart("resume"))) invokeRestart("resume")
|
|
3086
3554
|
invisible(NULL)
|
|
3087
3555
|
}
|
|
@@ -3102,13 +3570,13 @@ carmar_resume_if_stray <- function(i) {
|
|
|
3102
3570
|
#' has nothing worth resuming anyway.
|
|
3103
3571
|
#' @param id The wire id of the cell about to run.
|
|
3104
3572
|
#' @return NULL, invisibly.
|
|
3105
|
-
|
|
3573
|
+
beatrina_drain_stray_interrupt <- function(id) {
|
|
3106
3574
|
raised <- tryCatch({
|
|
3107
3575
|
n <- 0L
|
|
3108
3576
|
for (k in seq_len(1200L)) n <- n + 1L
|
|
3109
3577
|
FALSE
|
|
3110
3578
|
}, interrupt = function(i) TRUE)
|
|
3111
|
-
if (isTRUE(raised) &&
|
|
3579
|
+
if (isTRUE(raised) && beatrina_stop_pending(id)) {
|
|
3112
3580
|
signalCondition(structure(class = c("interrupt", "condition"),
|
|
3113
3581
|
list(message = "Execution interrupted", call = NULL)))
|
|
3114
3582
|
}
|
|
@@ -3125,12 +3593,12 @@ carmar_drain_stray_interrupt <- function(id) {
|
|
|
3125
3593
|
#' whole worker down with it, ending the session.
|
|
3126
3594
|
#' @param line One line as read_command returned it.
|
|
3127
3595
|
#' @return The command list, or NULL.
|
|
3128
|
-
|
|
3596
|
+
beatrina_parse_command <- function(line) {
|
|
3129
3597
|
withCallingHandlers({
|
|
3130
3598
|
cmd <- tryCatch(jsonlite::fromJSON(line), error = function(e) NULL)
|
|
3131
3599
|
if (!is.list(cmd) || !is.character(cmd$type) || length(cmd$type) != 1L) return(NULL)
|
|
3132
3600
|
resolve_cmdfile(cmd)
|
|
3133
|
-
}, interrupt =
|
|
3601
|
+
}, interrupt = beatrina_resume_if_stray)
|
|
3134
3602
|
}
|
|
3135
3603
|
|
|
3136
3604
|
#' Read one command line from stdin, tolerating an interrupt that lands while
|
|
@@ -3157,14 +3625,14 @@ read_command <- function(con) {
|
|
|
3157
3625
|
# half-way through when its interrupt fired — the supervisor's `ack` deadline
|
|
3158
3626
|
# (host/worker-plane.mjs) is the answer to that one.
|
|
3159
3627
|
line <- tryCatch(
|
|
3160
|
-
withCallingHandlers(readLines(con, n = 1L, warn = FALSE), interrupt =
|
|
3628
|
+
withCallingHandlers(readLines(con, n = 1L, warn = FALSE), interrupt = beatrina_resume_if_stray),
|
|
3161
3629
|
interrupt = function(i) NA_character_
|
|
3162
3630
|
)
|
|
3163
3631
|
# Strip the supervisor's comment tag; a line without it (older supervisor,
|
|
3164
3632
|
# test harness writing bare NDJSON) passes through untouched.
|
|
3165
3633
|
if (length(line) == 1L && !is.na(line) && nzchar(CMD_PREFIX) &&
|
|
3166
3634
|
startsWith(line, CMD_PREFIX)) {
|
|
3167
|
-
line <- substring(line, nchar(CMD_PREFIX) + 1L)
|
|
3635
|
+
line <- substring(line, nchar(CMD_PREFIX) + 1L, nchar(line))
|
|
3168
3636
|
}
|
|
3169
3637
|
line
|
|
3170
3638
|
}
|
|
@@ -3197,13 +3665,13 @@ resolve_cmdfile <- function(cmd) {
|
|
|
3197
3665
|
# `View(df)` must do what it does in RStudio. Overriding it on the SEARCH PATH
|
|
3198
3666
|
# rather than in globalenv keeps the user's environment clean (the Environment
|
|
3199
3667
|
# pane shows their objects, not ours) while still shadowing utils::View.
|
|
3200
|
-
|
|
3668
|
+
beatrina_tools <- new.env()
|
|
3201
3669
|
# Debug hooks live on the SEARCH PATH, not in this file's private scope, for a
|
|
3202
3670
|
# reason of visibility: a trace() tracer evaluates in the traced function's
|
|
3203
3671
|
# frame, whose lexical chain ends at globalenv() and then the search path —
|
|
3204
3672
|
# the worker's private local() is not on it. Same for expressions typed at a
|
|
3205
|
-
# Browse prompt, which is how .
|
|
3206
|
-
|
|
3673
|
+
# Browse prompt, which is how .beatrina_debug_where is called.
|
|
3674
|
+
beatrina_tools$.beatrina_debug_entered <- function(file, line, reason = "breakpoint") {
|
|
3207
3675
|
# Snapshot FIRST, then drop this function's own frame. Taking sys.calls()
|
|
3208
3676
|
# lazily inside the emit arguments would extend the stack through
|
|
3209
3677
|
# debug_stack/frame_vars themselves, defeat the machinery trimming, and put
|
|
@@ -3217,7 +3685,7 @@ carmar_tools$.carmar_debug_entered <- function(file, line, reason = "breakpoint"
|
|
|
3217
3685
|
locals = frame_vars(parent.frame())))
|
|
3218
3686
|
invisible(NULL)
|
|
3219
3687
|
}
|
|
3220
|
-
|
|
3688
|
+
beatrina_tools$.beatrina_debug_where <- function() {
|
|
3221
3689
|
calls <- sys.calls()
|
|
3222
3690
|
frames <- sys.frames()
|
|
3223
3691
|
n <- length(calls)
|
|
@@ -3232,7 +3700,7 @@ carmar_tools$.carmar_debug_where <- function() {
|
|
|
3232
3700
|
#' @param x HTML as a character vector (joined with newlines) or any value
|
|
3233
3701
|
#' `rich_html_of()` understands.
|
|
3234
3702
|
#' @return Invisibly `x`.
|
|
3235
|
-
|
|
3703
|
+
beatrina_tools$display_html <- function(x) {
|
|
3236
3704
|
rich <- if (is.character(x)) {
|
|
3237
3705
|
list(x = htmltools::HTML(paste(x, collapse = "\n")), kind = "html")
|
|
3238
3706
|
} else {
|
|
@@ -3247,16 +3715,16 @@ carmar_tools$display_html <- function(x) {
|
|
|
3247
3715
|
emit_rich(RUN_STATE$id, rich)
|
|
3248
3716
|
invisible(x)
|
|
3249
3717
|
}
|
|
3250
|
-
|
|
3718
|
+
beatrina_tools$View <- function(x, title = NULL) {
|
|
3251
3719
|
# The LABEL is what the user typed; the FETCH NAME is where the viewer reads
|
|
3252
|
-
# it from. Conflating them titled `View(mtcars)` as `.
|
|
3720
|
+
# it from. Conflating them titled `View(mtcars)` as `.beatrina_view`, because
|
|
3253
3721
|
# mtcars lives in the datasets package rather than the global environment.
|
|
3254
3722
|
label <- if (!is.null(title)) title else deparse(substitute(x))
|
|
3255
|
-
assign(".
|
|
3256
|
-
emit_view("view-request", ".
|
|
3723
|
+
assign(".beatrina_view", x, envir = globalenv())
|
|
3724
|
+
emit_view("view-request", ".beatrina_view", label = label)
|
|
3257
3725
|
invisible(NULL)
|
|
3258
3726
|
}
|
|
3259
|
-
attach(
|
|
3727
|
+
attach(beatrina_tools, name = "beatrina:tools", warn.conflicts = FALSE)
|
|
3260
3728
|
|
|
3261
3729
|
# Interactive mode reads the console; batch mode opens fd 0 as a connection.
|
|
3262
3730
|
# The console read is what lets browser() prompts and the dispatch loop share
|
|
@@ -3277,7 +3745,7 @@ if (identical(WORKER_MODE, "interactive")) {
|
|
|
3277
3745
|
}
|
|
3278
3746
|
|
|
3279
3747
|
# macOS 26 registers command-line R as a foreground application after Aqua or
|
|
3280
|
-
# AppKit initializes, even when the process was launched by
|
|
3748
|
+
# AppKit initializes, even when the process was launched by Beatrina's UIElement
|
|
3281
3749
|
# helper. The supervisor loads the packaged marker before sourcing this file
|
|
3282
3750
|
# (preventing an initial Dock flash); repeat the transition here, after all
|
|
3283
3751
|
# worker initialization and immediately before `ready`, so any framework that
|
|
@@ -3286,11 +3754,11 @@ if (identical(WORKER_MODE, "interactive")) {
|
|
|
3286
3754
|
# or restarted worker reaches this point automatically.
|
|
3287
3755
|
# OPT-IN — see macos_background_boot in kernel.R. The background transform hides
|
|
3288
3756
|
# the Dock icon but wedges R's console read on a GUI-launched macOS 26 worker,
|
|
3289
|
-
# so it is off unless
|
|
3757
|
+
# so it is off unless BEATRINA_MARK_BACKGROUND=1 is set.
|
|
3290
3758
|
if (identical(unname(Sys.info()[["sysname"]]), "Darwin") &&
|
|
3291
|
-
identical(Sys.getenv("
|
|
3292
|
-
is.loaded("
|
|
3293
|
-
invisible(try(.C("
|
|
3759
|
+
identical(Sys.getenv("BEATRINA_MARK_BACKGROUND", "0"), "1") &&
|
|
3760
|
+
is.loaded("beatrina_mark_background")) {
|
|
3761
|
+
invisible(try(.C("beatrina_mark_background", result = integer(1)), silent = TRUE))
|
|
3294
3762
|
}
|
|
3295
3763
|
|
|
3296
3764
|
# The ready frame says WHICH R this is. A session that silently uses the wrong
|
|
@@ -3308,13 +3776,13 @@ if (identical(unname(Sys.info()[["sysname"]]), "Darwin") &&
|
|
|
3308
3776
|
# Advertising the vocabulary lets a client know instantly. Kernels older than
|
|
3309
3777
|
# this simply omit the field, and clients fall back to probing.
|
|
3310
3778
|
#
|
|
3311
|
-
# A
|
|
3312
|
-
#
|
|
3313
|
-
#
|
|
3314
|
-
#
|
|
3315
|
-
#
|
|
3316
|
-
#
|
|
3317
|
-
#
|
|
3779
|
+
# A worker starts as a fresh R. Until 7.59 a "Restart into" handoff asked this
|
|
3780
|
+
# worker to `save.image()` and the successor loaded one file back inside a
|
|
3781
|
+
# deadline; that failed on large sessions and was removed ("restore work has
|
|
3782
|
+
# been abysmally bad"). Since 0.8.5 "Restart, keep variables" is an explicit
|
|
3783
|
+
# choice built differently (spike/workspace-keep.R): the page asks `suspend`
|
|
3784
|
+
# with no deadline, the new worker is asked `resume` by the supervisor after
|
|
3785
|
+
# `ready`, one file per object, and the report names what did not come back.
|
|
3318
3786
|
emit(list(type = "ready", pid = Sys.getpid(), r = R.version.string, cwd = getwd(),
|
|
3319
3787
|
# I(): a single library path must still ship as an array.
|
|
3320
3788
|
home = R.home(), libs = I(.libPaths()),
|
|
@@ -3328,8 +3796,9 @@ emit(list(type = "ready", pid = Sys.getpid(), r = R.version.string, cwd = getwd(
|
|
|
3328
3796
|
"project_status", "project_action",
|
|
3329
3797
|
"help", "hover", "wd", "files", "sniff",
|
|
3330
3798
|
"import", "readfile", "writefile", "writefiles_atomic", "view", "colstats",
|
|
3799
|
+
"column_values",
|
|
3331
3800
|
"mkdir", "renamepath", "deletepath", "copypath", "revealpath",
|
|
3332
|
-
"rm",
|
|
3801
|
+
"rm", "suspend",
|
|
3333
3802
|
if (identical(WORKER_MODE, "interactive")) "debug_breaks"))))
|
|
3334
3803
|
# (No package count here on purpose: installed.packages() reads every
|
|
3335
3804
|
# package's DESCRIPTION — 0.3–1.8 s on a big library — and no client ever
|
|
@@ -3342,7 +3811,7 @@ emit(list(type = "ready", pid = Sys.getpid(), r = R.version.string, cwd = getwd(
|
|
|
3342
3811
|
# parser's (a late signal, resumed), the dispatcher's (Stop as the cell's own
|
|
3343
3812
|
# outcome) — and the loop around them has the last word: an interrupt that
|
|
3344
3813
|
# escapes an iteration ends that iteration, never the loop, and one that escapes
|
|
3345
|
-
# the loop itself re-enters it through the `
|
|
3814
|
+
# the loop itself re-enters it through the `beatrina_serve_again` restart from a
|
|
3346
3815
|
# global calling handler. Before 2026-09-16 the parse step had no handler at all,
|
|
3347
3816
|
# and a late Stop signal raised there unwound the whole worker script into R's
|
|
3348
3817
|
# top-level prompt — where the `options(error=)` guard ended the process, or,
|
|
@@ -3365,7 +3834,7 @@ emit(list(type = "ready", pid = Sys.getpid(), r = R.version.string, cwd = getwd(
|
|
|
3365
3834
|
# view) catch it themselves, with meaning. The outer handler exists for the
|
|
3366
3835
|
# narrow window where one lands outside those — an uncaught interrupt ends
|
|
3367
3836
|
# the worker script, and the supervisor treats a dead worker as fatal.
|
|
3368
|
-
|
|
3837
|
+
beatrina_dispatch <- function(cmd) {
|
|
3369
3838
|
if (identical(cmd$type, "exec")) {
|
|
3370
3839
|
if (is.null(cmd$document)) run_cell(cmd$id, cmd$source, cmd$dims, cmd$srcname)
|
|
3371
3840
|
else run_document_cell(cmd$id, cmd$source, cmd$document, cmd$dims, cmd$srcname)
|
|
@@ -3379,6 +3848,8 @@ carmar_dispatch <- function(cmd) {
|
|
|
3379
3848
|
if (identical(cmd$type, "doctor")) emit_doctor(cmd$id)
|
|
3380
3849
|
if (identical(cmd$type, "complete")) emit_complete(cmd$id, cmd$line, cmd$cursor,
|
|
3381
3850
|
fn = cmd$fn, data = cmd$data)
|
|
3851
|
+
if (identical(cmd$type, "suspend")) emit_suspend(cmd$id)
|
|
3852
|
+
if (identical(cmd$type, "resume")) emit_resume(cmd$id, cmd$token)
|
|
3382
3853
|
if (identical(cmd$type, "packages")) emit_packages(cmd$id, cmd$scope)
|
|
3383
3854
|
if (identical(cmd$type, "package_action")) emit_package_action(cmd$id, cmd$action, cmd$name, cmd$lib)
|
|
3384
3855
|
if (identical(cmd$type, "package_help")) emit_package_help(cmd$id, cmd$name)
|
|
@@ -3411,6 +3882,9 @@ carmar_dispatch <- function(cmd) {
|
|
|
3411
3882
|
if (identical(cmd$type, "colstats")) emit_colstats(cmd$id, cmd$name, cmd$column,
|
|
3412
3883
|
query = cmd$query,
|
|
3413
3884
|
filters = cmd$filters)
|
|
3885
|
+
if (identical(cmd$type, "column_values")) emit_column_values(cmd$id, cmd$name, cmd$column,
|
|
3886
|
+
query = cmd$query,
|
|
3887
|
+
filters = cmd$filters)
|
|
3414
3888
|
if (identical(cmd$type, "rm")) emit_rm(cmd$id, cmd$names)
|
|
3415
3889
|
invisible(NULL)
|
|
3416
3890
|
}
|
|
@@ -3424,7 +3898,7 @@ carmar_dispatch <- function(cmd) {
|
|
|
3424
3898
|
# interrupts around the eval, but one landing in its epilogue (unlink,
|
|
3425
3899
|
# flush, the emit itself) used to escape this loop, end the worker script,
|
|
3426
3900
|
# and take the supervisor's event loop down with it.
|
|
3427
|
-
|
|
3901
|
+
beatrina_fail_frame <- function(cmd, text, outcome = "error") {
|
|
3428
3902
|
id_ok <- is.character(cmd$id) && length(cmd$id) == 1L
|
|
3429
3903
|
if (identical(cmd$type, "exec")) {
|
|
3430
3904
|
if (id_ok) emit(list(type = "done", id = cmd$id, status = outcome,
|
|
@@ -3438,11 +3912,11 @@ carmar_fail_frame <- function(cmd, text, outcome = "error") {
|
|
|
3438
3912
|
#' Serve one command. Returns "eof" when the supervisor has gone, "shutdown"
|
|
3439
3913
|
#' when it said so, NULL after a command or a line that was not one.
|
|
3440
3914
|
#' @param con The command connection (the console in interactive mode).
|
|
3441
|
-
|
|
3915
|
+
beatrina_serve_one <- function(con) {
|
|
3442
3916
|
line <- read_command(con)
|
|
3443
3917
|
if (length(line) == 0L) return("eof") # EOF: supervisor went away
|
|
3444
3918
|
if (is.na(line) || !nzchar(trimws(line))) return(NULL)
|
|
3445
|
-
cmd <-
|
|
3919
|
+
cmd <- beatrina_parse_command(line)
|
|
3446
3920
|
if (is.null(cmd)) return(NULL)
|
|
3447
3921
|
if (identical(cmd$type, "shutdown")) return("shutdown")
|
|
3448
3922
|
# The acknowledgement: the supervisor wrote this command to an idle worker and
|
|
@@ -3453,10 +3927,10 @@ carmar_serve_one <- function(con) {
|
|
|
3453
3927
|
# only wait forever — an exec has no deadline, by design. With it, a command
|
|
3454
3928
|
# that is never acknowledged is failed with a sentence and the queue moves on.
|
|
3455
3929
|
if (is.character(cmd$id) && length(cmd$id) == 1L && !is.na(cmd$id)) emit(list(type = "ack", id = cmd$id))
|
|
3456
|
-
tryCatch(
|
|
3457
|
-
error = function(e)
|
|
3930
|
+
tryCatch(allowInterrupts(beatrina_dispatch(cmd)),
|
|
3931
|
+
error = function(e) beatrina_fail_frame(cmd, paste("command failed:", conditionMessage(e))),
|
|
3458
3932
|
# Cleanup interrupts mean the same thing as interrupts during eval.
|
|
3459
|
-
interrupt = function(i)
|
|
3933
|
+
interrupt = function(i) beatrina_fail_frame(cmd, "Execution interrupted", "interrupted"))
|
|
3460
3934
|
NULL
|
|
3461
3935
|
}
|
|
3462
3936
|
|
|
@@ -3464,9 +3938,9 @@ carmar_serve_one <- function(con) {
|
|
|
3464
3938
|
#' iteration and nothing else.
|
|
3465
3939
|
#' @param con The command connection.
|
|
3466
3940
|
#' @return "eof" or "shutdown", invisibly.
|
|
3467
|
-
|
|
3941
|
+
beatrina_serve <- function(con) {
|
|
3468
3942
|
repeat {
|
|
3469
|
-
outcome <- tryCatch(
|
|
3943
|
+
outcome <- tryCatch(beatrina_serve_one(con), interrupt = function(i) NULL)
|
|
3470
3944
|
if (identical(outcome, "eof") || identical(outcome, "shutdown")) return(invisible(outcome))
|
|
3471
3945
|
}
|
|
3472
3946
|
}
|
|
@@ -3479,13 +3953,18 @@ carmar_serve <- function(con) {
|
|
|
3479
3953
|
# the process, as the `options(error=)` guard does for an error: a worker at
|
|
3480
3954
|
# R's top-level prompt is a zombie the supervisor cannot tell from an idle one.
|
|
3481
3955
|
globalCallingHandlers(interrupt = function(i) {
|
|
3482
|
-
if (!is.null(findRestart("
|
|
3956
|
+
if (!is.null(findRestart("beatrina_serve_again"))) invokeRestart("beatrina_serve_again")
|
|
3483
3957
|
quit(save = "no", status = 70L)
|
|
3484
3958
|
})
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3959
|
+
# Signals between commands must not unwind a consumed line or the loop's
|
|
3960
|
+
# restart setup. Only dispatch enables interrupts; run_cell distinguishes a
|
|
3961
|
+
# flagged Stop from a late signal, and its handlers are installed before entry.
|
|
3962
|
+
suspendInterrupts({
|
|
3963
|
+
repeat {
|
|
3964
|
+
outcome <- withRestarts(beatrina_serve(con), beatrina_serve_again = function() NULL)
|
|
3965
|
+
if (!is.null(outcome)) break
|
|
3966
|
+
}
|
|
3967
|
+
})
|
|
3489
3968
|
|
|
3490
3969
|
# stdin() is R's console, not ours to close. And an interactive R does not
|
|
3491
3970
|
# exit when the script does — it would sit at the top-level prompt waiting for
|