diff --git a/ServerTaskHelper/HttpRequestSampleWithoutHandler/Http/Program.cs b/ServerTaskHelper/HttpRequestSampleWithoutHandler/Http/Program.cs new file mode 100644 index 000000000..8534bece1 --- /dev/null +++ b/ServerTaskHelper/HttpRequestSampleWithoutHandler/Http/Program.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace HttpRequestSampleWithoutHandler.Http +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/ServerTaskHelper/HttpRequestSampleWithoutHandler/Http/Startup.cs b/ServerTaskHelper/HttpRequestSampleWithoutHandler/Http/Startup.cs new file mode 100644 index 000000000..5286d8735 --- /dev/null +++ b/ServerTaskHelper/HttpRequestSampleWithoutHandler/Http/Startup.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HttpRequestSampleWithoutHandler.Http +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMvc(); + } + } +} diff --git a/ServerTaskHelper/HttpRequestSampleWithoutHandler/HttpRequestSampleWithoutHandler.csproj b/ServerTaskHelper/HttpRequestSampleWithoutHandler/HttpRequestSampleWithoutHandler.csproj new file mode 100644 index 000000000..46d7a54ad --- /dev/null +++ b/ServerTaskHelper/HttpRequestSampleWithoutHandler/HttpRequestSampleWithoutHandler.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp2.1 + + + + + + + diff --git a/ServerTaskHelper/HttpRequestSampleWithoutHandler/MyApp.cs b/ServerTaskHelper/HttpRequestSampleWithoutHandler/MyApp.cs new file mode 100644 index 000000000..d9eb38fa4 --- /dev/null +++ b/ServerTaskHelper/HttpRequestSampleWithoutHandler/MyApp.cs @@ -0,0 +1,243 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace HttpRequestSampleWithoutHandler +{ + internal class MyApp + { + /* + While task execution you can see following logs in pipeline task log UI. + + 2019-01-06T13:29:46.8245516Z POST http://localhost:57636/api/mytask + Response Code: OK + Response: + 2019-01-06T13:29:47.2478076Z Task Started + 2019-01-06T13:29:47.2478595Z Message 0 + 2019-01-06T13:29:48.2896346Z Message 1 + 2019-01-06T13:29:49.3278869Z Message 2 + 2019-01-06T13:29:50.3780603Z Message 3 + 2019-01-06T13:29:51.4158194Z Message 4 + 2019-01-06T13:29:52.4514848Z Message 5 + 2019-01-06T13:29:53.4912485Z Message 6 + 2019-01-06T13:29:54.5305565Z Message 7 + 2019-01-06T13:29:55.5703345Z Message 8 + 2019-01-06T13:29:56.6087177Z Message 9 + 2019-01-06T13:29:57.6914822Z Task Completed + */ + + internal static void ExecuteAsync(string taskMessageBody, string projectId, string planUri, string hubName, string planId, string jobId, string timelineId, string taskInstanceId, string taskInstanceName, string authToken) + { + // Instead of sending logs each time, batch the logs and send the logs. + // see https://github.com/Microsoft/azure-pipelines-extensions/blob/master/ServerTaskHelper/DistributedTask.ServerTask.Remote.Common/TaskProgress/TaskLogger.cs to send logs in batches. + var pagesFolder = Path.Combine(Path.GetTempPath(), "pages"); + Directory.Delete(pagesFolder, true); + Directory.CreateDirectory(pagesFolder); + var logFileName = Path.Combine(pagesFolder, $"taskLog.log"); + var pageData = new FileStream(logFileName, FileMode.CreateNew); + var pageWriter = new StreamWriter(pageData, System.Text.Encoding.UTF8); + + using (var httpClient = new HttpClient()) + { + try + { + // Send task started event + SendTaskStartedEvent(httpClient, authToken, planUri, projectId, hubName, planId, jobId, taskInstanceId); + + // send task started message feed and log the message. You will see feed messages in task log UI. + SendTaskLogFeeds(httpClient, authToken, planUri, projectId, hubName, planId, jobId, timelineId, "Task Started"); + pageWriter.WriteLine($"{DateTime.UtcNow:O} Task Started"); + + // Do work + for (int i = 0; i < 10; i++) + { + var logMessage = $"{DateTime.UtcNow:O} Message {i}"; + SendTaskLogFeeds(httpClient, authToken, planUri, projectId, hubName, planId, jobId, timelineId, logMessage); + pageWriter.WriteLine(logMessage); + Thread.Sleep(1000); + } + + // Work completed now send task completed event state to mark task completed with succeeded/failed status + SendTaskCompletedEvent(httpClient, authToken, planUri, projectId, hubName, planId, jobId, taskInstanceId, "succeeded"); + + // Log the task completed message + pageWriter.WriteLine($"{DateTime.UtcNow:O} Task Completed"); + } + catch (Exception) + { + // Work completed now send task completed event with status 'failed' to mark task failed + SendTaskCompletedEvent(httpClient, authToken, planUri, projectId, hubName, planId, jobId, taskInstanceId, "failed"); + } + finally + { + // Upload the task logs. Create task log and append the all logs to task log. + // Create task log entry + var taskLogObjectString = CreateTaskLog(httpClient, authToken, planUri, projectId, hubName, planId, taskInstanceId); + var taskLogObject = JObject.Parse(taskLogObjectString); + + pageWriter.Flush(); + pageData.Flush(); + pageWriter.Dispose(); + + // Append task log data + AppendToTaskLog(httpClient, authToken, planUri, projectId, hubName, planId, taskLogObject["id"].Value(), logFileName); + + // Attache task log to the timeline record + UpdateTaskTimelineRecord(httpClient, authToken, planUri, projectId, hubName, planId, timelineId, taskInstanceId, taskLogObjectString); + } + } + } + + private static void SendTaskStartedEvent(HttpClient httpClient, string authToken, string planUri, string projectId, string hubName, string planId, string jobId, string taskInstanceId) + { + // Task Event example: + // url: {planUri}/{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/events?api-version=2.0-preview.1 + // body : { "name": "TaskStarted", "taskId": "taskInstanceId", "jobId": "jobId" } + + const string TaskEventsUrl = "{0}/{1}/_apis/distributedtask/hubs/{2}/plans/{3}/events?api-version=2.0-preview.1"; + + string taskStartedEventUrl = string.Format(TaskEventsUrl, planUri, projectId, hubName, planId); + var requestBodyJObject = new JObject(new JProperty("name", "TaskStarted")); + requestBodyJObject.Add(new JProperty("jobId", jobId)); + requestBodyJObject.Add(new JProperty("taskId", taskInstanceId)); + string requestBody = JsonConvert.SerializeObject(requestBodyJObject); + + PostData(httpClient, taskStartedEventUrl, requestBody, authToken); + } + + private static void SendTaskCompletedEvent(HttpClient httpClient, string authToken, string planUri, string projectId, string hubName, string planId, string jobId, string taskInstanceId, string result) + { + // Task Event example: + // url: {planUri}/{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/events?api-version=2.0-preview.1 + // body : ex: { "name": "TaskCompleted", "taskId": "taskInstanceId", "jobId": "jobId", "result": "succeeded" } + + const string TaskEventsUrl = "{0}/{1}/_apis/distributedtask/hubs/{2}/plans/{3}/events?api-version=2.0-preview.1"; + + var taskCompletedUrl = string.Format(TaskEventsUrl, planUri, projectId, hubName, planId); + var requestBodyJObject = new JObject(new JProperty("name", "TaskCompleted")); + requestBodyJObject.Add(new JProperty("result", result)); // succeeded or failed + requestBodyJObject.Add(new JProperty("jobId", jobId)); + requestBodyJObject.Add(new JProperty("taskId", taskInstanceId)); + var requestBody = JsonConvert.SerializeObject(requestBodyJObject); + + PostData(httpClient, taskCompletedUrl, requestBody, authToken); + } + + private static void SendTaskLogFeeds(HttpClient httpClient, string authToken, string planUri, string projectId, string hubName, string planId, string jobId, string timeLineId, string message) + { + // Task feed example: + // url : {planUri}/{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/timelines/{timelineId}/records/{jobId}/feed?api-version=4.1 + // body : {"value":["2019-01-04T12:32:42.2042287Z Task started."],"count":1} + + const string SendTaskFeedUrl = "{0}/{1}/_apis/distributedtask/hubs/{2}/plans/{3}/timelines/{4}/records/{5}/feed?api-version=4.1"; + + var taskFeedUrl = string.Format(SendTaskFeedUrl, planUri, projectId, hubName, planId, timeLineId, jobId); + + JArray array = new JArray(); + array.Add(message); + var requestBodyJObject = new JObject(); + requestBodyJObject["value"] = array; + requestBodyJObject.Add(new JProperty("count", 1)); + var requestBody = JsonConvert.SerializeObject(requestBodyJObject); + // request body for task feed { "value": "array of log data", "count": number of logs}, ex: {"value": ["2019-01-04T12:35:49.9198590Z container starting"], "count": 1} + + PostData(httpClient, taskFeedUrl, requestBody, authToken); + } + + private static string CreateTaskLog(HttpClient httpClient, string authToken, string planUri, string projectId, string hubName, string planId, string taskInstanceId) + { + // Create task log + // url: {planUri}/{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/logs?api-version=4.1" + // body: {"path":"logs\\{taskInstanceId}"}, example: {"path":"logs\\3b9c4dc6-1e5d-4379-b16c-6231d7620059"} + + const string CreateTaskLogUrl = "{0}/{1}/_apis/distributedtask/hubs/{2}/plans/{3}/logs?api-version=4.1"; + + var taskLogCreateUrl = string.Format(CreateTaskLogUrl, planUri, projectId, hubName, planId); + var requestBodyJObject = new JObject(new JProperty("path", string.Format(@"logs\{0:D}", taskInstanceId))); + var requestBody = JsonConvert.SerializeObject(requestBodyJObject); + + return PostData(httpClient, taskLogCreateUrl, requestBody, authToken).Content.ReadAsStringAsync().Result; + } + + private static void AppendToTaskLog(HttpClient httpClient, string authToken, string planUri, string projectId, string hubName, string planId, string taskLogId, string logFilePath) + { + // Append to task log + // url: {planUri}/{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/logs/{taskLogId}?api-version=4.1 + // body: log messages stream data + + const string AppendLogContentUrl = "{0}/{1}/_apis/distributedtask/hubs/{2}/plans/{3}/logs/{4}?api-version=4.1"; + + var appendTaskLogUrl = string.Format(AppendLogContentUrl, planUri, projectId, hubName, planId, taskLogId); + + using (var fs = File.Open(logFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + HttpContent content = new StreamContent(fs); + + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String( + System.Text.ASCIIEncoding.ASCII.GetBytes( + string.Format("{0}:{1}", "", authToken)))); + + var returnData = httpClient.PostAsync(new Uri(appendTaskLogUrl), content).Result; + } + } + + private static void UpdateTaskTimelineRecord(HttpClient httpClient, string authToken, string planUri, string projectId, string hubName, string planId, string timelineId, string taskInstanceId, string taskLogObject) + { + // Update timeline record + // url: {planUri}/{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/timelines/{timelineId}/records?api-version=4.1 + // body: {"value":[timelineRecords],"count":1} + // timelineRecord : {"id": taskInstanceId, "log": taskLogObject} + + const string UpdateTimelineUrl = "{0}/{1}/_apis/distributedtask/hubs/{2}/plans/{3}/timelines/{4}/records?api-version=4.1"; + + var updateTaskTimelineRecordUrl = string.Format(UpdateTimelineUrl, planUri, projectId, hubName, planId, timelineId); + + var timelineRecord = new JObject(new JProperty("Id", taskInstanceId)); + timelineRecord.Add(new JProperty("Log", taskLogObject)); + + JArray timelineRecords = new JArray(); + timelineRecords.Add(timelineRecord); + var requestBodyJObject = new JObject(); + requestBodyJObject["value"] = timelineRecords; + requestBodyJObject.Add(new JProperty("count", 1)); + + var requestBody = JsonConvert.SerializeObject(requestBodyJObject); + + PatchData(httpClient, updateTaskTimelineRecordUrl, requestBody, authToken); + } + + + private static HttpResponseMessage PostData(HttpClient httpClient, string url, string requestBody, string authToken) + { + var buffer = System.Text.Encoding.UTF8.GetBytes(requestBody); + var byteContent = new ByteArrayContent(buffer); + byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String( + System.Text.ASCIIEncoding.ASCII.GetBytes( + string.Format("{0}:{1}", "", authToken)))); + + return httpClient.PostAsync(new Uri(url), byteContent).Result; + } + + private static HttpResponseMessage PatchData(HttpClient httpClient, string url, string requestBody, string authToken) + { + var buffer = System.Text.Encoding.UTF8.GetBytes(requestBody); + var byteContent = new ByteArrayContent(buffer); + byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String( + System.Text.ASCIIEncoding.ASCII.GetBytes( + string.Format("{0}:{1}", "", authToken)))); + + return httpClient.PatchAsync(new Uri(url), byteContent).Result; + } + } +} diff --git a/ServerTaskHelper/HttpRequestSampleWithoutHandler/MyTaskController.cs b/ServerTaskHelper/HttpRequestSampleWithoutHandler/MyTaskController.cs new file mode 100644 index 000000000..adf50356a --- /dev/null +++ b/ServerTaskHelper/HttpRequestSampleWithoutHandler/MyTaskController.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using HttpRequestSampleWithoutHandler; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace HttpRequestHandler +{ + [Route("api/[controller]")] + public class MyTaskController : Controller + { + // GET api/MyTask + [HttpGet] + public IEnumerable Get() + { + return new string[] { "This is the sample task", "Use the post method in your release task to see this example work end-to-end." }; + } + + // POST api/MyTask + [HttpPost] + public void Execute() + { + // Read task body + string taskMessageBody; + using (var receiveStream = this.Request.Body) + { + using (var sr = new StreamReader(receiveStream, Encoding.UTF8)) + { + taskMessageBody = sr.ReadToEnd(); + } + } + + string projectId = this.Request.Headers["ProjectId"]; + string planId = this.Request.Headers["PlanId"]; + string jobId = this.Request.Headers["JobId"]; + string timelineId = this.Request.Headers["TimelineId"]; + string taskInstanceId = this.Request.Headers["TaskInstanceId"]; + string hubName = this.Request.Headers["HubName"]; + string taskInstanceName = this.Request.Headers["TaskInstanceName"]; + string planUrl = this.Request.Headers["PlanUrl"]; + string authToken = this.Request.Headers["AuthToken"]; + + // Ensure projectId, planId, jobId, timelineId, taskInstanceId are proper guids. + + // Handover time consuming work to another task. Completion event should be set to "Callback" in pipeline task. + Task.Run(() => MyApp.ExecuteAsync(taskMessageBody, projectId, planUrl, hubName, planId, jobId, timelineId, taskInstanceId, taskInstanceName, authToken)); + } + } +} diff --git a/ServerTaskHelper/HttpRequestSampleWithoutHandler/Properties/launchSettings.json b/ServerTaskHelper/HttpRequestSampleWithoutHandler/Properties/launchSettings.json new file mode 100644 index 000000000..267940dc0 --- /dev/null +++ b/ServerTaskHelper/HttpRequestSampleWithoutHandler/Properties/launchSettings.json @@ -0,0 +1,20 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:57636/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/mytask", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/ServerTaskHelper/ServerTaskHelper.sln b/ServerTaskHelper/ServerTaskHelper.sln index 2c2680c61..20894153a 100644 --- a/ServerTaskHelper/ServerTaskHelper.sln +++ b/ServerTaskHelper/ServerTaskHelper.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureFunctionHandler", "Azu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionSample", "AzureFunctionSample\AzureFunctionSample.csproj", "{69832419-26C2-4F3E-BB38-DE45CFE33BD9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpRequestSampleWithoutHandler", "HttpRequestSampleWithoutHandler\HttpRequestSampleWithoutHandler.csproj", "{FDD4AE8E-63DE-4C46-BBE1-C8599CC8F155}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {69832419-26C2-4F3E-BB38-DE45CFE33BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU {69832419-26C2-4F3E-BB38-DE45CFE33BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU {69832419-26C2-4F3E-BB38-DE45CFE33BD9}.Release|Any CPU.Build.0 = Release|Any CPU + {FDD4AE8E-63DE-4C46-BBE1-C8599CC8F155}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDD4AE8E-63DE-4C46-BBE1-C8599CC8F155}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDD4AE8E-63DE-4C46-BBE1-C8599CC8F155}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDD4AE8E-63DE-4C46-BBE1-C8599CC8F155}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE