Skip to content

Commit 1dd89a2

Browse files
committed
Inform the user when the process exits unexpectedly
Closes #988
1 parent 581fff4 commit 1dd89a2

File tree

8 files changed

+220
-17
lines changed

8 files changed

+220
-17
lines changed

compiler/base/orchestrator/src/coordinator.rs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ use tracing::{instrument, trace, trace_span, warn, Instrument};
2828
use crate::{
2929
bincode_input_closed,
3030
message::{
31-
CoordinatorMessage, DeleteFileRequest, ExecuteCommandRequest, JobId, Multiplexed,
32-
OneToOneResponse, ReadFileRequest, ReadFileResponse, SerializedError, WorkerMessage,
33-
WriteFileRequest,
31+
CoordinatorMessage, DeleteFileRequest, ExecuteCommandRequest, ExecuteCommandResponse,
32+
JobId, Multiplexed, OneToOneResponse, ReadFileRequest, ReadFileResponse, SerializedError,
33+
WorkerMessage, WriteFileRequest,
3434
},
3535
DropErrorDetailsExt,
3636
};
@@ -233,6 +233,7 @@ impl CargoTomlModifier for ExecuteRequest {
233233
#[derive(Debug, Clone)]
234234
pub struct ExecuteResponse {
235235
pub success: bool,
236+
pub exit_detail: String,
236237
}
237238

238239
#[derive(Debug, Clone)]
@@ -338,6 +339,7 @@ impl CargoTomlModifier for CompileRequest {
338339
#[derive(Debug, Clone)]
339340
pub struct CompileResponse {
340341
pub success: bool,
342+
pub exit_detail: String,
341343
pub code: String,
342344
}
343345

@@ -627,11 +629,17 @@ impl Container {
627629
.context(CouldNotStartCargoSnafu)?;
628630

629631
let task = async move {
630-
let success = task
632+
let ExecuteCommandResponse {
633+
success,
634+
exit_detail,
635+
} = task
631636
.await
632637
.context(CargoTaskPanickedSnafu)?
633638
.context(CargoFailedSnafu)?;
634-
Ok(ExecuteResponse { success })
639+
Ok(ExecuteResponse {
640+
success,
641+
exit_detail,
642+
})
635643
}
636644
.boxed();
637645

@@ -693,7 +701,10 @@ impl Container {
693701

694702
let commander = self.commander.clone();
695703
let task = async move {
696-
let success = task
704+
let ExecuteCommandResponse {
705+
success,
706+
exit_detail,
707+
} = task
697708
.await
698709
.context(CargoTaskPanickedSnafu)?
699710
.context(CargoFailedSnafu)?;
@@ -711,7 +722,11 @@ impl Container {
711722
// TODO: This is synchronous...
712723
let code = request.postprocess_result(code);
713724

714-
Ok(CompileResponse { success, code })
725+
Ok(CompileResponse {
726+
success,
727+
exit_detail,
728+
code,
729+
})
715730
}
716731
.boxed();
717732

@@ -744,7 +759,7 @@ impl Container {
744759

745760
match container_msg {
746761
WorkerMessage::ExecuteCommand(resp) => {
747-
return Ok(resp.success);
762+
return Ok(resp);
748763
}
749764
WorkerMessage::StdoutPacket(packet) => {
750765
stdout_tx.send(packet).await.ok(/* Receiver gone, that's OK */);
@@ -869,7 +884,7 @@ pub enum CompileError {
869884
}
870885

871886
struct SpawnCargo {
872-
task: JoinHandle<Result<bool, SpawnCargoError>>,
887+
task: JoinHandle<Result<ExecuteCommandResponse, SpawnCargoError>>,
873888
stdout_rx: mpsc::Receiver<String>,
874889
stderr_rx: mpsc::Receiver<String>,
875890
}
@@ -2060,6 +2075,31 @@ mod tests {
20602075
Ok(())
20612076
}
20622077

2078+
#[tokio::test]
2079+
#[snafu::report]
2080+
async fn exit_due_to_signal_is_reported() -> Result<()> {
2081+
let coordinator = new_coordinator().await;
2082+
2083+
let req = ExecuteRequest {
2084+
channel: Channel::Stable,
2085+
mode: Mode::Release,
2086+
edition: Edition::Rust2021,
2087+
crate_type: CrateType::Binary,
2088+
tests: false,
2089+
backtrace: false,
2090+
code: r#"fn main() { std::process::abort(); }"#.into(),
2091+
};
2092+
2093+
let res = coordinator.execute(req.clone()).await.unwrap();
2094+
2095+
assert!(!res.success);
2096+
assert_contains!(res.exit_detail, "abort");
2097+
2098+
coordinator.shutdown().await?;
2099+
2100+
Ok(())
2101+
}
2102+
20632103
fn new_execution_limited_request() -> ExecuteRequest {
20642104
ExecuteRequest {
20652105
channel: Channel::Stable,

compiler/base/orchestrator/src/message.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub struct ExecuteCommandRequest {
117117
#[derive(Debug, Serialize, Deserialize)]
118118
pub struct ExecuteCommandResponse {
119119
pub success: bool,
120+
pub exit_detail: String,
120121
}
121122

122123
#[derive(Debug, Serialize, Deserialize)]

compiler/base/orchestrator/src/worker.rs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use std::{
3636
collections::HashMap,
3737
io,
3838
path::{Path, PathBuf},
39-
process::Stdio,
39+
process::{ExitStatus, Stdio},
4040
};
4141
use tokio::{
4242
fs,
@@ -509,7 +509,111 @@ async fn process_end(
509509
}
510510

511511
let success = status.success();
512-
Ok(ExecuteCommandResponse { success })
512+
let exit_detail = extract_exit_detail(status);
513+
514+
Ok(ExecuteCommandResponse {
515+
success,
516+
exit_detail,
517+
})
518+
}
519+
520+
mod signals {
521+
mod descriptions {
522+
#![allow(dead_code)]
523+
524+
pub const SIGABRT: &str = "abort program";
525+
pub const SIGALRM: &str = "real-time timer expired";
526+
pub const SIGBUS: &str = "bus error";
527+
pub const SIGEMT: &str = "emulate instruction executed";
528+
pub const SIGFPE: &str = "floating-point exception";
529+
pub const SIGHUP: &str = "terminal line hangup";
530+
pub const SIGILL: &str = "illegal instruction";
531+
pub const SIGINT: &str = "interrupt program";
532+
pub const SIGKILL: &str = "kill program";
533+
pub const SIGPIPE: &str = "write on a pipe with no reader";
534+
pub const SIGQUIT: &str = "quit program";
535+
pub const SIGSEGV: &str = "segmentation violation";
536+
pub const SIGSYS: &str = "non-existent system call invoked";
537+
pub const SIGTERM: &str = "software termination signal";
538+
pub const SIGTRAP: &str = "trace trap";
539+
pub const SIGUSR1: &str = "user-defined signal 1";
540+
pub const SIGUSR2: &str = "user-defined signal 2";
541+
}
542+
543+
type Pair = (&'static str, &'static str);
544+
545+
macro_rules! sigtable {
546+
[$($name:ident,)*] => {
547+
[
548+
$((stringify!($name), descriptions::$name),)*
549+
]
550+
};
551+
}
552+
553+
#[cfg(target_os = "macos")]
554+
const SIGNALS: [Pair; 15] = sigtable![
555+
SIGHUP, // 1
556+
SIGINT, // 2
557+
SIGQUIT, // 3
558+
SIGILL, // 4
559+
SIGTRAP, // 5
560+
SIGABRT, // 6
561+
SIGEMT, // 7
562+
SIGFPE, // 8
563+
SIGKILL, // 9
564+
SIGBUS, // 10
565+
SIGSEGV, // 11
566+
SIGSYS, // 12
567+
SIGPIPE, // 13
568+
SIGALRM, // 14
569+
SIGTERM, // 15
570+
];
571+
572+
#[cfg(target_os = "linux")]
573+
const SIGNALS: [Pair; 15] = sigtable![
574+
SIGHUP, // 1
575+
SIGINT, // 2
576+
SIGQUIT, // 3
577+
SIGILL, // 4
578+
SIGTRAP, // 5
579+
SIGABRT, // 6
580+
SIGBUS, // 7
581+
SIGFPE, // 8
582+
SIGKILL, // 9
583+
SIGUSR1, // 10
584+
SIGSEGV, // 11
585+
SIGUSR2, // 12
586+
SIGPIPE, // 13
587+
SIGALRM, // 14
588+
SIGTERM, // 15
589+
];
590+
591+
const SIG_UNKNOWN: Pair = ("???", "Unknown signal");
592+
593+
pub fn get(signal: i32) -> Pair {
594+
let details = (|| {
595+
let signal = usize::try_from(signal).ok()?;
596+
let signal = signal.checked_sub(1)?;
597+
SIGNALS.get(signal).copied()
598+
})();
599+
600+
details.unwrap_or(SIG_UNKNOWN)
601+
}
602+
}
603+
604+
fn extract_exit_detail(status: ExitStatus) -> String {
605+
use std::os::unix::process::ExitStatusExt;
606+
607+
if let Some(code) = status.code() {
608+
return format!("Exited with status {code}");
609+
}
610+
611+
if let Some(signal) = status.signal() {
612+
let (name, description) = signals::get(signal);
613+
return format!("Exited with signal {signal} ({name}): {description}");
614+
}
615+
616+
String::new()
513617
}
514618

515619
#[derive(Debug, Snafu)]

tests/spec/features/segfault_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require 'spec_helper'
2+
require 'support/editor'
3+
require 'support/playground_actions'
4+
5+
RSpec.feature "When the code triggers a signal", type: :feature, js: true do
6+
include PlaygroundActions
7+
8+
before { visit '/' }
9+
10+
scenario "when a crate type is provided" do
11+
code = <<~EOF
12+
fn main() {
13+
unsafe { *(0xDECAF_000 as *mut usize) = 1 };
14+
}
15+
EOF
16+
editor.set(code)
17+
18+
click_on("Run")
19+
within(:output, :error) do
20+
expect(page).to have_content "signal 11"
21+
expect(page).to have_content "SIGSEGV"
22+
expect(page).to have_content "segmentation violation"
23+
end
24+
end
25+
26+
def editor
27+
Editor.new(page)
28+
end
29+
end

ui/frontend/reducers/output/execute.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const { action: wsExecuteEnd, schema: wsExecuteEndSchema } = createWebsocketResp
5151
'output/execute/wsExecuteEnd',
5252
z.object({
5353
success: z.boolean(),
54+
exitDetail: z.string(),
5455
}),
5556
);
5657

@@ -68,6 +69,7 @@ export interface ExecuteRequestBody {
6869

6970
interface ExecuteResponseBody {
7071
success: boolean;
72+
exitDetail: string;
7173
stdout: string;
7274
stderr: string;
7375
}
@@ -113,9 +115,12 @@ const slice = createSlice({
113115
state.requestsInProgress += 1;
114116
})
115117
.addCase(performExecute.fulfilled, (state, action) => {
116-
const { stdout, stderr } = action.payload;
118+
const { success, exitDetail, stdout, stderr } = action.payload;
117119
Object.assign(state, { stdout, stderr });
118120
delete state.error;
121+
if (!success) {
122+
state.error = exitDetail;
123+
}
119124
state.requestsInProgress -= 1;
120125
})
121126
.addCase(performExecute.rejected, (state, action) => {
@@ -148,8 +153,12 @@ const slice = createSlice({
148153
)
149154
.addCase(
150155
wsExecuteEnd,
151-
sequenceNumberMatches((state) => {
156+
sequenceNumberMatches((state, payload) => {
152157
state.requestsInProgress = 0; // Only tracking one request
158+
159+
if (!payload.success) {
160+
state.error = payload.exitDetail;
161+
}
153162
}),
154163
);
155164
},

ui/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ struct CompileRequest {
284284
#[derive(Debug, Clone, Serialize)]
285285
struct CompileResponse {
286286
success: bool,
287+
#[serde(rename = "exitDetail")]
288+
exit_detail: String,
287289
code: String,
288290
stdout: String,
289291
stderr: String,
@@ -306,6 +308,8 @@ struct ExecuteRequest {
306308
#[derive(Debug, Clone, Serialize)]
307309
struct ExecuteResponse {
308310
success: bool,
311+
#[serde(rename = "exitDetail")]
312+
exit_detail: String,
309313
stdout: String,
310314
stderr: String,
311315
}

ui/src/server_axum.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -904,10 +904,15 @@ pub(crate) mod api_orchestrator_integration_impls {
904904
stdout,
905905
stderr,
906906
} = other;
907-
let CompileResponse { success, code } = response;
907+
let CompileResponse {
908+
success,
909+
exit_detail,
910+
code,
911+
} = response;
908912

909913
Self {
910914
success,
915+
exit_detail,
911916
code,
912917
stdout,
913918
stderr,
@@ -963,10 +968,14 @@ pub(crate) mod api_orchestrator_integration_impls {
963968
stdout,
964969
stderr,
965970
} = other;
966-
let ExecuteResponse { success } = response;
971+
let ExecuteResponse {
972+
success,
973+
exit_detail,
974+
} = response;
967975

968976
Self {
969977
success,
978+
exit_detail,
970979
stdout,
971980
stderr,
972981
}

0 commit comments

Comments
 (0)