Skip to content

Commit

Permalink
Add feature to open error output file of active job (#172)
Browse files Browse the repository at this point in the history
* add utility function for opening text files in the editor

* refactor to use openTextFile utility

* add show error function
- adds command and button to items in job dashboard
- adds functionality to show job error files
- refactors fetching of runtime metadata from jobs with scontrol
- adds testing for job error files

* correct necessary fields from squeue
- stdout is no longer needed

* c8 coverage update

* add more guards on metadata update

* update testing to include errorfile strings

* change button order and icons
  • Loading branch information
Dando18 authored Dec 4, 2024
1 parent 7115ece commit 04813f4
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 81 deletions.
18 changes: 14 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
"title": "Show Job Output",
"icon": "$(output)"
},
{
"command": "job-dashboard.show-error",
"title": "Show Job Error",
"icon": "$(bracket-error)"
},
{
"command": "job-dashboard.show-source",
"title": "Show Job Source",
Expand Down Expand Up @@ -134,22 +139,27 @@
{
"command": "job-dashboard.cancel",
"when": "view == job-dashboard && viewItem == jobItem",
"group": "inline"
"group": "inline@2"
},
{
"command": "job-dashboard.cancel-and-resubmit",
"when": "view == job-dashboard && viewItem == jobItem",
"group": "inline"
"group": "inline@1"
},
{
"command": "job-dashboard.show-output",
"when": "view == job-dashboard && viewItem == jobItem",
"group": "inline"
"group": "inline@3"
},
{
"command": "job-dashboard.show-error",
"when": "view == job-dashboard && viewItem == jobItem",
"group": "inline@4"
},
{
"command": "job-dashboard.show-source",
"when": "view == job-dashboard && viewItem == jobItem",
"group": "inline"
"group": "inline@5"
},
{
"command": "submit-dashboard.submit",
Expand Down
21 changes: 21 additions & 0 deletions src/fileutilities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import * as vscode from 'vscode';

/**
* Opens a text file in the editor.
*
* @param fpath - The relative path to the file to be opened.
*
* This function resolves the given file path relative to the workspace,
* opens the text document, and then shows it in the editor. If there is
* an error during this process, an error message is displayed.
*/
export function openTextFileInEditor(fpath: string): void {
const absPath = resolvePathRelativeToWorkspace(fpath);
vscode.workspace.openTextDocument(absPath).then(
doc => {
vscode.window.showTextDocument(doc);
},
error => {
vscode.window.showErrorMessage(`Error opening file ${fpath}: ${error}`);
}
);
}

/**
* Resolves the path relative to the workspace.
* If the path is not an absolute path or a file URI, it is resolved relative to the workspace root.
Expand Down
37 changes: 19 additions & 18 deletions src/jobs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { Job, Scheduler, sortJobs } from './scheduler';
import { resolvePathRelativeToWorkspace } from './fileutilities';
import { resolvePathRelativeToWorkspace, openTextFileInEditor } from './fileutilities';

/**
* Represents an information item in the tree view. These are used to display
Expand Down Expand Up @@ -248,10 +248,12 @@ export class JobQueueProvider implements vscode.TreeDataProvider<JobItem | InfoI
this.cancelAndResubmit(jobItem)
);
vscode.commands.registerCommand('job-dashboard.show-output', (jobItem: JobItem) => this.showOutput(jobItem));
vscode.commands.registerCommand('job-dashboard.show-error', (jobItem: JobItem) => this.showError(jobItem));
vscode.commands.registerCommand('job-dashboard.show-source', (jobItem: JobItem) => this.showSource(jobItem));

this.initAutoRefresh();
vscode.workspace.onDidChangeConfiguration(e => {
/* c8 ignore next 3 */
if (e.affectsConfiguration('slurm-dashboard.job-dashboard.refreshInterval')) {
this.initAutoRefresh();
}
Expand Down Expand Up @@ -324,34 +326,33 @@ export class JobQueueProvider implements vscode.TreeDataProvider<JobItem | InfoI
private showOutput(jobItem: JobItem): void {
const fpath = this.scheduler.getJobOutputPath(jobItem.job);
if (fpath) {
vscode.workspace.openTextDocument(resolvePathRelativeToWorkspace(fpath)).then(
doc => {
vscode.window.showTextDocument(doc);
},
error => {
vscode.window.showErrorMessage(`Failed to open output file ${jobItem.job.outputFile}.\n${error}`);
}
);
openTextFileInEditor(fpath);
} else {
vscode.window.showErrorMessage(`Job ${jobItem.job.id} has no associated output file.`);
}
}

/**
* Open the error output file for this job in a VSCode text window.
* @param jobItem
*/
private showError(jobItem: JobItem): void {
const fpath = this.scheduler.getJobErrorPath(jobItem.job);
if (fpath) {
openTextFileInEditor(fpath);
} else {
vscode.window.showErrorMessage(`Job ${jobItem.job.id} has no associated error file.`);
}
}

/**
* Open the JobItem's source batch file in a VSCode text window.
* @param jobItem The job item.
*/
private showSource(jobItem: JobItem): void {
if (jobItem.job.batchFile) {
const fpath = resolvePathRelativeToWorkspace(jobItem.job.batchFile);
vscode.workspace.openTextDocument(fpath).then(
doc => {
vscode.window.showTextDocument(doc);
},
error => {
vscode.window.showErrorMessage(`Failed to open batch file ${jobItem.job.batchFile}.\n${error}`);
}
);
const fpath = resolvePathRelativeToWorkspace(jobItem.job.batchFile).toString();
openTextFileInEditor(fpath);
} else {
vscode.window.showErrorMessage(`Job ${jobItem.job.id} has no associated batch file.`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/jobscripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class JobScriptProvider implements vscode.TreeDataProvider<JobScript> {
const jobScriptExts = vscode.workspace
.getConfiguration('slurm-dashboard')
.get('submit-dashboard.jobScriptExtensions', ['.slurm', '.sbatch', '.job']);
/* c8 ignore next 6 */
/* c8 ignore next 7 */
if (JSON.stringify(jobScriptExts) !== JSON.stringify(['.slurm', '.sbatch', '.job'])) {
vscode.window.showWarningMessage(
'The slurm-dashboard.submit-dashboard.jobScriptExtensions setting has been modified, but ' +
Expand Down
147 changes: 109 additions & 38 deletions src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class Job {
public nodeList?: string,
public batchFile?: string,
public outputFile?: string,
public errorFile?: string,
public maxTime?: WallTime,
public curTime?: WallTime
) {}
Expand Down Expand Up @@ -141,6 +142,14 @@ export interface Scheduler {
* @returns The output path for the job or undefined if the job does not have an output file.
*/
getJobOutputPath(job: Job): string | undefined;

/**
* Retrieve the error path for a job file. Is permitted to simply return
* job.errorFile. Returns undefined if the job does not have an error file.
* @param job The job for which to retrieve the error path.
* @returns The error path for the job or undefined if the job does not have an error file.
*/
getJobErrorPath(job: Job): string | undefined;
}

/**
Expand Down Expand Up @@ -193,7 +202,6 @@ export class SlurmScheduler implements Scheduler {
new SchedulerDataColumn('NodeList', 255),
new SchedulerDataColumn('Partition', 255),
new SchedulerDataColumn('QOS', 255),
new SchedulerDataColumn('STDOUT', 255),
new SchedulerDataColumn('TimeLimit', 255),
new SchedulerDataColumn('TimeUsed', 255),
new SchedulerDataColumn('Command', 255), // command last since it can sometimes have spaces in it
Expand Down Expand Up @@ -343,6 +351,7 @@ export class SlurmScheduler implements Scheduler {
results['NodeList'],
results['Command'],
undefined /* let this be filled in by getJobOutputPath later */,
undefined /* error path is also patched later */,
timeLimit,
timeUsed
);
Expand All @@ -352,6 +361,55 @@ export class SlurmScheduler implements Scheduler {
return jobs;
}

/**
* Retrieves runtime metadata for a given job by executing the `scontrol show job` command.
*
* @param job - The job object containing the job ID.
* @returns A record containing key-value pairs of the job's runtime metadata.
* @throws Will throw an error if scontrol command fails
*/
private getJobRuntimeMetadata(job: Job): Record<string, any> {
const metadata: Record<string, any> = {};

const command = `scontrol show job ${job.id}`;
const output = execSync(command).toString().trim();
const lines = output.split('\n');
for (let line of lines) {
const pairs = line.split(/\s+/);
for (let pair of pairs) {
const [key, value] = pair.split('=');
if (key && value) {
metadata[key.trim()] = value.trim();
}
}
}

return metadata;
}

/**
* Patches the given job with runtime metadata.
*
* @param job - The job object to be patched.
* @param force - Optional. If true, forces the patching even if the job already has output and error files. Defaults to false.
*/
private patchJobWithRuntimeMetadata(job: Job, force: boolean = false): void {
/* early exit if we already have all the fields */
if (!force && job.outputFile && job.errorFile) {
return;
}

const metadata = this.getJobRuntimeMetadata(job);

if (metadata['StdOut']) {
job.outputFile = metadata['StdOut'];
}

if (metadata['StdErr']) {
job.errorFile = metadata['StdErr'];
}
}

/**
* Finds the path for the standard output file of a job.
* Returns job.outputFile if it is already defined.
Expand All @@ -361,44 +419,48 @@ export class SlurmScheduler implements Scheduler {
* @returns The resolved stdout path or undefined if the job does not have an output file.
*/
public getJobOutputPath(job: Job): string | undefined {
/* early exit if it's already defined */
if (job.outputFile) {
return job.outputFile;
return job.outputFile; /* early exit if it's already defined */
}

const command = `scontrol show job ${job.id}`;

try {
const output = execSync(command).toString().trim();

/* find line that starts with StdOut */
const lines = output.split('\n');
let stdoutLine: string | undefined = undefined;
for (let line of lines) {
if (line.trim().startsWith('StdOut=')) {
stdoutLine = line.trim();
break;
}
this.patchJobWithRuntimeMetadata(job);
if (!job.outputFile) {
throw new Error('Failed to get job output path.');
}
} catch (error) {
vscode.window.showErrorMessage(`Failed to get job output path for job ${job.id}.`);
}
return job.outputFile;
}

/* if we didn't find a line, return undefined */
if (!stdoutLine) {
throw new Error(`Failed to find stdout line in output: ${output}`);
}
/**
* Retrieves the error file path for a given job.
*
* This method first checks if the job's error file path is already defined.
* If it is, it returns the path immediately. If not, it attempts to patch
* the job with runtime metadata to retrieve the error file path.
*
* If the error file path is still not defined after patching, an error message
* is displayed to the user and `undefined` is returned.
*
* @param job - The job for which to retrieve the error file path.
* @returns The error file path as a string if available, otherwise `undefined`.
*/
public getJobErrorPath(job: Job): string | undefined {
if (job.errorFile) {
return job.errorFile; /* early exit if it's already defined */
}

/* extract path from line: StdOut=/path/to/file */
const parts = stdoutLine.split('=');
if (parts.length < 2) {
throw new Error(`Failed to parse stdout line: ${stdoutLine}`);
try {
this.patchJobWithRuntimeMetadata(job);
if (!job.errorFile) {
throw new Error('Failed to get job error path.');
}
const fpath = parts[1].trim();

job.outputFile = fpath;
return fpath;
} catch (error) {
vscode.window.showErrorMessage(`Failed to get job output path for job ${job.id}.\nError: ${error}`);
return undefined;
vscode.window.showErrorMessage(`Failed to get job error path for job ${job.id}.`);
}
return job.errorFile;
}
}

Expand All @@ -408,15 +470,15 @@ export class Debug implements Scheduler {
*/
// prettier-ignore
private jobs: Job[] = [
new Job("1", "job1", "RUNNING", "debug", '[node1]', "job1.sh", "job1.out", new WallTime(0, 0, 30, 0), new WallTime(0, 0, 12, 43)),
new Job("2", "job2", "RUNNING", "debug", '[node1]', "job2.sh", "job2.out", new WallTime(0, 1, 30, 0), new WallTime(0, 1, 28, 1)),
new Job("3", "job3", "RUNNING", "debug", '[node1]', "job3.sh", "job3.out", new WallTime(0, 0, 30, 0), new WallTime(0, 0, 1, 15)),
new Job("4", "job4", "PENDING", "debug", '[]', "job4.sh", "job4.out", new WallTime(0, 1, 20, 40), new WallTime(0, 0, 0, 0)),
new Job("5", "job5", "PENDING", "debug", '[]', "job5.sh", "job5.out", new WallTime(1, 12, 0, 0), new WallTime(0, 0, 0, 0)),
new Job("6", "job6", "COMPLETED", "debug", '[]', "job6.sh", "job6.out", new WallTime(0, 7, 0, 0), new WallTime(0, 7, 0, 0)),
new Job("7", "job7", "TIMEOUT", "debug", '[]', "job7.sh", "job7.out", new WallTime(0, 1, 30, 0), new WallTime(0, 1, 30, 0)),
new Job("8", "job8", "CANCELLED", "debug", '[]', "job8.sh", "job8.out", new WallTime(0, 23, 59, 59), new WallTime(0, 0, 0, 0)),
new Job("9", "job9", "FAILED", "debug", '[]', "job9.sh", "job9.out", new WallTime(0, 0, 5, 0), new WallTime(0, 0, 0, 0)),
new Job("1", "job1", "RUNNING", "debug", '[node1]', "job1.sh", "job1.out", "job1.err", new WallTime(0, 0, 30, 0), new WallTime(0, 0, 12, 43)),
new Job("2", "job2", "RUNNING", "debug", '[node1]', "job2.sh", "job2.out", "job2.err", new WallTime(0, 1, 30, 0), new WallTime(0, 1, 28, 1)),
new Job("3", "job3", "RUNNING", "debug", '[node1]', "job3.sh", "job3.out", "job3.err", new WallTime(0, 0, 30, 0), new WallTime(0, 0, 1, 15)),
new Job("4", "job4", "PENDING", "debug", '[]', "job4.sh", "job4.out", "job4.err", new WallTime(0, 1, 20, 40), new WallTime(0, 0, 0, 0)),
new Job("5", "job5", "PENDING", "debug", '[]', "job5.sh", "job5.out", "job5.err", new WallTime(1, 12, 0, 0), new WallTime(0, 0, 0, 0)),
new Job("6", "job6", "COMPLETED", "debug", '[]', "job6.sh", "job6.out", "job6.err", new WallTime(0, 7, 0, 0), new WallTime(0, 7, 0, 0)),
new Job("7", "job7", "TIMEOUT", "debug", '[]', "job7.sh", "job7.out", "job7.err", new WallTime(0, 1, 30, 0), new WallTime(0, 1, 30, 0)),
new Job("8", "job8", "CANCELLED", "debug", '[]', "job8.sh", "job8.out", "job8.err", new WallTime(0, 23, 59, 59), new WallTime(0, 0, 0, 0)),
new Job("9", "job9", "FAILED", "debug", '[]', "job9.sh", "job9.out", "job9.err", new WallTime(0, 0, 5, 0), new WallTime(0, 0, 0, 0)),
];

/**
Expand Down Expand Up @@ -452,6 +514,15 @@ export class Debug implements Scheduler {
public getJobOutputPath(job: Job): string | undefined {
return job.outputFile;
}

/**
* For the debug scheduler, just returns the job's error file.
* @param job The job for which to retrieve the error path.
* @returns The error path for the job or undefined if the job does not have an error file.
*/
public getJobErrorPath(job: Job): string | undefined {
return job.errorFile;
}
}

/**
Expand Down
Empty file.
Loading

0 comments on commit 04813f4

Please sign in to comment.