diff --git a/Cargo.lock b/Cargo.lock index 5cbd0905..df78fba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,7 +480,7 @@ dependencies = [ [[package]] name = "config" -version = "0.3.34" +version = "0.3.35" dependencies = [ "chrono", "clap", @@ -586,7 +586,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto" -version = "0.3.34" +version = "0.3.35" dependencies = [ "aes-gcm", "base64", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "testutils" -version = "0.3.34" +version = "0.3.35" dependencies = [ "pem", "rsa", @@ -3484,7 +3484,7 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" -version = "0.3.34" +version = "0.3.35" dependencies = [ "tokio", "tower-api", @@ -3509,7 +3509,7 @@ dependencies = [ [[package]] name = "tower-api" -version = "0.3.34" +version = "0.3.35" dependencies = [ "reqwest", "serde", @@ -3521,7 +3521,7 @@ dependencies = [ [[package]] name = "tower-cmd" -version = "0.3.34" +version = "0.3.35" dependencies = [ "axum", "bytes", @@ -3591,7 +3591,7 @@ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-package" -version = "0.3.34" +version = "0.3.35" dependencies = [ "async-compression", "config", @@ -3610,7 +3610,7 @@ dependencies = [ [[package]] name = "tower-runtime" -version = "0.3.34" +version = "0.3.35" dependencies = [ "chrono", "config", @@ -3631,7 +3631,7 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower-telemetry" -version = "0.3.34" +version = "0.3.35" dependencies = [ "tracing", "tracing-appender", @@ -3640,7 +3640,7 @@ dependencies = [ [[package]] name = "tower-uv" -version = "0.3.34" +version = "0.3.35" dependencies = [ "async-compression", "async_zip", @@ -3654,7 +3654,7 @@ dependencies = [ [[package]] name = "tower-version" -version = "0.3.34" +version = "0.3.35" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index d9fcbd5b..4e81a450 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,18 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.3.34" - - - - - - - - - - - +version = "0.3.35" description = "Tower is the best way to host Python data apps in production" rust-version = "1.81" authors = ["Brad Heller "] diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index df0a3f97..a993a7a3 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -166,15 +166,24 @@ where // Monitor app output and status concurrently let app = launcher.app.unwrap(); - let status_task = tokio::spawn(monitor_status(app)); + let status_task = tokio::spawn(monitor_local_status(app)); // Wait for both tasks to complete let final_result = output_task.await.unwrap(); - let status_result = status_task.await; + let status_result = status_task.await.unwrap(); // And if we crashed, err out - if let Ok(Status::Crashed { .. }) = status_result { - return Err(Error::AppCrashed); + match status_result { + Status::Exited => output::success("Your local run exited cleanly."), + Status::Crashed { .. } => { + output::error("Your local run crashed!"); + return Err(Error::AppCrashed); + } + _ => { + debug!("Unexpected status after monitoring: {:?}", status_result); + output::error("An unexpected error occurred while monitoring your local run status!"); + return Err(Error::AppCrashed); + } } Ok(final_result) @@ -536,14 +545,16 @@ async fn monitor_output(mut output: OutputReceiver) { } } -/// monitor_status is a helper function that will monitor the status of a given app and waits for +/// monitor_local_status is a helper function that will monitor the status of a given app and waits for /// it to progress to a terminal state. -async fn monitor_status(app: LocalApp) -> Status { +async fn monitor_local_status(app: LocalApp) -> Status { debug!("Starting status monitoring for LocalApp"); let mut check_count = 0; - let max_checks = 600; // 60 seconds with 100ms intervals + let mut err_count = 0; loop { + check_count += 1; + debug!( "Status check #{}, attempting to get app status", check_count @@ -551,44 +562,40 @@ async fn monitor_status(app: LocalApp) -> Status { match app.status().await { Ok(status) => { - debug!("Got app status (some status)"); + // We reset the error count to indicate that we can intermittently get statuses. + err_count = 0; + match status { tower_runtime::Status::Exited => { - debug!("App exited cleanly, stopping status monitoring"); - output::success("Your app exited cleanly."); + debug!("Run exited cleanly, stopping status monitoring"); + + // We're done. Exit this loop and function. return status; } tower_runtime::Status::Crashed { .. } => { - debug!("App crashed, stopping status monitoring"); - output::error("Your app crashed!"); + debug!("Run crashed, stopping status monitoring"); + + // We're done. Exit this loop and function. return status; } _ => { debug!("App status: other, continuing to monitor"); - check_count += 1; - if check_count >= max_checks { - debug!("Status monitoring timed out after {} checks", max_checks); - output::error( - "App status monitoring timed out, but app may still be running", - ); - return tower_runtime::Status::Running; // Return a default status for timeout - } sleep(Duration::from_millis(100)).await; - continue; } } } Err(e) => { debug!("Failed to get app status: {:?}", e); - check_count += 1; - if check_count >= max_checks { - debug!( - "Failed to get app status after {} attempts, giving up", - max_checks - ); - output::error("Failed to get app status after timeout"); - return tower_runtime::Status::Running; // Return a default status for timeout + err_count += 1; + + // If we get five errors in a row, we abandon monitoring. + if err_count >= 5{ + debug!("Failed to get app status after 5 attempts, giving up"); + output::error("An error occured while monitoring your local run status!"); + return tower_runtime::Status::Crashed { code: -1 } } + + // Otherwise, keep on keepin' on. sleep(Duration::from_millis(100)).await; } } diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index c5b8b35b..7cf8a86a 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -35,7 +35,7 @@ pub struct Output { pub line: String, } -#[derive(Copy, Clone, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Status { None, Running, diff --git a/crates/tower-uv/src/lib.rs b/crates/tower-uv/src/lib.rs index 564c1435..beb668b5 100644 --- a/crates/tower-uv/src/lib.rs +++ b/crates/tower-uv/src/lib.rs @@ -39,11 +39,22 @@ impl From for Error { } fn normalize_env_vars(env_vars: &HashMap) -> HashMap { + // we copy this locally so we can mutate the results. + let mut env_vars = env_vars.clone(); + + // Copy the PATH across so that the child process can find tools/binaries already installed + // on the host system. + let path = std::env::var("PATH").unwrap_or_default(); + env_vars.insert("PATH".to_string(), path); + + // This special hack helps us support Sling. The user needs to specify exactly where the Sling + // binary is installed using the SLING_BINARY environment variable such that we don't have to + // download the Sling binary every single time. + let sling_binary = std::env::var("SLING_BINARY").unwrap_or_default(); + env_vars.insert("SLING_BINARY".to_string(), sling_binary); + #[cfg(windows)] { - // we copy this locally so we can mutate the results. - let mut env_vars = env_vars.clone(); - // If we are running on Windows, we need to retain the SYSTEMROOT env var because Python // needs it to initialize it's random number generator. Fun fact! let systemroot = std::env::var("SYSTEMROOT").unwrap_or_default(); @@ -57,17 +68,10 @@ fn normalize_env_vars(env_vars: &HashMap) -> HashMap Result<(), Error> { diff --git a/pyproject.toml b/pyproject.toml index 3bfc4346..f24d527c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,18 +4,7 @@ build-backend = "maturin" [project] name = "tower" -version = "0.3.34" - - - - - - - - - - - +version = "0.3.35" description = "Tower CLI and runtime environment for Tower." authors = [{ name = "Tower Computing Inc.", email = "brad@tower.dev" }] readme = "README.md" diff --git a/scripts/semver.py b/scripts/semver.py index ce5e9b4e..b48415b5 100755 --- a/scripts/semver.py +++ b/scripts/semver.py @@ -166,14 +166,14 @@ def replace_line_with_regex(file_path, pattern, replace_text): def update_cargo_file(version): - pattern = re.compile(r'^\s*version\s*=\s*".*"$', re.MULTILINE) + pattern = re.compile(r'^\s*version\s*=\s*".*"\n*', re.MULTILINE) replace_line_with_regex( "Cargo.toml", pattern, f'version = "{version.to_tag_string()}"' ) def update_pyproject_file(version): - pattern = re.compile(r'^\s*version\s*=\s*".*"$', re.MULTILINE) + pattern = re.compile(r'^\s*version\s*=\s*".*"\n*', re.MULTILINE) replace_line_with_regex( "pyproject.toml", pattern, f'version = "{version.to_python_string()}"' ) diff --git a/tests/integration/features/cli_runs.feature b/tests/integration/features/cli_runs.feature index 8e234ea0..5afa3e40 100644 --- a/tests/integration/features/cli_runs.feature +++ b/tests/integration/features/cli_runs.feature @@ -13,7 +13,7 @@ Feature: CLI Run Commands When I run "tower run --local" via CLI Then timestamps should be green colored And each log line should be on a separate line - And the final status should show "Your app crashed!" in red + And the final status should show "Your local run crashed!" in red Scenario: CLI remote run should show detailed validation errors Given I have a valid Towerfile in the current directory diff --git a/tests/integration/features/steps/cli_steps.py b/tests/integration/features/steps/cli_steps.py index 054fc8c7..9ad612f6 100644 --- a/tests/integration/features/steps/cli_steps.py +++ b/tests/integration/features/steps/cli_steps.py @@ -107,16 +107,16 @@ def step_final_crash_status_should_show_error(context): assert "Error:" in output, f"Expected 'Error:' in crash message, got: {output}" -@step('the final status should show "Your app crashed!" in red') +@step('the final status should show "Your local run crashed!" in red') def step_final_status_should_show_crashed_in_red(context): - """Verify local run shows 'Your app crashed!' in red""" + """Verify local run shows 'Your local run crashed!' in red""" output = context.cli_output assert ( red_color_code in output ), f"Expected red color codes in output, got: {output}" assert ( - "Your app crashed!" in output - ), f"Expected 'Your app crashed!' message, got: {output}" + "Your local run crashed!" in output + ), f"Expected 'Your local run crashed!' message, got: {output}" @step('the output should show "{expected_text}"') diff --git a/uv.lock b/uv.lock index c87705e5..e2f07d96 100644 --- a/uv.lock +++ b/uv.lock @@ -2744,7 +2744,7 @@ wheels = [ [[package]] name = "tower" -version = "0.3.34" +version = "0.3.35" source = { editable = "." } dependencies = [ { name = "attrs" },