diff --git a/Refund.Tests/JobQueues/ClusterQueueBatchTests.cs b/Refund.Tests/JobQueues/ClusterQueueBatchTests.cs index eafa355..478489f 100644 --- a/Refund.Tests/JobQueues/ClusterQueueBatchTests.cs +++ b/Refund.Tests/JobQueues/ClusterQueueBatchTests.cs @@ -78,6 +78,15 @@ public void ParseActiveJobs_AcceptsCommaSeparator(string line, string id, Cluste Assert.Equal(expected, result[id]); } + [Fact] + public void JobResourceValues_IncludeSubmittingUsername() + { + var job = new MaskJob { SubmittedByUsername = "alice" }; + var values = job.GetResourceValues(); + + Assert.Equal("alice", values["username"]); + } + [Fact] public void BuildWorkerScript_PreservesDollarSignsInCommand() { diff --git a/Refund/DataModel/Job.cs b/Refund/DataModel/Job.cs index 7c8c837..f104b7d 100644 --- a/Refund/DataModel/Job.cs +++ b/Refund/DataModel/Job.cs @@ -204,6 +204,14 @@ public abstract class Job : RelayBase, IFolderContent [Clearable] public User UpdatedBy { get; set; } + /// + /// Username of the user who queued/submitted this job. + /// This is included in cluster submission templates via {{username}}. + /// + [Clearable] + [RelayProperty] + public string SubmittedByUsername { get; set; } = string.Empty; + /// /// User-provided notes or comments about this job. /// @@ -430,7 +438,8 @@ public Dictionary GetResourceValues() { "gpu_memory_gb", GpuMemoryGb.ToString() }, { "run_directory", RunDirectory }, { "std_out", PathStdOut }, - { "std_err", PathStdErr } + { "std_err", PathStdErr }, + { "username", SubmittedByUsername } }; } diff --git a/Refund/JobQueues/ClusterQueue.cs b/Refund/JobQueues/ClusterQueue.cs index 53435b3..4c08394 100644 --- a/Refund/JobQueues/ClusterQueue.cs +++ b/Refund/JobQueues/ClusterQueue.cs @@ -516,7 +516,7 @@ await job.WriteToLifecycleLog( await job.WriteToLifecycleLog($"Submitting script: {scriptPath}"); string rawOutput = null; - string jobId = await SubmitScript(scriptPath, o => rawOutput = o); + string jobId = await SubmitScript(scriptPath, o => rawOutput = o, job.SubmittedByUsername); await job.WriteToLifecycleLog(rawOutput); await job.WriteToLifecycleLog($"Parsed cluster job ID: {jobId}"); @@ -684,9 +684,12 @@ private async Task PrepareAndWriteScript(Job job, Dictionary - public async Task SubmitScript(string scriptPath, Action onRawOutput = null) + public async Task SubmitScript(string scriptPath, Action onRawOutput = null, string username = "") { - string clusterCommand = SubmitJobTemplate.ReplaceRegex("{{\\s*script_path_abs\\s*}}", scriptPath); + string clusterCommand = SubmitJobTemplate + .ReplaceRegex("{{\\s*script_path_abs\\s*}}", scriptPath) + .ReplaceRegex("{{\\s*username\\s*}}", username ?? string.Empty); + string output = await ExecuteOnCluster(clusterCommand); onRawOutput?.Invoke(output); return ParseClusterJobId(output); diff --git a/Refund/Services/Core/DataManager/DataManager.Job.cs b/Refund/Services/Core/DataManager/DataManager.Job.cs index 043fd6a..5f4d996 100644 --- a/Refund/Services/Core/DataManager/DataManager.Job.cs +++ b/Refund/Services/Core/DataManager/DataManager.Job.cs @@ -613,6 +613,7 @@ await ExecuteWithLock(async () => { j.Status = JobStatus.Waiting; j.QueueId = originalQueue.Id; + j.SubmittedByUsername = user.Username; j.AddEvent(EventType.WaitingStarted, originalUser); }); diff --git a/Relay/Screens/Overlay/Settings/QueueEditor.razor.cs b/Relay/Screens/Overlay/Settings/QueueEditor.razor.cs index e5f63cc..b5faa74 100644 --- a/Relay/Screens/Overlay/Settings/QueueEditor.razor.cs +++ b/Relay/Screens/Overlay/Settings/QueueEditor.razor.cs @@ -181,6 +181,7 @@ private IEnumerable GetSubmissionScriptVariables() var variables = new List { "command", + "username", "job_id", "n_processes", "n_cores", @@ -205,7 +206,7 @@ private IEnumerable GetCommandTemplateVariables() private IEnumerable GetSubmitJobTemplateVariables() { - return ["script_path_abs"]; + return ["script_path_abs", "username"]; } private IEnumerable GetStatusJobTemplateVariables()