Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions crates/turborepo-lib/src/commands/run.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{env, future::Future, sync::Arc};

use tracing::{error, Instrument};
use tracing::Instrument;
use turborepo_api_client::SharedHttpClient;
use turborepo_log::StructuredLogSink;
use turborepo_query_api::QueryServer;
Expand Down Expand Up @@ -140,7 +140,9 @@ pub async fn run(
let (sender, handle) = {
let _span = tracing::info_span!("start_ui").entered();
// The TUI needs a handle to the terminal sink so it can re-enable
// streamed output when the user toggles out of the alternate screen.
// streamed output when the user toggles out of the alternate
// screen, and its watchdog restores streamed output if the
// render thread exits mid-run.
run.start_ui(sinks.terminal.clone())?.unzip()
};

Expand Down Expand Up @@ -175,12 +177,11 @@ pub async fn run(
sender.stop().await;
}

// Wait for TUI cleanup (terminal restoration, task persistence)
// before printing anything else; render errors are logged by the
// watchdog inside `start_ui`.
if let Some(handle) = handle {
match handle.await {
Ok(Err(e)) => error!("error encountered rendering tui: {e}"),
Err(e) => error!("render thread panicked: {e}"),
Ok(Ok(())) => {}
}
handle.await.ok();
}

if let Some(path) = subscriber.stderr_redirect_path() {
Expand Down
77 changes: 72 additions & 5 deletions crates/turborepo-lib/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,17 @@ impl EngineExt for Engine<Built> {
})
.filter_map(|task| {
let pkg_name = PackageName::from(task.package());
let json = pkg_graph.package_json(&pkg_name)?;
// TODO: delegate to command factory to filter down tasks to those that will
// have a runnable command.
(task.task() == "proxy" || json.command(task.task()).is_some())
.then(|| task.to_string())
let info = pkg_graph.package_info(&pkg_name)?;
// Ask the package's toolchain whether the task resolves to a
// runnable command — the same authority execution uses. For
// JS packages this is the package.json scripts lookup; for
// Cargo packages it consults the verb tables, so toolchain
// tasks appear in the TUI task list.
let defines_task = pkg_graph
.toolchains()
.get(&info.toolchain)
.is_some_and(|toolchain| toolchain.defines_task(info, task.task()));
(task.task() == "proxy" || defines_task).then(|| task.to_string())
})
.collect()
}
Expand Down Expand Up @@ -303,6 +309,67 @@ mod test {
}
}

#[tokio::test(flavor = "multi_thread")]
async fn test_tasks_with_command_asks_toolchains() {
// The TUI task list must come from the same authority execution
// uses: the package's toolchain. JS packages resolve via
// package.json scripts; Cargo packages resolve via the toolchain's
// verb tables — no scripts anywhere.
let tmp = tempfile::TempDir::with_prefix("tasks_with_command").unwrap();
let root = AbsoluteSystemPath::from_std_path(tmp.path()).unwrap();

// A minimal Cargo workspace with one binary crate.
root.join_component("Cargo.toml")
.create_with_contents("[workspace]\nmembers = [\"crates/*\"]\nresolver = \"2\"\n")
.unwrap();
let crate_dir = root.join_components(&["crates", "my-crate"]);
crate_dir.join_component("src").create_dir_all().unwrap();
crate_dir
.join_component("Cargo.toml")
.create_with_contents(
"[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
crate_dir
.join_components(&["src", "main.rs"])
.create_with_contents("fn main() {}\n")
.unwrap();

let mut engine: Engine<Building> = Engine::new();
for (package, task) in [
// JS package with a build script (DummyDiscovery gives "a" one).
("a", "build"),
// JS package without any scripts.
("c", "build"),
// The synthetic Cargo workspace package.
("cargo", "test"),
// A binary crate.
("my-crate", "build"),
] {
let task_id = TaskId::new(package, task);
engine.get_index(&task_id);
engine.add_definition(task_id, TaskDefinition::default());
}
let engine = engine.seal();

let graph = PackageGraph::builder(root, PackageJson::default())
.with_package_discovery(DummyDiscovery(
turbopath::AbsoluteSystemPathBuf::try_from(tmp.path()).unwrap(),
))
.with_toolchain(turborepo_repository::cargo::CargoToolchain::new(
root.to_owned(),
))
.build()
.await
.unwrap();

let mut tasks = engine.tasks_with_command(&graph);
tasks.sort();
// "c#build" is absent: no script defines it. Both Cargo tasks are
// present without any package.json involvement.
assert_eq!(tasks, vec!["a#build", "cargo#test", "my-crate#build"]);
}

#[tokio::test]
async fn issue_4291() {
// we had an issue where our engine validation would reject running persistent
Expand Down
21 changes: 19 additions & 2 deletions crates/turborepo-lib/src/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ pub struct Run {
shutdown_started_emitted: Arc<AtomicBool>,
}

type UIResult<T> = Result<Option<(T, JoinHandle<Result<(), turborepo_ui::Error>>)>, Error>;
// The join handle covers the render thread plus its sink-restoring
// watchdog; render errors are logged there, not surfaced to the caller.
type UIResult<T> = Result<Option<(T, JoinHandle<()>)>, Error>;

type TuiResult = UIResult<TuiSender>;

Expand Down Expand Up @@ -602,9 +604,24 @@ impl Run {
repo_root,
scrollback_len,
Some(interrupt),
terminal_sink,
terminal_sink.clone(),
)?;

// The terminal sink is disabled while the TUI owns the screen.
// Whatever ends the render thread — normal shutdown, a render
// error, a panic — output must return to the stream sink
// immediately: a mid-run TUI death would otherwise leave the rest
// of the run executing in silence, with every task's output
// dropped. Task output must always have a live sink.
let handle = tokio::spawn(async move {
match handle.await {
Ok(Err(e)) => tracing::error!("error encountered rendering tui: {e}"),
Err(e) => tracing::error!("render thread panicked: {e}"),
Ok(Ok(())) => {}
}
terminal_sink.enable();
});

Ok(Some((sender, handle)))
}

Expand Down
9 changes: 3 additions & 6 deletions crates/turborepo-lib/src/run/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ pub struct WatchClient {
telemetry: CommandEventBuilder,
handler: SignalHandler,
ui_sender: Option<UISender>,
ui_handle: Option<JoinHandle<Result<(), turborepo_ui::Error>>>,
ui_handle: Option<JoinHandle<()>>,
experimental_write_cache: bool,
query_server: Option<Arc<dyn turborepo_query_api::QueryServer>>,
}
Expand Down Expand Up @@ -661,12 +661,9 @@ impl WatchClient {
if let Some(sender) = &self.ui_sender {
sender.stop().await;
}
// Render errors are logged by the watchdog inside `start_ui`.
if let Some(handle) = self.ui_handle.take() {
match handle.await {
Ok(Err(err)) => tracing::error!("error encountered rendering tui: {err}"),
Err(err) => tracing::error!("render thread panicked: {err}"),
Ok(Ok(())) => {}
}
handle.await.ok();
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-run-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ impl TaskCache {
result: turborepo_ui::tui::event::CacheResult,
) {
if let Some(sender) = tui_sender {
sender.status(message, result);
sender.status(message, result, self.task_output_logs.into());
}
if !message.is_empty() {
let line = format!("{message}\n");
Expand Down
8 changes: 6 additions & 2 deletions crates/turborepo-task-executor/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,6 @@ where
task_handle: &mut turborepo_log::grouping::TaskHandle,
telemetry: &PackageTaskEventBuilder,
) -> Result<ExecOutcome, InternalError> {
task_output.start(self.task_cache.output_logs().into());

if !self.task_cache.is_caching_disabled() {
let missing_platform_env = self.platform_env.validate(&self.execution_env);
if !missing_platform_env.is_empty() {
Expand Down Expand Up @@ -430,6 +428,12 @@ where
None => None,
};

// The task is only presented as running once its process is about
// to exist. Everything before this point — cache restore, waiting
// on the serial group — happens while the task is still pending,
// and cache hits finish without ever starting.
task_output.start(self.task_cache.output_logs().into());

// Spawn the process
let cmd = self.cmd.clone();
let mut process =
Expand Down
15 changes: 11 additions & 4 deletions crates/turborepo-ui/src/sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ impl UISender {
}
}

pub fn status(&self, task: String, status: String, result: CacheResult) {
pub fn status(
&self,
task: String,
status: String,
result: CacheResult,
output_logs: OutputLogs,
) {
match self {
UISender::Tui(sender) => sender.status(task, status, result),
UISender::Tui(sender) => sender.status(task, status, result, output_logs),
}
}
fn set_stdin(&self, task: String, stdin: Box<dyn std::io::Write + Send>) {
Expand Down Expand Up @@ -115,12 +121,13 @@ impl TaskSender {
self.handle.set_stdin(self.name.clone(), stdin);
}

pub fn status(&self, status: &str, result: CacheResult) {
pub fn status(&self, status: &str, result: CacheResult, output_logs: OutputLogs) {
// Since this will be rendered via ratatui we any ANSI escape codes will not be
// handled.
// TODO: prevent the status from having ANSI codes in this scenario
let status = console::strip_ansi_codes(status).into_owned();
self.handle.status(self.name.clone(), status, result);
self.handle
.status(self.name.clone(), status, result, output_logs);
}
}

Expand Down
Loading
Loading