runtime: avoid websocket runtime drop panic
This commit is contained in:
parent
1173e3af38
commit
2b0901eb62
|
|
@ -2552,8 +2552,39 @@ struct TungstenitePluginWebSocketClient;
|
|||
type AsyncSystemWebSocket =
|
||||
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
|
||||
|
||||
struct PluginWebSocketRuntime {
|
||||
runtime: Option<TokioRuntime>,
|
||||
}
|
||||
|
||||
impl PluginWebSocketRuntime {
|
||||
fn new(runtime: TokioRuntime) -> Self {
|
||||
Self {
|
||||
runtime: Some(runtime),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self) -> &TokioRuntime {
|
||||
self.runtime
|
||||
.as_ref()
|
||||
.expect("plugin websocket runtime missing")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PluginWebSocketRuntime {
|
||||
fn drop(&mut self) {
|
||||
let Some(runtime) = self.runtime.take() else {
|
||||
return;
|
||||
};
|
||||
if TokioHandle::try_current().is_ok() {
|
||||
let _ = tokio::task::spawn_blocking(move || drop(runtime));
|
||||
} else {
|
||||
drop(runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TungstenitePluginWebSocketConnection {
|
||||
runtime: TokioRuntime,
|
||||
runtime: PluginWebSocketRuntime,
|
||||
socket: AsyncSystemWebSocket,
|
||||
}
|
||||
|
||||
|
|
@ -2592,7 +2623,7 @@ impl PluginWebSocketClient for TungstenitePluginWebSocketClient {
|
|||
)
|
||||
.await
|
||||
};
|
||||
let (socket, _response) = block_on_websocket_future(&runtime, open)
|
||||
let (socket, _response) = block_on_websocket_future(runtime.get(), open)
|
||||
.map_err(|error| {
|
||||
PluginWebSocketError::new(format!(
|
||||
"WebSocket open timed out after {} ms for {}: {error}",
|
||||
|
|
@ -2619,7 +2650,7 @@ impl PluginWebSocketConnection for TungstenitePluginWebSocketConnection {
|
|||
PLUGIN_WEBSOCKET_DEFAULT_TIMEOUT,
|
||||
self.socket.send(Message::Text(text.to_string().into())),
|
||||
);
|
||||
block_on_websocket_future(&self.runtime, send)
|
||||
block_on_websocket_future(self.runtime.get(), send)
|
||||
.map_err(|_| PluginWebSocketError::new("WebSocket send timed out"))?
|
||||
.map_err(|error| PluginWebSocketError::new(format!("WebSocket send failed: {error}")))
|
||||
}
|
||||
|
|
@ -2631,7 +2662,7 @@ impl PluginWebSocketConnection for TungstenitePluginWebSocketConnection {
|
|||
) -> Result<PluginWebSocketRecvResponse, PluginWebSocketError> {
|
||||
for _ in 0..PLUGIN_WEBSOCKET_MAX_CONTROL_FRAMES {
|
||||
let next = tokio::time::timeout(timeout, self.socket.next());
|
||||
let message = block_on_websocket_future(&self.runtime, next)
|
||||
let message = block_on_websocket_future(self.runtime.get(), next)
|
||||
.map_err(|_| PluginWebSocketError::new("WebSocket receive timed out"))?
|
||||
.ok_or_else(|| PluginWebSocketError::new("WebSocket stream ended"))?
|
||||
.map_err(|error| {
|
||||
|
|
@ -2660,7 +2691,7 @@ impl PluginWebSocketConnection for TungstenitePluginWebSocketConnection {
|
|||
PLUGIN_WEBSOCKET_DEFAULT_TIMEOUT,
|
||||
self.socket.send(Message::Pong(payload)),
|
||||
);
|
||||
block_on_websocket_future(&self.runtime, send)
|
||||
block_on_websocket_future(self.runtime.get(), send)
|
||||
.map_err(|_| PluginWebSocketError::new("WebSocket pong timed out"))?
|
||||
.map_err(|error| {
|
||||
PluginWebSocketError::new(format!("WebSocket pong failed: {error}"))
|
||||
|
|
@ -2676,19 +2707,20 @@ impl PluginWebSocketConnection for TungstenitePluginWebSocketConnection {
|
|||
|
||||
fn close(&mut self) -> Result<(), PluginWebSocketError> {
|
||||
let close = tokio::time::timeout(PLUGIN_WEBSOCKET_DEFAULT_TIMEOUT, self.socket.close(None));
|
||||
block_on_websocket_future(&self.runtime, close)
|
||||
block_on_websocket_future(self.runtime.get(), close)
|
||||
.map_err(|_| PluginWebSocketError::new("WebSocket close timed out"))?
|
||||
.map_err(|error| PluginWebSocketError::new(format!("WebSocket close failed: {error}")))
|
||||
}
|
||||
}
|
||||
|
||||
fn new_websocket_runtime() -> Result<TokioRuntime, PluginWebSocketError> {
|
||||
TokioRuntimeBuilder::new_current_thread()
|
||||
fn new_websocket_runtime() -> Result<PluginWebSocketRuntime, PluginWebSocketError> {
|
||||
let runtime = TokioRuntimeBuilder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
PluginWebSocketError::new(format!("WebSocket runtime build failed: {error}"))
|
||||
})
|
||||
})?;
|
||||
Ok(PluginWebSocketRuntime::new(runtime))
|
||||
}
|
||||
|
||||
fn block_on_websocket_future<F: std::future::Future>(
|
||||
|
|
@ -5656,6 +5688,12 @@ mod tests {
|
|||
use std::sync::{Arc, Mutex};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn websocket_runtime_drop_is_safe_inside_async_context() {
|
||||
let runtime = new_websocket_runtime().unwrap();
|
||||
drop(runtime);
|
||||
}
|
||||
|
||||
fn tool(name: &str) -> manifest::plugin::PluginToolManifest {
|
||||
manifest::plugin::PluginToolManifest {
|
||||
name: name.into(),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ FRONTEND_PORT="${YOI_DEV_FRONTEND_PORT:-5173}"
|
|||
|
||||
RUNTIME_ENABLED="${YOI_DEV_RUNTIME_ENABLED:-1}"
|
||||
ACTION_DELAY_SECONDS="${YOI_DEV_ACTION_DELAY_SECONDS:-60}"
|
||||
WORKDIR_ID="$(basename "$(dirname "$(dirname "$ROOT_DIR")")")"
|
||||
UNIT_PREFIX="${YOI_DEV_SYSTEMD_UNIT_PREFIX:-yoi-dev-$WORKDIR_ID}"
|
||||
USE_SYSTEMD="${YOI_DEV_USE_SYSTEMD:-1}"
|
||||
FOREGROUND_MODE="${YOI_DEV_WORKSPACE_FOREGROUND:-0}"
|
||||
|
||||
usage() {
|
||||
|
|
@ -39,8 +42,8 @@ usage() {
|
|||
Usage: $(basename "$0") <start|stop|restart|status>
|
||||
|
||||
Manage the local Yoi development stack for this checkout:
|
||||
runtime cargo run -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server -- --bind $RUNTIME_BIND
|
||||
backend cargo run -p yoi-workspace-server -- serve --workspace $ROOT_DIR --db $ROOT_DIR/.yoi/workspace.db --listen $BACKEND_LISTEN
|
||||
runtime target/debug/worker-runtime-rest-server --bind $RUNTIME_BIND
|
||||
backend target/debug/yoi-workspace-server serve --workspace $ROOT_DIR --db $ROOT_DIR/.yoi/workspace.db --listen $BACKEND_LISTEN
|
||||
frontend deno run -A npm:vite@7.2.7 dev --host $FRONTEND_HOST --port $FRONTEND_PORT (cwd: web/workspace)
|
||||
|
||||
Actions:
|
||||
|
|
@ -49,7 +52,7 @@ Actions:
|
|||
restart schedule a detached job that stops/starts runtime and backend only; frontend is left untouched
|
||||
status print pidfile and port-listener status without mutating processes
|
||||
|
||||
By default, start/stop/restart return immediately and run in a detached job after $ACTION_DELAY_SECONDS seconds.
|
||||
By default, start/stop/restart return immediately and run in a fully detached nohup+setsid job after $ACTION_DELAY_SECONDS seconds.
|
||||
This keeps API/tool-call sessions intact while the backend/runtime are restarted. Set
|
||||
YOI_DEV_WORKSPACE_FOREGROUND=1 to run the mutating action synchronously.
|
||||
|
||||
|
|
@ -69,6 +72,18 @@ log() {
|
|||
printf '[dev-workspace] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
run_cargo() {
|
||||
if command -v cc >/dev/null 2>&1 && command -v pkg-config >/dev/null 2>&1; then
|
||||
cargo "$@"
|
||||
return
|
||||
fi
|
||||
if command -v nix >/dev/null 2>&1 && [[ -f "$ROOT_DIR/flake.nix" ]]; then
|
||||
nix develop "$ROOT_DIR" -c cargo "$@"
|
||||
return
|
||||
fi
|
||||
cargo "$@"
|
||||
}
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "$PID_DIR" "$LOG_DIR"
|
||||
}
|
||||
|
|
@ -77,6 +92,24 @@ pid_file() {
|
|||
printf '%s/%s.pid' "$PID_DIR" "$1"
|
||||
}
|
||||
|
||||
systemd_unit_name() {
|
||||
local name="$1"
|
||||
printf '%s-%s.service' "$UNIT_PREFIX" "$name"
|
||||
}
|
||||
|
||||
systemd_available() {
|
||||
[[ "$USE_SYSTEMD" != "0" ]] || return 1
|
||||
command -v systemd-run >/dev/null 2>&1 || return 1
|
||||
systemctl --user is-system-running >/dev/null 2>&1 || return 1
|
||||
}
|
||||
|
||||
systemd_main_pid() {
|
||||
local name="$1"
|
||||
local unit
|
||||
unit="$(systemd_unit_name "$name")"
|
||||
systemctl --user show -P MainPID "$unit" 2>/dev/null | awk '$1 != "" && $1 != "0" { print $1; exit }'
|
||||
}
|
||||
|
||||
log_file() {
|
||||
printf '%s/%s.log' "$LOG_DIR" "$1"
|
||||
}
|
||||
|
|
@ -155,20 +188,27 @@ stop_pid() {
|
|||
|
||||
stop_managed_service() {
|
||||
local name="$1"
|
||||
local file
|
||||
local file unit pid
|
||||
file="$(pid_file "$name")"
|
||||
if [[ ! -f "$file" ]]; then
|
||||
return 0
|
||||
|
||||
if systemd_available; then
|
||||
unit="$(systemd_unit_name "$name")"
|
||||
if systemctl --user is-active --quiet "$unit" 2>/dev/null; then
|
||||
log "stopping $name systemd unit $unit"
|
||||
systemctl --user stop "$unit" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
local pid
|
||||
pid="$(cat "$file")"
|
||||
if is_running "$pid"; then
|
||||
stop_pid "$pid" "$name"
|
||||
if [[ -f "$file" ]]; then
|
||||
pid="$(cat "$file")"
|
||||
if is_running "$pid"; then
|
||||
stop_pid "$pid" "$name"
|
||||
fi
|
||||
rm -f "$file"
|
||||
fi
|
||||
rm -f "$file"
|
||||
}
|
||||
|
||||
|
||||
stop_port_listeners() {
|
||||
local label="$1"
|
||||
local port="$2"
|
||||
|
|
@ -209,35 +249,88 @@ start_service() {
|
|||
stop_managed_service "$name"
|
||||
stop_port_listeners "$name" "$port"
|
||||
|
||||
local logfile pidfile
|
||||
local logfile pidfile pid
|
||||
logfile="$(log_file "$name")"
|
||||
pidfile="$(pid_file "$name")"
|
||||
: >"$logfile"
|
||||
|
||||
if systemd_available; then
|
||||
local unit
|
||||
unit="$(systemd_unit_name "$name")"
|
||||
log "starting $name systemd unit $unit; log: $logfile"
|
||||
systemd-run --user --unit="$unit" --collect --same-dir --working-directory="$cwd" \
|
||||
--property="StandardOutput=append:$logfile" \
|
||||
--property="StandardError=append:$logfile" \
|
||||
--property="KillMode=control-group" \
|
||||
"$@" >/dev/null
|
||||
for _ in {1..50}; do
|
||||
pid="$(systemd_main_pid "$name" || true)"
|
||||
if [[ -n "$pid" ]]; then
|
||||
printf '%s\n' "$pid" >"$pidfile"
|
||||
log "$name systemd pid $pid"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
printf '%s\n' "$name systemd unit did not expose MainPID" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "starting $name; log: $logfile"
|
||||
(
|
||||
cd "$cwd"
|
||||
exec setsid "$@"
|
||||
) >"$logfile" 2>&1 &
|
||||
local pid="$!"
|
||||
pid="$!"
|
||||
printf '%s\n' "$pid" >"$pidfile"
|
||||
log "$name pid $pid"
|
||||
}
|
||||
|
||||
|
||||
build_runtime_backend() {
|
||||
if [[ "$RUNTIME_ENABLED" != "0" ]]; then
|
||||
log "building runtime binary"
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
run_cargo build -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server
|
||||
)
|
||||
else
|
||||
log "runtime disabled by YOI_DEV_RUNTIME_ENABLED=0; skipping runtime build"
|
||||
fi
|
||||
|
||||
log "building backend binary"
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
run_cargo build -p yoi-workspace-server --bin yoi-workspace-server
|
||||
)
|
||||
}
|
||||
|
||||
start_runtime() {
|
||||
if [[ "$RUNTIME_ENABLED" == "0" ]]; then
|
||||
log "runtime disabled by YOI_DEV_RUNTIME_ENABLED=0"
|
||||
return 0
|
||||
fi
|
||||
local port
|
||||
local port runtime_bin
|
||||
port="$(port_for_addr "$RUNTIME_BIND")"
|
||||
runtime_bin="$ROOT_DIR/target/debug/worker-runtime-rest-server"
|
||||
if [[ ! -x "$runtime_bin" ]]; then
|
||||
printf 'runtime binary not found or not executable: %s\n' "$runtime_bin" >&2
|
||||
return 1
|
||||
fi
|
||||
start_service runtime "$ROOT_DIR" "$port" \
|
||||
cargo run -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server -- --bind "$RUNTIME_BIND"
|
||||
"$runtime_bin" --bind "$RUNTIME_BIND"
|
||||
}
|
||||
|
||||
start_backend() {
|
||||
local port
|
||||
local port backend_bin
|
||||
port="$(port_for_addr "$BACKEND_LISTEN")"
|
||||
backend_bin="$ROOT_DIR/target/debug/yoi-workspace-server"
|
||||
if [[ ! -x "$backend_bin" ]]; then
|
||||
printf 'backend binary not found or not executable: %s\n' "$backend_bin" >&2
|
||||
return 1
|
||||
fi
|
||||
start_service backend "$ROOT_DIR" "$port" \
|
||||
cargo run -p yoi-workspace-server -- serve --workspace "$ROOT_DIR" --db "$ROOT_DIR/.yoi/workspace.db" --listen "$BACKEND_LISTEN"
|
||||
"$backend_bin" serve --workspace "$ROOT_DIR" --db "$ROOT_DIR/.yoi/workspace.db" --listen "$BACKEND_LISTEN"
|
||||
}
|
||||
|
||||
start_frontend() {
|
||||
|
|
@ -256,6 +349,7 @@ stop_runtime_backend() {
|
|||
}
|
||||
|
||||
start_runtime_backend() {
|
||||
build_runtime_backend
|
||||
start_runtime
|
||||
start_backend
|
||||
}
|
||||
|
|
@ -285,16 +379,26 @@ status_service() {
|
|||
local port="$2"
|
||||
local managed="-"
|
||||
local listeners="-"
|
||||
if service_pid "$name" >/dev/null; then
|
||||
local unit="-"
|
||||
local systemd_pid=""
|
||||
if systemd_available; then
|
||||
unit="$(systemd_unit_name "$name")"
|
||||
systemd_pid="$(systemd_main_pid "$name" || true)"
|
||||
if [[ -n "$systemd_pid" ]]; then
|
||||
managed="$systemd_pid"
|
||||
fi
|
||||
fi
|
||||
if [[ "$managed" == "-" ]] && service_pid "$name" >/dev/null; then
|
||||
managed="$(service_pid "$name")"
|
||||
fi
|
||||
mapfile -t pids < <(listener_pids_for_port "$port" || true)
|
||||
if [[ "${#pids[@]}" -gt 0 ]]; then
|
||||
listeners="${pids[*]}"
|
||||
fi
|
||||
printf '%-8s managed_pid=%-8s port=%-6s listener_pids=%s\n' "$name" "$managed" "$port" "$listeners"
|
||||
printf '%-8s managed_pid=%-8s port=%-6s listener_pids=%-12s unit=%s\n' "$name" "$managed" "$port" "$listeners" "$unit"
|
||||
}
|
||||
|
||||
|
||||
status_all() {
|
||||
ensure_dirs
|
||||
status_service runtime "$(port_for_addr "$RUNTIME_BIND")"
|
||||
|
|
@ -311,7 +415,7 @@ schedule_detached_action() {
|
|||
job_log="$LOG_DIR/${action}-$stamp.job.log"
|
||||
|
||||
log "scheduling $action in ${ACTION_DELAY_SECONDS}s; log: $job_log"
|
||||
setsid bash -lc '
|
||||
nohup setsid bash -c '
|
||||
set -euo pipefail
|
||||
delay="$1"
|
||||
root="$2"
|
||||
|
|
@ -324,9 +428,11 @@ schedule_detached_action() {
|
|||
printf "[%s] dev-workspace %s finished with status %s\n" "$(date -Is)" "$action" "$status"
|
||||
exit "$status"
|
||||
' dev-workspace-job "$ACTION_DELAY_SECONDS" "$ROOT_DIR" "$action" >>"$job_log" 2>&1 < /dev/null &
|
||||
local scheduled_pid="$!"
|
||||
disown "$scheduled_pid" 2>/dev/null || true
|
||||
|
||||
printf 'scheduled_action=%s\n' "$action"
|
||||
printf 'scheduled_pid=%s\n' "$!"
|
||||
printf 'scheduled_pid=%s\n' "$scheduled_pid"
|
||||
printf 'scheduled_after_seconds=%s\n' "$ACTION_DELAY_SECONDS"
|
||||
printf 'scheduled_log=%s\n' "$job_log"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user