Skip to content

Commit a7ee93e

Browse files
Kobzolclaude
andcommitted
Add 'hq task workdir' command for displaying task working directories
Implements the task workdir command requested in issue #644. The command displays working directories for selected tasks within a specific job. Features: - CLI output showing task-specific working directories - JSON output with task_id -> workdir mapping - Quiet output mode support - Supports task ID arrays and ranges - Job selector supports "last" and specific job IDs - Resolves task-specific working directory paths - Integrates with existing task selection mechanisms Usage: - hq task workdir <job_selector> <task_selector> - hq task workdir 1 2 - hq task workdir last 1-5 - hq task workdir 2 1,3,5 The implementation reuses the existing task path resolution system to provide accurate working directory information for individual tasks, complementing the job-level workdir command. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7503ba3 commit a7ee93e

File tree

12 files changed

+898
-5
lines changed

12 files changed

+898
-5
lines changed

crates/hyperqueue/src/bin/hq.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use hyperqueue::client::commands::data::command_task_data;
88
use hyperqueue::client::commands::doc::command_doc;
99
use hyperqueue::client::commands::job::{
1010
JobCancelOpts, JobCatOpts, JobCloseOpts, JobForgetOpts, JobInfoOpts, JobListOpts,
11-
JobTaskIdsOpts, cancel_job, close_job, forget_job, output_job_cat, output_job_detail,
12-
output_job_list, output_job_summary,
11+
JobTaskIdsOpts, JobWorkdirOpts, cancel_job, close_job, forget_job, output_job_cat,
12+
output_job_detail, output_job_list, output_job_summary, output_job_workdir,
1313
};
1414
use hyperqueue::client::commands::journal::command_journal;
1515
use hyperqueue::client::commands::outputlog::command_reader;
@@ -31,8 +31,9 @@ use hyperqueue::client::output::outputs::{Output, Outputs};
3131
use hyperqueue::client::output::quiet::Quiet;
3232
use hyperqueue::client::status::Status;
3333
use hyperqueue::client::task::{
34-
TaskCommand, TaskExplainOpts, TaskInfoOpts, TaskListOpts, output_job_task_explain,
35-
output_job_task_ids, output_job_task_info, output_job_task_list,
34+
TaskCommand, TaskExplainOpts, TaskInfoOpts, TaskListOpts, TaskWorkdirOpts,
35+
output_job_task_explain, output_job_task_ids, output_job_task_info, output_job_task_list,
36+
output_job_task_workdir,
3637
};
3738
use hyperqueue::common::cli::{
3839
ColorPolicy, CommonOpts, DeploySshOpts, GenerateCompletionOpts, HwDetectOpts, JobCommand,
@@ -140,6 +141,14 @@ async fn command_job_close(gsettings: &GlobalSettings, opts: JobCloseOpts) -> an
140141
close_job(gsettings, &mut connection, opts.selector).await
141142
}
142143

144+
async fn command_job_workdir(
145+
gsettings: &GlobalSettings,
146+
opts: JobWorkdirOpts,
147+
) -> anyhow::Result<()> {
148+
let mut connection = get_client_session(gsettings.server_directory()).await?;
149+
output_job_workdir(gsettings, &mut connection, opts.selector).await
150+
}
151+
143152
async fn command_job_delete(gsettings: &GlobalSettings, opts: JobForgetOpts) -> anyhow::Result<()> {
144153
let mut connection = get_client_session(gsettings.server_directory()).await?;
145154
forget_job(gsettings, &mut connection, opts).await
@@ -212,6 +221,14 @@ async fn command_task_explain(
212221
output_job_task_explain(gsettings, &mut session, opts).await
213222
}
214223

224+
async fn command_task_workdir(
225+
gsettings: &GlobalSettings,
226+
opts: TaskWorkdirOpts,
227+
) -> anyhow::Result<()> {
228+
let mut session = get_client_session(gsettings.server_directory()).await?;
229+
output_job_task_workdir(gsettings, &mut session, opts).await
230+
}
231+
215232
async fn command_worker_start(
216233
gsettings: &GlobalSettings,
217234
opts: WorkerStartOpts,
@@ -496,6 +513,7 @@ async fn main() -> hyperqueue::Result<()> {
496513
JobCommand::TaskIds(opts) => command_job_task_ids(&gsettings, opts).await,
497514
JobCommand::Open(opts) => command_job_open(&gsettings, opts).await,
498515
JobCommand::Close(opts) => command_job_close(&gsettings, opts).await,
516+
JobCommand::Workdir(opts) => command_job_workdir(&gsettings, opts).await,
499517
},
500518
SubCommand::Submit(opts) => {
501519
command_job_submit(&gsettings, OptsWithMatches::new(opts, matches)).await
@@ -504,6 +522,7 @@ async fn main() -> hyperqueue::Result<()> {
504522
TaskCommand::List(opts) => command_task_list(&gsettings, opts).await,
505523
TaskCommand::Info(opts) => command_task_info(&gsettings, opts).await,
506524
TaskCommand::Explain(opts) => command_task_explain(&gsettings, opts).await,
525+
TaskCommand::Workdir(opts) => command_task_workdir(&gsettings, opts).await,
507526
},
508527
SubCommand::Data(opts) => command_task_data(&gsettings, opts).await,
509528
#[cfg(feature = "dashboard")]

crates/hyperqueue/src/client/commands/job.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ pub struct JobCatOpts {
113113
pub stream: OutputStream,
114114
}
115115

116+
#[derive(Parser)]
117+
pub struct JobWorkdirOpts {
118+
/// Single ID, ID range or `last` to display the most recently submitted job
119+
#[arg(value_parser = parse_last_all_range)]
120+
pub selector: IdSelector,
121+
}
122+
116123
pub async fn output_job_list(
117124
gsettings: &GlobalSettings,
118125
session: &mut ClientSession,
@@ -340,3 +347,37 @@ pub async fn forget_job(
340347

341348
Ok(())
342349
}
350+
351+
pub async fn output_job_workdir(
352+
gsettings: &GlobalSettings,
353+
session: &mut ClientSession,
354+
selector: IdSelector,
355+
) -> anyhow::Result<()> {
356+
let message = FromClientMessage::JobDetail(JobDetailRequest {
357+
job_id_selector: selector,
358+
task_selector: Some(TaskSelector {
359+
id_selector: TaskIdSelector::All,
360+
status_selector: TaskStatusSelector::All,
361+
}),
362+
});
363+
let response =
364+
rpc_call!(session.connection(), message, ToClientMessage::JobDetailResponse(r) => r)
365+
.await?;
366+
367+
let jobs: Vec<JobDetail> = response
368+
.details
369+
.into_iter()
370+
.filter_map(|(id, job)| match job {
371+
Some(job) => Some(job),
372+
None => {
373+
log::error!("Job {id} not found");
374+
None
375+
}
376+
})
377+
.collect();
378+
379+
gsettings
380+
.printer()
381+
.print_job_workdir(jobs, &response.server_uid);
382+
Ok(())
383+
}

crates/hyperqueue/src/client/output/cli.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,19 @@ impl Output for CliOutput {
472472
}
473473
}
474474

475+
fn print_task_workdir(&self, jobs: Vec<(JobId, JobDetail)>, server_uid: &str) {
476+
for (job_id, job) in jobs {
477+
let task_paths = resolve_task_paths(&job, server_uid);
478+
479+
println!("Job {}:", job_id);
480+
for (task_id, resolved_paths) in task_paths.iter() {
481+
if let Some(paths) = resolved_paths {
482+
println!(" Task {}: {}", task_id.as_num(), paths.cwd.display());
483+
}
484+
}
485+
}
486+
}
487+
475488
fn print_job_list(&self, jobs: Vec<JobInfo>, total_jobs: usize) {
476489
let job_count = jobs.len();
477490
let mut has_opened = false;
@@ -632,6 +645,40 @@ impl Output for CliOutput {
632645
}
633646
}
634647

648+
fn print_job_workdir(&self, jobs: Vec<JobDetail>, server_uid: &str) {
649+
for job in jobs {
650+
let task_paths = resolve_task_paths(&job, server_uid);
651+
652+
// Collect unique working directories
653+
let mut workdirs: std::collections::BTreeSet<String> =
654+
std::collections::BTreeSet::new();
655+
656+
// Add submission directory(s)
657+
for submit_desc in &job.submit_descs {
658+
workdirs.insert(
659+
submit_desc
660+
.description()
661+
.submit_dir
662+
.to_string_lossy()
663+
.to_string(),
664+
);
665+
}
666+
667+
// Add task working directories
668+
for (_, resolved_paths) in task_paths.iter() {
669+
if let Some(paths) = resolved_paths {
670+
workdirs.insert(paths.cwd.to_string_lossy().to_string());
671+
}
672+
}
673+
674+
// Print job header and working directories
675+
println!("Job {}:", job.info.id);
676+
for workdir in workdirs {
677+
println!(" {}", workdir);
678+
}
679+
}
680+
}
681+
635682
fn print_job_wait(
636683
&self,
637684
duration: Duration,

crates/hyperqueue/src/client/output/json.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,43 @@ impl Output for JsonOutput {
154154
self.print(Value::Array(job_details));
155155
}
156156

157+
fn print_job_workdir(&self, jobs: Vec<JobDetail>, server_uid: &str) {
158+
let job_workdirs: Vec<_> = jobs
159+
.into_iter()
160+
.map(|job| {
161+
let task_paths = resolve_task_paths(&job, server_uid);
162+
163+
// Collect unique working directories
164+
let mut workdirs: std::collections::BTreeSet<String> =
165+
std::collections::BTreeSet::new();
166+
167+
// Add submission directory(s)
168+
for submit_desc in &job.submit_descs {
169+
workdirs.insert(
170+
submit_desc
171+
.description()
172+
.submit_dir
173+
.to_string_lossy()
174+
.to_string(),
175+
);
176+
}
177+
178+
// Add task working directories
179+
for (_, resolved_paths) in task_paths.iter() {
180+
if let Some(paths) = resolved_paths {
181+
workdirs.insert(paths.cwd.to_string_lossy().to_string());
182+
}
183+
}
184+
185+
json!({
186+
"job_id": job.info.id,
187+
"workdirs": workdirs.into_iter().collect::<Vec<_>>()
188+
})
189+
})
190+
.collect();
191+
self.print(Value::Array(job_workdirs));
192+
}
193+
157194
fn print_job_wait(
158195
&self,
159196
duration: Duration,
@@ -221,6 +258,29 @@ impl Output for JsonOutput {
221258
self.print(json!(map));
222259
}
223260

261+
fn print_task_workdir(&self, jobs: Vec<(JobId, JobDetail)>, server_uid: &str) {
262+
let task_workdirs: Vec<_> = jobs
263+
.into_iter()
264+
.map(|(job_id, job)| {
265+
let task_paths = resolve_task_paths(&job, server_uid);
266+
let tasks: HashMap<u32, String> = task_paths
267+
.iter()
268+
.filter_map(|(task_id, resolved_paths)| {
269+
resolved_paths.as_ref().map(|paths| {
270+
(task_id.as_num(), paths.cwd.to_string_lossy().to_string())
271+
})
272+
})
273+
.collect();
274+
275+
json!({
276+
"job_id": job_id,
277+
"tasks": tasks
278+
})
279+
})
280+
.collect();
281+
self.print(Value::Array(task_workdirs));
282+
}
283+
224284
fn print_summary(&self, filename: &Path, summary: Summary) {
225285
let json = json!({
226286
"filename": filename,

crates/hyperqueue/src/client/output/outputs.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub trait Output {
4848
fn print_job_list(&self, jobs: Vec<JobInfo>, total_jobs: usize);
4949
fn print_job_summary(&self, jobs: Vec<JobInfo>);
5050
fn print_job_detail(&self, jobs: Vec<JobDetail>, worker_map: WorkerMap, server_uid: &str);
51+
fn print_job_workdir(&self, jobs: Vec<JobDetail>, server_uid: &str);
5152
fn print_job_wait(
5253
&self,
5354
duration: Duration,
@@ -80,6 +81,7 @@ pub trait Output {
8081
verbosity: Verbosity,
8182
);
8283
fn print_task_ids(&self, jobs_task_id: Vec<(JobId, IntArray)>);
84+
fn print_task_workdir(&self, jobs: Vec<(JobId, JobDetail)>, server_uid: &str);
8385

8486
// Stream
8587
fn print_summary(&self, path: &Path, summary: Summary);

crates/hyperqueue/src/client/output/quiet.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ impl Output for Quiet {
9999
}
100100
fn print_job_detail(&self, _jobs: Vec<JobDetail>, _worker_map: WorkerMap, _server_uid: &str) {}
101101

102+
fn print_job_workdir(&self, _jobs: Vec<JobDetail>, _server_uid: &str) {}
103+
102104
fn print_job_wait(
103105
&self,
104106
_duration: Duration,
@@ -139,6 +141,8 @@ impl Output for Quiet {
139141

140142
fn print_task_ids(&self, _job_task_ids: Vec<(JobId, IntArray)>) {}
141143

144+
fn print_task_workdir(&self, _jobs: Vec<(JobId, JobDetail)>, _server_uid: &str) {}
145+
142146
// Stream
143147
fn print_summary(&self, _filename: &Path, _summary: Summary) {}
144148

crates/hyperqueue/src/client/task.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub enum TaskCommand {
2727
Info(TaskInfoOpts),
2828
/// Explain if task can run on a selected worker
2929
Explain(TaskExplainOpts),
30+
/// Display working directory of selected task(s)
31+
Workdir(TaskWorkdirOpts),
3032
}
3133

3234
#[derive(clap::Parser)]
@@ -65,6 +67,16 @@ pub struct TaskExplainOpts {
6567
pub task_id: JobTaskId,
6668
}
6769

70+
#[derive(clap::Parser)]
71+
pub struct TaskWorkdirOpts {
72+
/// Select specific job
73+
#[arg(value_parser = parse_last_single_id)]
74+
pub job_selector: SingleIdSelector,
75+
76+
/// Select specific task(s)
77+
pub task_selector: IntArray,
78+
}
79+
6880
pub async fn output_job_task_list(
6981
gsettings: &GlobalSettings,
7082
session: &mut ClientSession,
@@ -200,3 +212,44 @@ pub async fn output_job_task_explain(
200212
.print_explanation(response.task_id, &response.explanation);
201213
Ok(())
202214
}
215+
216+
pub async fn output_job_task_workdir(
217+
gsettings: &GlobalSettings,
218+
session: &mut ClientSession,
219+
opts: TaskWorkdirOpts,
220+
) -> anyhow::Result<()> {
221+
let task_selector = TaskSelector {
222+
id_selector: TaskIdSelector::Specific(opts.task_selector),
223+
status_selector: TaskStatusSelector::All,
224+
};
225+
226+
let job_id_selector = match opts.job_selector {
227+
SingleIdSelector::Specific(id) => IdSelector::Specific(IntArray::from_id(id)),
228+
SingleIdSelector::Last => IdSelector::LastN(1),
229+
};
230+
231+
let message = FromClientMessage::JobDetail(JobDetailRequest {
232+
job_id_selector,
233+
task_selector: Some(task_selector),
234+
});
235+
let response =
236+
rpc_call!(session.connection(), message, ToClientMessage::JobDetailResponse(r) => r)
237+
.await?;
238+
239+
let jobs = response
240+
.details
241+
.into_iter()
242+
.filter_map(|(job_id, opt_job)| match opt_job {
243+
Some(job) => Some((job_id, job)),
244+
None => {
245+
log::warn!("Job {job_id} not found");
246+
None
247+
}
248+
})
249+
.collect();
250+
251+
gsettings
252+
.printer()
253+
.print_task_workdir(jobs, &response.server_uid);
254+
Ok(())
255+
}

crates/hyperqueue/src/common/cli.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::client::commands::data::DataOpts;
1111
use crate::client::commands::doc::DocOpts;
1212
use crate::client::commands::job::{
1313
JobCancelOpts, JobCatOpts, JobCloseOpts, JobForgetOpts, JobInfoOpts, JobListOpts,
14-
JobTaskIdsOpts,
14+
JobTaskIdsOpts, JobWorkdirOpts,
1515
};
1616
use crate::client::commands::journal::JournalOpts;
1717
use crate::client::commands::outputlog::OutputLogOpts;
@@ -374,6 +374,8 @@ pub enum JobCommand {
374374
Open(SubmitJobConfOpts),
375375
/// Close an open job
376376
Close(JobCloseOpts),
377+
/// Display working directory of selected job(s)
378+
Workdir(JobWorkdirOpts),
377379
}
378380

379381
#[derive(Parser)]

0 commit comments

Comments
 (0)