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
22 changes: 11 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 1 addition & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <brad@tower.dev>"]
Expand Down
65 changes: 36 additions & 29 deletions crates/tower-cmd/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -536,59 +545,57 @@ 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
);

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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/tower-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub struct Output {
pub line: String,
}

#[derive(Copy, Clone, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Status {
None,
Running,
Expand Down
28 changes: 16 additions & 12 deletions crates/tower-uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,22 @@ impl From<install::Error> for Error {
}

fn normalize_env_vars(env_vars: &HashMap<String, String>) -> HashMap<String, String> {
// 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();
Expand All @@ -57,17 +68,10 @@ fn normalize_env_vars(env_vars: &HashMap<String, String>) -> HashMap<String, Str
// Apparently, according to some random person on Stack Overflow, sometimes the var can be
// TEMP and sometimes it can be TMP. So uh...let's just grab both just in case.
let tmp = std::env::var("TMP").unwrap_or_default();
env_vars.insert("TMP".to_string(), tmp);

return env_vars;
env_vars.insert("TMP".to_string(), tmp);
}

#[cfg(not(windows))]
{
// On non-Windows platforms, we can just return the env vars as-is. We have to do this
// clone thing to get rid fo the lifetime issues.
return env_vars.clone();
}
env_vars
}

async fn test_uv_path(path: &PathBuf) -> Result<(), Error> {
Expand Down
13 changes: 1 addition & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions scripts/semver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()}"'
)
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/features/cli_runs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/features/steps/cli_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"')
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading