Skip to content

Commit 02c089d

Browse files
authored
Merge pull request #26 from Ryan-Rong-24/feature/no-tui-flag
Added --no-tui flag and support for stdout logs
2 parents 5930e59 + 35f42ff commit 02c089d

File tree

3 files changed

+208
-19
lines changed

3 files changed

+208
-19
lines changed

src/cmd/mod.rs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ pub struct Cli {
5959
// Optional: Specify output file
6060
#[arg(short, long)]
6161
pub output: Option<String>,
62+
63+
/// Skip the TUI and print results directly to stdout
64+
#[arg(long)]
65+
pub no_tui: bool,
6266
}
6367

6468
#[derive(Subcommand, Debug)]
@@ -96,6 +100,10 @@ enum Commands {
96100
// Optional: Specify output file
97101
#[arg(short, long)]
98102
output: Option<String>,
103+
104+
/// Skip the TUI and print results directly to stdout
105+
#[arg(long)]
106+
no_tui: bool,
99107
},
100108
}
101109

@@ -121,6 +129,7 @@ pub async fn execute(cli: Cli) -> Result<()> {
121129
leaderboard,
122130
mode,
123131
output,
132+
no_tui,
124133
}) => {
125134
let config = load_config()?;
126135
let cli_id = config.cli_id.ok_or_else(|| {
@@ -133,15 +142,28 @@ pub async fn execute(cli: Cli) -> Result<()> {
133142

134143
// Use filepath from Submit command first, fallback to top-level filepath
135144
let final_filepath = filepath.or(cli.filepath);
136-
submit::run_submit_tui(
137-
final_filepath, // Resolved filepath
138-
gpu, // From Submit command
139-
leaderboard, // From Submit command
140-
mode, // From Submit command
141-
cli_id,
142-
output, // From Submit command
143-
)
144-
.await
145+
146+
if no_tui {
147+
submit::run_submit_plain(
148+
final_filepath, // Resolved filepath
149+
gpu, // From Submit command
150+
leaderboard, // From Submit command
151+
mode, // From Submit command
152+
cli_id,
153+
output, // From Submit command
154+
)
155+
.await
156+
} else {
157+
submit::run_submit_tui(
158+
final_filepath, // Resolved filepath
159+
gpu, // From Submit command
160+
leaderboard, // From Submit command
161+
mode, // From Submit command
162+
cli_id,
163+
output, // From Submit command
164+
)
165+
.await
166+
}
145167
}
146168
None => {
147169
// Check if any of the submission-related flags were used at the top level

src/cmd/submit.rs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,16 @@ impl App {
301301
file.read_to_string(&mut file_content)?;
302302

303303
self.submission_task = Some(tokio::spawn(async move {
304-
service::submit_solution(&client, &filepath, &file_content, &leaderboard, &gpu, &mode)
305-
.await
304+
service::submit_solution(
305+
&client,
306+
&filepath,
307+
&file_content,
308+
&leaderboard,
309+
&gpu,
310+
&mode,
311+
None,
312+
)
313+
.await
306314
}));
307315
Ok(())
308316
}
@@ -672,3 +680,110 @@ pub async fn run_submit_tui(
672680

673681
Ok(())
674682
}
683+
684+
pub async fn run_submit_plain(
685+
filepath: Option<String>,
686+
gpu: Option<String>,
687+
leaderboard: Option<String>,
688+
mode: Option<String>,
689+
cli_id: String,
690+
output: Option<String>,
691+
) -> Result<()> {
692+
let file_to_submit = match filepath {
693+
Some(fp) => fp,
694+
None => {
695+
return Err(anyhow!("File path is required when using --no-tui"));
696+
}
697+
};
698+
699+
if !Path::new(&file_to_submit).exists() {
700+
return Err(anyhow!("File not found: {}", file_to_submit));
701+
}
702+
703+
let (directives, has_multiple_gpus) = utils::get_popcorn_directives(&file_to_submit)?;
704+
705+
if has_multiple_gpus {
706+
return Err(anyhow!(
707+
"Multiple GPUs are not supported yet. Please specify only one GPU."
708+
));
709+
}
710+
711+
// Determine final values
712+
let final_gpu = gpu
713+
.or_else(|| {
714+
if !directives.gpus.is_empty() {
715+
Some(directives.gpus[0].clone())
716+
} else {
717+
None
718+
}
719+
})
720+
.ok_or_else(|| anyhow!("GPU not specified. Use --gpu flag or add GPU directive to file"))?;
721+
722+
let final_leaderboard = leaderboard
723+
.or_else(|| {
724+
if !directives.leaderboard_name.is_empty() {
725+
Some(directives.leaderboard_name.clone())
726+
} else {
727+
None
728+
}
729+
})
730+
.ok_or_else(|| {
731+
anyhow!("Leaderboard not specified. Use --leaderboard flag or add leaderboard directive to file")
732+
})?;
733+
734+
let final_mode = mode.ok_or_else(|| {
735+
anyhow!("Submission mode not specified. Use --mode flag (test, benchmark, leaderboard, profile)")
736+
})?;
737+
738+
// Read file content
739+
let mut file = File::open(&file_to_submit)?;
740+
let mut file_content = String::new();
741+
file.read_to_string(&mut file_content)?;
742+
743+
eprintln!("Submitting to leaderboard: {}", final_leaderboard);
744+
eprintln!("GPU: {}", final_gpu);
745+
eprintln!("Mode: {}", final_mode);
746+
eprintln!("File: {}", file_to_submit);
747+
eprintln!("\nWaiting for results...");
748+
749+
// Create client and submit
750+
let client = service::create_client(Some(cli_id))?;
751+
let result = service::submit_solution(
752+
&client,
753+
&file_to_submit,
754+
&file_content,
755+
&final_leaderboard,
756+
&final_gpu,
757+
&final_mode,
758+
Some(Box::new(|msg| {
759+
eprintln!("{}", msg);
760+
})),
761+
)
762+
.await?;
763+
764+
// Clean up the result text
765+
let trimmed = result.trim();
766+
let content = if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 {
767+
&trimmed[1..trimmed.len() - 1]
768+
} else {
769+
trimmed
770+
};
771+
772+
let content = content.replace("\\n", "\n");
773+
774+
// Write to file if output is specified
775+
if let Some(output_path) = output {
776+
if let Some(parent) = Path::new(&output_path).parent() {
777+
std::fs::create_dir_all(parent)
778+
.map_err(|e| anyhow!("Failed to create directories for {}: {}", output_path, e))?;
779+
}
780+
std::fs::write(&output_path, &content)
781+
.map_err(|e| anyhow!("Failed to write result to file {}: {}", output_path, e))?;
782+
eprintln!("\nResults written to: {}", output_path);
783+
}
784+
785+
// Print to stdout
786+
println!("\n{}", content);
787+
788+
Ok(())
789+
}

src/service/mod.rs

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ pub async fn submit_solution<P: AsRef<Path>>(
101101
leaderboard: &str,
102102
gpu: &str,
103103
submission_mode: &str,
104+
on_log: Option<Box<dyn Fn(String) + Send + Sync>>,
104105
) -> Result<String> {
105106
let base_url =
106107
env::var("POPCORN_API_URL").map_err(|_| anyhow!("POPCORN_API_URL is not set"))?;
@@ -172,11 +173,62 @@ pub async fn submit_solution<P: AsRef<Path>>(
172173

173174
if let (Some(event), Some(data)) = (event_type, data_json) {
174175
match event {
175-
"status" => (),
176+
"status" => {
177+
if let Some(ref cb) = on_log {
178+
// Try to parse as JSON and extract "message" or just return raw data
179+
if let Ok(val) = serde_json::from_str::<Value>(data) {
180+
if let Some(msg) = val.get("message").and_then(|m| m.as_str()) {
181+
cb(msg.to_string());
182+
} else {
183+
cb(data.to_string());
184+
}
185+
} else {
186+
cb(data.to_string());
187+
}
188+
}
189+
}
176190
"result" => {
177191
let result_val: Value = serde_json::from_str(data)?;
178-
let reports = result_val.get("reports").unwrap();
179-
return Ok(reports.to_string());
192+
193+
if let Some(ref cb) = on_log {
194+
// Handle "results" array
195+
if let Some(results_array) = result_val.get("results").and_then(|v| v.as_array()) {
196+
for (i, result_item) in results_array.iter().enumerate() {
197+
let mode_key = submission_mode.to_lowercase();
198+
199+
if let Some(run_obj) = result_item.get("runs")
200+
.and_then(|r| r.get(&mode_key))
201+
.and_then(|t| t.get("run"))
202+
{
203+
if let Some(stdout) = run_obj.get("stdout").and_then(|s| s.as_str()) {
204+
if !stdout.is_empty() {
205+
cb(format!("STDOUT (Run {}):\n{}", i + 1, stdout));
206+
}
207+
}
208+
// Also check stderr
209+
if let Some(stderr) = run_obj.get("stderr").and_then(|s| s.as_str()) {
210+
if !stderr.is_empty() {
211+
cb(format!("STDERR (Run {}):\n{}", i + 1, stderr));
212+
}
213+
}
214+
}
215+
}
216+
} else {
217+
// Fallback for single object or different structure
218+
if let Some(stdout) = result_val.get("stdout").and_then(|s| s.as_str()) {
219+
if !stdout.is_empty() {
220+
cb(format!("STDOUT:\n{}", stdout));
221+
}
222+
}
223+
}
224+
}
225+
226+
if let Some(reports) = result_val.get("reports") {
227+
return Ok(reports.to_string());
228+
} else {
229+
// If no reports, return the whole result as a string
230+
return Ok(serde_json::to_string_pretty(&result_val)?);
231+
}
180232
}
181233
"error" => {
182234
let error_val: Value = serde_json::from_str(data)?;
@@ -198,11 +250,11 @@ pub async fn submit_solution<P: AsRef<Path>>(
198250
return Err(anyhow!(error_msg));
199251
}
200252
_ => {
201-
stderr
202-
.write_all(
203-
format!("Ignoring unknown SSE event: {}\n", event).as_bytes(),
204-
)
205-
.await?;
253+
let msg = format!("Ignoring unknown SSE event: {}\n", event);
254+
if let Some(ref cb) = on_log {
255+
cb(msg.clone());
256+
}
257+
stderr.write_all(msg.as_bytes()).await?;
206258
stderr.flush().await?;
207259
}
208260
}

0 commit comments

Comments
 (0)