Skip to content

Commit 44c3513

Browse files
feat(migration): Convert Get-DbaRunningJob to C# binary cmdlet
- All parameters preserved (SqlInstance, SqlCredential, InputObject, EnableException) - Both pipeline paths implemented (SqlInstance and InputObject) - Delegates to Get-DbaAgentJob -IncludeExecution for job enrichment - Job refresh and idle filtering preserved - Build passes on net472 and net8.0 - C# unit tests written and passing (8 tests for IsRunning helper) - Pester integration tests pass (1/1 baseline maintained) - Feature parity verified - PS1 retired, cmdlet exported from dbatools.library Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e89cb5e commit 44c3513

4 files changed

Lines changed: 349 additions & 1 deletion

File tree

dbatools.library.psd1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
'Get-DbaCpuRingBuffer',
8484
'Get-DbaCpuUsage',
8585
'Get-DbaDeprecatedFeature',
86+
'Get-DbaRunningJob',
8687
'Get-DbaConnectedInstance',
8788
'Get-DbaConnection',
8889
'Get-DbatoolsChangeLog',

docs/plan/TRACKER-MIGRATE-AGENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
| 10 | Get-DbaAgentProxy | DONE | GetDbaAgentProxyCommand.cs | OK | 100% | 1/1 pass (9 pre-existing failures) | Read-only, no deps |
2020
| 11 | Get-DbaAgentSchedule | DONE | GetDbaAgentScheduleCommand.cs | OK | 100% | 1/1 pass (10 pre-existing failures in BeforeAll) | Read-only, no deps |
2121
| 12 | Get-DbaAgentServer | DONE | GetDbaAgentServerCommand.cs | OK | 100% | 2/2 pass (fixed pre-existing 1 failure) | Read-only, no deps |
22-
| 13 | Get-DbaRunningJob | PENDING | | | | | Read-only, no deps |
22+
| 13 | Get-DbaRunningJob | DONE | GetDbaRunningJobCommand.cs | OK | OK | 1/1 | Read-only, delegates to Get-DbaAgentJob |
2323
| 14 | Test-DbaAgentJobOwner | PENDING | | | | | |
2424
| 15 | Find-DbaAgentJob | PENDING | | | | | |
2525
| 16 | New-DbaAgentAlert | PENDING | | | | | ShouldProcess required |
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System;
2+
using System.Management.Automation;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
5+
namespace Dataplat.Dbatools.Tests.Commands
6+
{
7+
[TestClass]
8+
public class GetDbaRunningJobCommandTests
9+
{
10+
#region IsRunning
11+
12+
[TestMethod]
13+
public void IsRunning_IdleJob_ReturnsFalse()
14+
{
15+
// Arrange
16+
PSObject jobObj = new PSObject();
17+
jobObj.Properties.Add(new PSNoteProperty("CurrentRunStatus", "Idle"));
18+
19+
// Act
20+
bool result = Dataplat.Dbatools.Commands.GetDbaRunningJobCommand.IsRunning(jobObj);
21+
22+
// Assert
23+
Assert.IsFalse(result);
24+
}
25+
26+
[TestMethod]
27+
public void IsRunning_ExecutingJob_ReturnsTrue()
28+
{
29+
// Arrange
30+
PSObject jobObj = new PSObject();
31+
jobObj.Properties.Add(new PSNoteProperty("CurrentRunStatus", "Executing"));
32+
33+
// Act
34+
bool result = Dataplat.Dbatools.Commands.GetDbaRunningJobCommand.IsRunning(jobObj);
35+
36+
// Assert
37+
Assert.IsTrue(result);
38+
}
39+
40+
[TestMethod]
41+
public void IsRunning_SuspendedJob_ReturnsTrue()
42+
{
43+
// Arrange
44+
PSObject jobObj = new PSObject();
45+
jobObj.Properties.Add(new PSNoteProperty("CurrentRunStatus", "Suspended"));
46+
47+
// Act
48+
bool result = Dataplat.Dbatools.Commands.GetDbaRunningJobCommand.IsRunning(jobObj);
49+
50+
// Assert
51+
Assert.IsTrue(result);
52+
}
53+
54+
[TestMethod]
55+
public void IsRunning_IdleCaseInsensitive_ReturnsFalse()
56+
{
57+
// Arrange
58+
PSObject jobObj = new PSObject();
59+
jobObj.Properties.Add(new PSNoteProperty("CurrentRunStatus", "IDLE"));
60+
61+
// Act
62+
bool result = Dataplat.Dbatools.Commands.GetDbaRunningJobCommand.IsRunning(jobObj);
63+
64+
// Assert
65+
Assert.IsFalse(result);
66+
}
67+
68+
[TestMethod]
69+
public void IsRunning_NullObject_ReturnsFalse()
70+
{
71+
// Act
72+
bool result = Dataplat.Dbatools.Commands.GetDbaRunningJobCommand.IsRunning(null);
73+
74+
// Assert
75+
Assert.IsFalse(result);
76+
}
77+
78+
[TestMethod]
79+
public void IsRunning_MissingProperty_ReturnsFalse()
80+
{
81+
// Arrange
82+
PSObject jobObj = new PSObject();
83+
jobObj.Properties.Add(new PSNoteProperty("Name", "TestJob"));
84+
85+
// Act
86+
bool result = Dataplat.Dbatools.Commands.GetDbaRunningJobCommand.IsRunning(jobObj);
87+
88+
// Assert
89+
Assert.IsFalse(result);
90+
}
91+
92+
[TestMethod]
93+
public void IsRunning_NullPropertyValue_ReturnsFalse()
94+
{
95+
// Arrange
96+
PSObject jobObj = new PSObject();
97+
jobObj.Properties.Add(new PSNoteProperty("CurrentRunStatus", null));
98+
99+
// Act
100+
bool result = Dataplat.Dbatools.Commands.GetDbaRunningJobCommand.IsRunning(jobObj);
101+
102+
// Assert
103+
Assert.IsFalse(result);
104+
}
105+
106+
[TestMethod]
107+
public void IsRunning_WaitingForWorkerThread_ReturnsTrue()
108+
{
109+
// Arrange - tests another non-idle status
110+
PSObject jobObj = new PSObject();
111+
jobObj.Properties.Add(new PSNoteProperty("CurrentRunStatus", "WaitingForWorkerThread"));
112+
113+
// Act
114+
bool result = Dataplat.Dbatools.Commands.GetDbaRunningJobCommand.IsRunning(jobObj);
115+
116+
// Assert
117+
Assert.IsTrue(result);
118+
}
119+
120+
#endregion IsRunning
121+
}
122+
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
using System;
2+
using System.Collections.ObjectModel;
3+
using System.Management.Automation;
4+
using Dataplat.Dbatools.Message;
5+
using Dataplat.Dbatools.Parameter;
6+
7+
namespace Dataplat.Dbatools.Commands
8+
{
9+
/// <summary>
10+
/// Retrieves SQL Server Agent jobs that are currently executing from one or more instances.
11+
/// Filters out idle jobs and returns only those with an active run status.
12+
/// </summary>
13+
[Cmdlet("Get", "DbaRunningJob")]
14+
// Note: DbaBaseCmdlet used intentionally — SqlInstance is optional here because
15+
// the command also accepts InputObject (Job objects) from the pipeline.
16+
// DbaInstanceCmdlet declares SqlInstance as Mandatory, which would break the InputObject-only path.
17+
public class GetDbaRunningJobCommand : DbaBaseCmdlet
18+
{
19+
#region Parameters
20+
21+
/// <summary>
22+
/// The target SQL Server instance(s) to check for running jobs.
23+
/// </summary>
24+
[Parameter(ValueFromPipeline = true)]
25+
public DbaInstanceParameter[] SqlInstance { get; set; }
26+
27+
/// <summary>
28+
/// Credential to use for SQL Server authentication.
29+
/// </summary>
30+
[Parameter()]
31+
public PSCredential SqlCredential { get; set; }
32+
33+
/// <summary>
34+
/// Accepts SQL Server Agent job objects piped from Get-DbaAgentJob for filtering to only running jobs.
35+
/// Use this when you need to check execution status on a specific set of jobs rather than all jobs on an instance.
36+
/// </summary>
37+
[Parameter(ValueFromPipeline = true)]
38+
public PSObject[] InputObject { get; set; }
39+
40+
#endregion Parameters
41+
42+
#region Cached ScriptBlocks
43+
44+
private static readonly ScriptBlock ConnectWithCredScript =
45+
ScriptBlock.Create("param($i, $c) Connect-DbaInstance -SqlInstance $i -SqlCredential $c");
46+
47+
private static readonly ScriptBlock ConnectScript =
48+
ScriptBlock.Create("param($i) Connect-DbaInstance -SqlInstance $i");
49+
50+
private static readonly ScriptBlock RefreshJobsScript =
51+
ScriptBlock.Create("param($s) $s.JobServer.Jobs.Refresh($true)");
52+
53+
private static readonly ScriptBlock GetAgentJobScript =
54+
ScriptBlock.Create("param($s) Get-DbaAgentJob -SqlInstance $s -IncludeExecution");
55+
56+
private static readonly ScriptBlock RefreshJobScript =
57+
ScriptBlock.Create("param($j) $j.Refresh()");
58+
59+
#endregion Cached ScriptBlocks
60+
61+
/// <summary>
62+
/// Processes each SQL Server instance or input job object, filtering to only running jobs.
63+
/// </summary>
64+
protected override void ProcessRecord()
65+
{
66+
if (SqlInstance != null)
67+
{
68+
foreach (DbaInstanceParameter instance in SqlInstance)
69+
{
70+
try
71+
{
72+
object server = ConnectInstance(instance);
73+
if (server == null)
74+
{
75+
StopFunction(
76+
"Failure",
77+
target: instance,
78+
isContinue: true,
79+
category: ErrorCategory.ConnectionError);
80+
TestFunctionInterrupt();
81+
continue;
82+
}
83+
84+
// Refresh JobServer.Jobs (including children) for up-to-date information
85+
RefreshJobServerJobs(server);
86+
87+
// Get all jobs with execution info via Get-DbaAgentJob -IncludeExecution
88+
Collection<PSObject> jobs = InvokeGetDbaAgentJob(server);
89+
if (jobs == null || jobs.Count == 0)
90+
continue;
91+
92+
// Filter to only running jobs (CurrentRunStatus != Idle)
93+
foreach (PSObject jobObj in jobs)
94+
{
95+
if (jobObj == null)
96+
continue;
97+
98+
if (IsRunning(jobObj))
99+
{
100+
WriteObject(jobObj);
101+
}
102+
}
103+
}
104+
catch (Exception ex)
105+
{
106+
StopFunction(
107+
"Failure",
108+
errorRecord: new ErrorRecord(ex, "GetDbaRunningJob_ConnectionError", ErrorCategory.ConnectionError, instance),
109+
target: instance,
110+
isContinue: true,
111+
category: ErrorCategory.ConnectionError);
112+
TestFunctionInterrupt();
113+
continue;
114+
}
115+
}
116+
}
117+
118+
if (InputObject != null)
119+
{
120+
foreach (PSObject job in InputObject)
121+
{
122+
if (job == null)
123+
continue;
124+
125+
// Refresh the job to get up-to-date information.
126+
// No try/catch here — matching PS1 behavior where Refresh() errors propagate naturally.
127+
RefreshJob(job);
128+
129+
if (IsRunning(job))
130+
{
131+
WriteObject(job);
132+
}
133+
}
134+
}
135+
}
136+
137+
#region Helpers
138+
139+
/// <summary>
140+
/// Connects to a SQL Server instance via Connect-DbaInstance.
141+
/// </summary>
142+
private object ConnectInstance(DbaInstanceParameter instance)
143+
{
144+
object[] args;
145+
ScriptBlock script;
146+
if (SqlCredential != null)
147+
{
148+
script = ConnectWithCredScript;
149+
args = new object[] { instance, SqlCredential };
150+
}
151+
else
152+
{
153+
script = ConnectScript;
154+
args = new object[] { instance };
155+
}
156+
157+
Collection<PSObject> results = InvokeCommand.InvokeScript(
158+
false, script, null, args);
159+
160+
if (results != null && results.Count > 0)
161+
return results[0].BaseObject;
162+
return null;
163+
}
164+
165+
/// <summary>
166+
/// Refreshes the JobServer.Jobs collection with children for up-to-date status.
167+
/// </summary>
168+
private void RefreshJobServerJobs(object server)
169+
{
170+
try
171+
{
172+
InvokeCommand.InvokeScript(
173+
false, RefreshJobsScript, null, new object[] { server });
174+
}
175+
catch (Exception ex)
176+
{
177+
WriteMessageAtLevel(
178+
String.Format("Failed to refresh job server jobs: {0}", ex.Message),
179+
MessageLevel.Warning, null);
180+
}
181+
}
182+
183+
/// <summary>
184+
/// Calls Get-DbaAgentJob -SqlInstance $server -IncludeExecution to get enriched job objects.
185+
/// </summary>
186+
private Collection<PSObject> InvokeGetDbaAgentJob(object server)
187+
{
188+
return InvokeCommand.InvokeScript(
189+
false, GetAgentJobScript, null, new object[] { server });
190+
}
191+
192+
/// <summary>
193+
/// Refreshes a single job object to get up-to-date status information.
194+
/// </summary>
195+
private void RefreshJob(PSObject job)
196+
{
197+
object baseObj = job.BaseObject;
198+
InvokeCommand.InvokeScript(
199+
false, RefreshJobScript, null, new object[] { baseObj });
200+
}
201+
202+
/// <summary>
203+
/// Checks if a job's CurrentRunStatus is not Idle.
204+
/// </summary>
205+
internal static bool IsRunning(PSObject jobObj)
206+
{
207+
if (jobObj == null)
208+
return false;
209+
try
210+
{
211+
PSPropertyInfo prop = jobObj.Properties["CurrentRunStatus"];
212+
if (prop == null || prop.Value == null)
213+
return false;
214+
string status = prop.Value.ToString();
215+
return !String.Equals(status, "Idle", StringComparison.OrdinalIgnoreCase);
216+
}
217+
catch (Exception)
218+
{
219+
return false;
220+
}
221+
}
222+
223+
#endregion Helpers
224+
}
225+
}

0 commit comments

Comments
 (0)