Skip to content

Commit 23ea7b1

Browse files
committed
Full support for jobs, asynch requests and job listings
1 parent 15ae46f commit 23ea7b1

File tree

13 files changed

+406
-49
lines changed

13 files changed

+406
-49
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Configuration;
2+
3+
namespace DynamicPowerShellApi.Configuration
4+
{
5+
/// <summary> A job store configuration. </summary>
6+
/// <remarks> Anthony, 6/1/2015. </remarks>
7+
/// <seealso cref="T:System.Configuration.ConfigurationElement"/>
8+
public class JobStoreConfiguration
9+
: ConfigurationElement
10+
{
11+
/// <summary> Gets the full pathname of the job directory. </summary>
12+
/// <value> The full pathname of the job file. </value>
13+
[ConfigurationProperty("JobStorePath", IsRequired = true)]
14+
public string JobStorePath
15+
{
16+
get
17+
{
18+
return (string)this["JobStorePath"];
19+
}
20+
}
21+
}
22+
}

Configuration/WebAPIConfiguration.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,15 @@ public Authentication Authentication
6767
return (Authentication)this["Authentication"];
6868
}
6969
}
70+
71+
/// <summary> Gets the jobs configuration object. </summary>
72+
[ConfigurationProperty("Jobs", IsRequired = true)]
73+
public JobStoreConfiguration Jobs
74+
{
75+
get
76+
{
77+
return (JobStoreConfiguration)this["Jobs"];
78+
}
79+
}
7080
}
7181
}

Constants.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,11 @@ public static class Constants
77
/// Full pathname of the status URL file.
88
/// </summary>
99
public const string StatusUrlPath = "/api/server/status";
10+
11+
/// <summary> Full pathname of the job list file. </summary>
12+
public const string JobListPath = "/api/server/jobs";
13+
14+
/// <summary> Full URI of the get job file. </summary>
15+
public const string GetJobPath = "/api/server/job";
1016
}
1117
}

Controllers/GenericController.cs

Lines changed: 171 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
using DynamicPowerShellApi.Logging;
1+
using System.IO;
2+
using System.Management.Automation;
3+
using System.Web.Http.Results;
4+
using DynamicPowerShellApi.Jobs;
5+
using DynamicPowerShellApi.Logging;
26
using DynamicPowerShellApi.Model;
7+
using Newtonsoft.Json;
38

49
namespace DynamicPowerShellApi.Controllers
510
{
611
using Configuration;
712
using Exceptions;
13+
using Microsoft.Owin;
814
using Newtonsoft.Json.Linq;
915
using System;
1016
using System.Collections.Generic;
@@ -29,26 +35,35 @@ public class GenericController : ApiController
2935
/// </summary>
3036
private readonly ICrashLogger _crashLogger;
3137

38+
/// <summary> The job list provider. </summary>
39+
private readonly IJobListProvider _jobListProvider;
40+
3241
/// <summary>
3342
/// Initialises a new instance of the <see cref="GenericController"/> class.
3443
/// </summary>
3544
public GenericController()
3645
{
3746
}
3847

39-
/// <summary>
40-
/// Initialises a new instance of the <see cref="GenericController"/> class.
41-
/// </summary>
42-
/// <param name="powershellRunner">
43-
/// The PowerShell runner.
44-
/// </param>
45-
/// <param name="crashLogger">
46-
/// An implementation of a crash logger.
47-
/// </param>
48-
public GenericController(IRunner powershellRunner, ICrashLogger crashLogger)
48+
/// <summary> Initialises a new instance of the <see cref="GenericController"/> class. </summary>
49+
/// <remarks> Anthony, 6/1/2015. </remarks>
50+
/// <exception cref="ArgumentNullException"> Thrown when one or more required arguments are
51+
/// null. </exception>
52+
/// <param name="powershellRunner"> The PowerShell runner. </param>
53+
/// <param name="crashLogger"> An implementation of a crash logger. </param>
54+
/// <param name="jobListProvider"> The job list provider. </param>
55+
public GenericController(IRunner powershellRunner, ICrashLogger crashLogger, IJobListProvider jobListProvider)
4956
{
57+
if (jobListProvider == null)
58+
throw new ArgumentNullException("jobListProvider");
59+
if (crashLogger == null)
60+
throw new ArgumentNullException("crashLogger");
61+
if (powershellRunner == null)
62+
throw new ArgumentNullException("powershellRunner");
63+
5064
_powershellRunner = powershellRunner;
5165
_crashLogger = crashLogger;
66+
_jobListProvider = jobListProvider;
5267
}
5368

5469
/// <summary>
@@ -59,7 +74,40 @@ public GenericController(IRunner powershellRunner, ICrashLogger crashLogger)
5974
[AllowAnonymous]
6075
public HttpResponseMessage Status()
6176
{
62-
return new HttpResponseMessage { Content = new StringContent("OK") };
77+
return new HttpResponseMessage {Content = new StringContent("OK")};
78+
}
79+
80+
[Route("jobs")]
81+
public dynamic AllJobStatus()
82+
{
83+
dynamic jobs = new
84+
{
85+
running = _jobListProvider.GetRunningJobs(),
86+
completed = _jobListProvider.GetCompletedJobs()
87+
};
88+
89+
return jobs;
90+
}
91+
92+
/// <summary> Gets a job. </summary>
93+
/// <remarks> Anthony, 6/1/2015. </remarks>
94+
/// <exception cref="ArgumentOutOfRangeException"> Thrown when one or more arguments are outside
95+
/// the required range. </exception>
96+
/// <param name="jobId"> Identifier for the job. </param>
97+
/// <returns> The job. </returns>
98+
[Route("job")]
99+
public dynamic GetJob(string jobId)
100+
{
101+
Guid jobGuid;
102+
if (!Guid.TryParse(jobId, out jobGuid))
103+
throw new ArgumentOutOfRangeException("jobId is not a valid GUID");
104+
105+
using (TextReader reader = new StreamReader(
106+
Path.Combine(WebApiConfiguration.Instance.Jobs.JobStorePath, jobId + ".json")))
107+
{
108+
dynamic d = JObject.Parse(reader.ReadToEnd());
109+
return d;
110+
}
63111
}
64112

65113
/// <summary>
@@ -79,7 +127,7 @@ public HttpResponseMessage Status()
79127
/// <exception cref="Exception">
80128
/// </exception>
81129
[AuthorizeIfEnabled]
82-
public async Task<HttpResponseMessage> ProcessRequestAsync()
130+
public async Task<HttpResponseMessage> ProcessRequestAsync(HttpRequestMessage request = null)
83131
{
84132
DynamicPowershellApiEvents
85133
.Raise
@@ -168,13 +216,114 @@ public async Task<HttpResponseMessage> ProcessRequestAsync()
168216
{
169217
DynamicPowershellApiEvents.Raise.VerboseMessaging(String.Format("Started Executing the runner"));
170218

171-
PowershellReturn output =
172-
await _powershellRunner.ExecuteAsync(method.PowerShellPath, method.Snapin, method.Module, query2.ToList(), asJob);
219+
if (!asJob)
220+
{
221+
PowershellReturn output =
222+
await _powershellRunner.ExecuteAsync(method.PowerShellPath, method.Snapin, method.Module, query2.ToList(), asJob);
223+
224+
JToken token = output.ActualPowerShellData.StartsWith("[")
225+
? (JToken) JArray.Parse(output.ActualPowerShellData)
226+
: JObject.Parse(output.ActualPowerShellData);
173227

174-
JToken token = output.ActualPowerShellData.StartsWith("[")
175-
? (JToken) JArray.Parse(output.ActualPowerShellData)
176-
: JObject.Parse(output.ActualPowerShellData);
177-
return new HttpResponseMessage { Content = new JsonContent(token) };
228+
return new HttpResponseMessage
229+
{
230+
Content = new JsonContent(token)
231+
};
232+
}
233+
else // run as job.
234+
{
235+
Guid jobId = Guid.NewGuid();
236+
string requestedHost = String.Empty;
237+
238+
if (Request.Properties.ContainsKey("MS_OwinContext"))
239+
requestedHost = ((OwinContext)Request.Properties["MS_OwinContext"]).Request.RemoteIpAddress;
240+
241+
_jobListProvider.AddRequestedJob(jobId, requestedHost );
242+
243+
// Go off and run this job please sir.
244+
var task = Task<bool>.Factory.StartNew(
245+
() =>
246+
{
247+
try
248+
{
249+
Task<PowershellReturn> goTask =
250+
_powershellRunner.ExecuteAsync(
251+
method.PowerShellPath,
252+
method.Snapin,
253+
method.Module,
254+
query2.ToList(),
255+
true);
256+
257+
goTask.Wait();
258+
var output = goTask.Result;
259+
260+
JToken token = output.ActualPowerShellData.StartsWith("[")
261+
? (JToken)JArray.Parse(output.ActualPowerShellData)
262+
: JObject.Parse(output.ActualPowerShellData);
263+
264+
_jobListProvider.CompleteJob(jobId, output.PowerShellReturnedValidData, String.Empty);
265+
266+
string outputPath = Path.Combine(WebApiConfiguration.Instance.Jobs.JobStorePath, jobId + ".json");
267+
268+
using (TextWriter writer = File.CreateText(outputPath))
269+
{
270+
JsonSerializer serializer = new JsonSerializer
271+
{
272+
Formatting = Formatting.Indented // Make it readable for Ops sake!
273+
};
274+
serializer.Serialize(writer, token);
275+
}
276+
277+
return true;
278+
}
279+
catch (PowerShellExecutionException poException)
280+
{
281+
CrashLogEntry entry = new CrashLogEntry
282+
{
283+
Exceptions = poException.Exceptions,
284+
LogTime = poException.LogTime,
285+
RequestAddress = requestedHost,
286+
RequestMethod = methodName,
287+
RequestUrl = Request.RequestUri.ToString()
288+
};
289+
entry.SetActivityId(activityId);
290+
string logFile = _crashLogger.SaveLog(entry);
291+
292+
DynamicPowershellApiEvents.Raise.InvalidPowerShellOutput(poException.Message + " logged to " + logFile);
293+
294+
ErrorResponse response =
295+
new ErrorResponse
296+
{
297+
ActivityId = activityId,
298+
LogFile = logFile,
299+
Message = poException.Message
300+
};
301+
302+
JToken token = new JObject(response);
303+
304+
_jobListProvider.CompleteJob(jobId, false, String.Empty);
305+
306+
string outputPath = Path.Combine(WebApiConfiguration.Instance.Jobs.JobStorePath, jobId + ".json");
307+
308+
using (TextWriter writer = File.CreateText(outputPath))
309+
{
310+
JsonSerializer serializer = new JsonSerializer
311+
{
312+
Formatting = Formatting.Indented // Make it readable for Ops sake!
313+
};
314+
serializer.Serialize(writer, token);
315+
}
316+
return true;
317+
}
318+
}
319+
);
320+
321+
// return the Job ID.
322+
return new HttpResponseMessage
323+
{
324+
Content = new JsonContent(new JValue(jobId))
325+
};
326+
}
178327
}
179328
catch (PowerShellExecutionException poException)
180329
{
@@ -191,7 +340,7 @@ public async Task<HttpResponseMessage> ProcessRequestAsync()
191340

192341
DynamicPowershellApiEvents.Raise.InvalidPowerShellOutput(poException.Message + " logged to " + logFile);
193342

194-
var response = Request.CreateResponse<ErrorResponse>(HttpStatusCode.InternalServerError,
343+
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.InternalServerError,
195344
new ErrorResponse
196345
{
197346
ActivityId = activityId,
@@ -206,7 +355,7 @@ public async Task<HttpResponseMessage> ProcessRequestAsync()
206355
{
207356
CrashLogEntry entry = new CrashLogEntry
208357
{
209-
Exceptions = new List<PowerShellException>()
358+
Exceptions = new List<PowerShellException>
210359
{
211360
new PowerShellException
212361
{
@@ -226,7 +375,7 @@ public async Task<HttpResponseMessage> ProcessRequestAsync()
226375

227376
DynamicPowershellApiEvents.Raise.UnhandledException(ex.Message + " logged to " + logFile, ex.StackTrace ?? String.Empty);
228377

229-
var response = Request.CreateResponse<ErrorResponse>(HttpStatusCode.InternalServerError,
378+
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.InternalServerError,
230379
new ErrorResponse
231380
{
232381
ActivityId = activityId,

DynamicPowerShellAPI.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,16 @@
103103
<SpecificVersion>False</SpecificVersion>
104104
<HintPath>packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.0.26\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll</HintPath>
105105
</Reference>
106+
<Reference Include="Microsoft.Owin, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
107+
<HintPath>packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll</HintPath>
108+
</Reference>
106109
<Reference Include="Newtonsoft.Json">
107110
<HintPath>packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
108111
<Private>True</Private>
109112
</Reference>
113+
<Reference Include="Owin">
114+
<HintPath>packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
115+
</Reference>
110116
<Reference Include="System" />
111117
<Reference Include="System.Configuration" />
112118
<Reference Include="System.Core" />
@@ -128,6 +134,7 @@
128134
<ItemGroup>
129135
<Compile Include="AuthorizeIfEnabledAttribute.cs" />
130136
<Compile Include="Configuration\Authentication.cs" />
137+
<Compile Include="Configuration\JobStoreConfiguration.cs" />
131138
<Compile Include="Configuration\WebApiCollection.cs" />
132139
<Compile Include="Configuration\Parameter.cs" />
133140
<Compile Include="Configuration\ParameterCollection.cs" />
@@ -148,6 +155,8 @@
148155
<Compile Include="GenericActionSelector.cs" />
149156
<Compile Include="GenericControllerSelector.cs" />
150157
<Compile Include="IRunner.cs" />
158+
<Compile Include="Jobs\IJobListProvider.cs" />
159+
<Compile Include="Jobs\JobListProvider.cs" />
151160
<Compile Include="JsonContent.cs" />
152161
<Compile Include="Logging\CrashLog.cs" />
153162
<Compile Include="Logging\CrashLogger.cs" />

DynamicPowerShellApi.Host/App.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<section name="WebApiConfiguration" type="DynamicPowerShellApi.Configuration.WebApiConfiguration, DynamicPowerShellApi" allowLocation="true" allowDefinition="Everywhere" />
55
</configSections>
66
<WebApiConfiguration HostAddress="http://localhost:9000">
7+
<Jobs JobStorePath="c:\temp\" />
78
<Authentication Enabled="false" StoreName="My" StoreLocation="LocalMachine" Thumbprint="E6B6364C75ED8B6495A42D543AC728B4C2263082" Audience="http://aperture.identity/connectors" />
89
<Apis>
910
<WebApi Name="Example">

GenericActionSelector.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,22 @@ public GenericActionSelector(HttpConfiguration configuration)
4949
/// </returns>
5050
public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
5151
{
52+
// if the user is requesting the server status..
5253
if (controllerContext.Request.RequestUri.AbsolutePath == Constants.StatusUrlPath)
5354
return new ReflectedHttpActionDescriptor(
5455
new HttpControllerDescriptor(_currentConfiguration, "generic", typeof(GenericController)),
5556
typeof(GenericController).GetMethod("Status"));
5657

58+
if (controllerContext.Request.RequestUri.AbsolutePath == Constants.JobListPath)
59+
return new ReflectedHttpActionDescriptor(
60+
new HttpControllerDescriptor(_currentConfiguration, "generic", typeof(GenericController)),
61+
typeof(GenericController).GetMethod("AllJobStatus"));
62+
63+
if (controllerContext.Request.RequestUri.AbsolutePath == Constants.GetJobPath)
64+
return new ReflectedHttpActionDescriptor(
65+
new HttpControllerDescriptor(_currentConfiguration, "generic", typeof(GenericController)),
66+
typeof(GenericController).GetMethod("GetJob"));
67+
5768
// Always give the same action
5869
return ActionDescriptor;
5970
}
@@ -69,9 +80,7 @@ public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDesc
6980
{
7081
// Exercised only by ASP.NET Web API’s API explorer feature
7182

72-
List<HttpActionDescriptor> descriptors = new List<HttpActionDescriptor>();
73-
74-
descriptors.Add(ActionDescriptor);
83+
List<HttpActionDescriptor> descriptors = new List<HttpActionDescriptor> { ActionDescriptor };
7584

7685
ILookup<string, HttpActionDescriptor> result = descriptors.ToLookup(
7786
p => "generic",

0 commit comments

Comments
 (0)