diff --git a/src/AzureExtension/DevBox/DevBoxInstance.cs b/src/AzureExtension/DevBox/DevBoxInstance.cs index 92eed76c..0df0cf1b 100644 --- a/src/AzureExtension/DevBox/DevBoxInstance.cs +++ b/src/AzureExtension/DevBox/DevBoxInstance.cs @@ -630,8 +630,7 @@ public IAsyncOperation> GetComputeSystemPrope public IApplyConfigurationOperation CreateApplyConfigurationOperation(string configuration) { - var taskAPI = $"{DevBoxState.Uri}{Constants.CustomizationAPI}{DateTime.Now.ToFileTimeUtc()}?{Constants.APIVersion}"; - return new WingetConfigWrapper(configuration, taskAPI, _devBoxManagementService, AssociatedDeveloperId, _log, GetState()); + return new WingetConfigWrapper(configuration, DevBoxState.Uri, _devBoxManagementService, AssociatedDeveloperId, _log, GetState(), ConnectAsync); } // Unsupported operations diff --git a/src/AzureExtension/DevBox/DevBoxProvider.cs b/src/AzureExtension/DevBox/DevBoxProvider.cs index 684b4add..b1579e74 100644 --- a/src/AzureExtension/DevBox/DevBoxProvider.cs +++ b/src/AzureExtension/DevBox/DevBoxProvider.cs @@ -159,11 +159,12 @@ public IAsyncOperation GetComputeSystemsAsync(IDeveloperId ArgumentNullException.ThrowIfNull(developerId); ArgumentNullException.ThrowIfNullOrEmpty(developerId.LoginId); - _log.Information($"Attempting to retrieving all Dev Boxes for {developerId.LoginId}, at {DateTime.Now}"); + var start = DateTime.Now; + _log.Information($"Attempting to retrieving all Dev Boxes for {developerId.LoginId}"); var computeSystems = await GetDevBoxesAsync(developerId); - _log.Information($"Successfully retrieved all Dev Boxes for {developerId.LoginId}, at {DateTime.Now}"); + _log.Information($"Successfully retrieved all Dev Boxes for {developerId.LoginId}, in {DateTime.Now - start}"); return new ComputeSystemsResult(computeSystems); } catch (Exception ex) diff --git a/src/AzureExtension/DevBox/Helpers/AdaptiveCardJSONToCSClass.cs b/src/AzureExtension/DevBox/Helpers/AdaptiveCardJSONToCSClass.cs new file mode 100644 index 00000000..52169a12 --- /dev/null +++ b/src/AzureExtension/DevBox/Helpers/AdaptiveCardJSONToCSClass.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AzureExtension.DevBox.Helpers; + +public class AdaptiveCardJSONToCSClass +{ + public string? Data + { + get; set; + } + + public string? Id + { + get; set; + } + + public string? Title + { + get; set; + } + + public string? Type + { + get; set; + } +} diff --git a/src/AzureExtension/DevBox/Helpers/WaitingForUserAdaptiveCardSession.cs b/src/AzureExtension/DevBox/Helpers/WaitingForUserAdaptiveCardSession.cs index 61218a8e..b8781c71 100644 --- a/src/AzureExtension/DevBox/Helpers/WaitingForUserAdaptiveCardSession.cs +++ b/src/AzureExtension/DevBox/Helpers/WaitingForUserAdaptiveCardSession.cs @@ -3,9 +3,11 @@ using System.Drawing; using System.Text; +using System.Text.Json; using System.Text.Json.Nodes; using DevHomeAzureExtension.Helpers; using Microsoft.Windows.DevHome.SDK; +using Serilog; using Windows.Foundation; namespace AzureExtension.DevBox.Helpers; @@ -16,11 +18,28 @@ public class WaitingForUserAdaptiveCardSession : IExtensionAdaptiveCardSession2, private string? _template; - private ManualResetEvent _resumeEvent; + private ManualResetEvent _resumeEvent; + + private ManualResetEvent _launchEvent; + + private const string _waitingForUserSessionState = "WaitingForUserSession"; + + private const string _launchAction = "launchAction"; + + private const string _resumeAction = "resumeAction"; + + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(WaitingForUserAdaptiveCardSession)); + + private JsonSerializerOptions _serializerOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; - public WaitingForUserAdaptiveCardSession(ManualResetEvent resumeEvent) + public WaitingForUserAdaptiveCardSession(ManualResetEvent resumeEvent, ManualResetEvent launchEvent) { - this._resumeEvent = resumeEvent; + _resumeEvent = resumeEvent; + _launchEvent = launchEvent; } public event TypedEventHandler Stopped = (s, e) => { }; @@ -39,6 +58,7 @@ public ProviderOperationResult Initialize(IExtensionAdaptiveCard extensionUI) { "icon", ConvertIconToDataString("Caution.png") }, { "loginRequiredText", Resources.GetResource("DevBox_AdaptiveCard_Text") }, { "loginRequiredDescriptionText", Resources.GetResource("DevBox_AdaptiveCard_InnerDescription") }, + { "LaunchText", Resources.GetResource("DevBox_AdaptiveCard_LaunchText") }, { "ResumeText", Resources.GetResource("DevBox_AdaptiveCard_ResumeText") }, }; @@ -70,21 +90,43 @@ private static string ConvertIconToDataString(string fileName) public IAsyncOperation OnAction(string action, string inputs) { return Task.Run(() => - { - var state = _extensionAdaptiveCard?.State; - ProviderOperationResult operationResult; - if (state == "WaitingForUserSession") - { - operationResult = new ProviderOperationResult(ProviderOperationStatus.Success, null, null, null); - _resumeEvent.Set(); - Stopped?.Invoke(this, new(operationResult, string.Empty)); - } - else - { - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Something went wrong", $"Unexpected state:{_extensionAdaptiveCard?.State}"); - } + { + try + { + var state = _extensionAdaptiveCard?.State; + ProviderOperationResult operationResult; + var data = JsonSerializer.Deserialize(action, _serializerOptions); + if (state != null && state.Equals(_waitingForUserSessionState, StringComparison.OrdinalIgnoreCase) && data != null) + { + switch (data.Id) + { + case _launchAction: + operationResult = new ProviderOperationResult(ProviderOperationStatus.Success, null, null, null); + _launchEvent.Set(); + _resumeEvent.Set(); + break; + case _resumeAction: + operationResult = new ProviderOperationResult(ProviderOperationStatus.Success, null, null, null); + _resumeEvent.Set(); + break; + default: + throw new InvalidOperationException($"Unexpected action:{data.Id}"); + } - return operationResult; + Stopped.Invoke(this, new(operationResult, string.Empty)); + } + else + { + throw new InvalidOperationException($"Unexpected state:{state} or Parsing Error"); + } + + return operationResult; + } + catch (Exception ex) + { + _log.Error(ex, "Error occurred while processing the action"); + return new ProviderOperationResult(ProviderOperationStatus.Failure, null, ex.Message, ex.StackTrace); + } }).AsAsyncOperation(); } } diff --git a/src/AzureExtension/DevBox/Helpers/WingetConfigWrapper.cs b/src/AzureExtension/DevBox/Helpers/WingetConfigWrapper.cs index 4ed5ed40..aa72912f 100644 --- a/src/AzureExtension/DevBox/Helpers/WingetConfigWrapper.cs +++ b/src/AzureExtension/DevBox/Helpers/WingetConfigWrapper.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using AzureExtension.Contracts; +using AzureExtension.DevBox.DevBoxJsonToCsClasses; using AzureExtension.DevBox.Exceptions; using DevHomeAzureExtension.Helpers; using Microsoft.Windows.DevHome.SDK; @@ -63,7 +64,9 @@ public class WingetConfigWrapper : IApplyConfigurationOperation, IDisposable private ApplyConfigurationSetResult _applyConfigurationSetResult = new(null, null); - private string _restAPI; + private string _taskAPI; + + private string _baseAPI; private IDevBoxManagementService _managementService; @@ -75,10 +78,16 @@ public class WingetConfigWrapper : IApplyConfigurationOperation, IDisposable private bool _pendingNotificationShown; + private ManualResetEvent _launchEvent = new(false); + private ManualResetEvent _resumeEvent = new(false); private ComputeSystemState _computeSystemState; + private Func> _connectAsync; + + private bool _alreadyUpdatedUI; + // Using a common failure result for all the tasks // since we don't get any other information from the REST API private ConfigurationUnitResultInformation _commonfailureResult = new ConfigurationUnitResultInformation( @@ -86,24 +95,22 @@ public class WingetConfigWrapper : IApplyConfigurationOperation, IDisposable public WingetConfigWrapper( string configuration, - string taskAPI, + string baseAPI, IDevBoxManagementService devBoxManagementService, IDeveloperId associatedDeveloperId, Serilog.ILogger log, - ComputeSystemState computeSystemState) + ComputeSystemState computeSystemState, + Func> connectAsync) { - _restAPI = taskAPI; + _baseAPI = baseAPI; + _taskAPI = $"{_baseAPI}{Constants.CustomizationAPI}{DateTime.Now.ToFileTimeUtc()}?{Constants.APIVersion}"; _managementService = devBoxManagementService; _devId = associatedDeveloperId; _log = log; _computeSystemState = computeSystemState; + _connectAsync = connectAsync; - // If the dev box isn't running, skip initialization - // Later this logic will be changed to start the dev box - if (_computeSystemState == ComputeSystemState.Running) - { - Initialize(configuration); - } + Initialize(configuration); } public void Initialize(string configuration) @@ -239,7 +246,7 @@ private void SetStateForCustomizationTask(TaskJSONToCSClasses.BaseClass response Thread.Sleep(TimeSpan.FromSeconds(15)); // Make the log API string : Remove the API version from the URI and add 'logs' - var logURI = _restAPI.Substring(0, _restAPI.LastIndexOf('?')); + var logURI = _taskAPI.Substring(0, _taskAPI.LastIndexOf('?')); var id = response.Tasks[i].Id; logURI += $"/logs/{id}?{Constants.APIVersion}"; @@ -272,14 +279,37 @@ private void SetStateForCustomizationTask(TaskJSONToCSClasses.BaseClass response } // If waiting for user session and no task is running, show the adaptive card - // We add a wait since Dev Boxes take a little over 2 minutes to start applying - // the configuration and we don't want to show the same message immediately after. + // We add a wait to give Dev Box time to start the task + // But if this is the first login, Dev Box Agent needs to configure itself + // So an extra wait might be needed. if (isWaitingForUserSession && !isAnyTaskRunning) { - ApplyConfigurationActionRequiredEventArgs eventArgs = new(new WaitingForUserAdaptiveCardSession(_resumeEvent)); - ActionRequired?.Invoke(this, eventArgs); - WaitHandle.WaitAny(new[] { _resumeEvent }); - Thread.Sleep(TimeSpan.FromSeconds(135)); + if (_alreadyUpdatedUI) + { + Thread.Sleep(TimeSpan.FromSeconds(100)); + _alreadyUpdatedUI = false; + } + else + { + ApplyConfigurationActionRequiredEventArgs eventArgs = new(new WaitingForUserAdaptiveCardSession(_resumeEvent, _launchEvent)); + ActionRequired?.Invoke(this, eventArgs); + WaitHandle.WaitAny(new[] { _resumeEvent }); + + // Check if the launch event is also set + // If it is, wait for a few seconds longer to account for it + if (_launchEvent.WaitOne(0)) + { + _log.Information("Launching the dev box"); + _connectAsync(string.Empty).GetAwaiter().GetResult(); + Thread.Sleep(TimeSpan.FromSeconds(30)); + _launchEvent.Reset(); + } + + Thread.Sleep(TimeSpan.FromSeconds(20)); + _alreadyUpdatedUI = true; + } + + _resumeEvent.Reset(); } break; @@ -306,19 +336,25 @@ IAsyncOperation IApplyConfigurationOperation.StartAsyn { if (_computeSystemState != ComputeSystemState.Running) { - throw new InvalidOperationException(Resources.GetResource(NotRunningFailedKey)); + // Check if the dev box might have been started in the meantime + var stateRequest = await _managementService.HttpsRequestToDataPlane(new Uri(_baseAPI), _devId, HttpMethod.Get, null); + var state = JsonSerializer.Deserialize(stateRequest.JsonResponseRoot.ToString(), Constants.JsonOptions)!; + if (state.PowerState != Constants.DevBoxPowerStates.Running) + { + throw new InvalidOperationException(Resources.GetResource(NotRunningFailedKey)); + } } _log.Information($"Applying config {_fullTaskJSON}"); HttpContent httpContent = new StringContent(_fullTaskJSON, Encoding.UTF8, "application/json"); - var result = await _managementService.HttpsRequestToDataPlane(new Uri(_restAPI), _devId, HttpMethod.Put, httpContent); + var result = await _managementService.HttpsRequestToDataPlane(new Uri(_taskAPI), _devId, HttpMethod.Put, httpContent); var setStatus = string.Empty; while (setStatus != "Succeeded" && setStatus != "Failed" && setStatus != "ValidationFailed") { await Task.Delay(TimeSpan.FromSeconds(15)); - var poll = await _managementService.HttpsRequestToDataPlane(new Uri(_restAPI), _devId, HttpMethod.Get, null); + var poll = await _managementService.HttpsRequestToDataPlane(new Uri(_taskAPI), _devId, HttpMethod.Get, null); var rawResponse = poll.JsonResponseRoot.ToString(); var response = JsonSerializer.Deserialize(rawResponse, _taskJsonSerializerOptions); setStatus = response?.Status; diff --git a/src/AzureExtension/DevBox/Templates/WaitingForUserSessionTemplate.json b/src/AzureExtension/DevBox/Templates/WaitingForUserSessionTemplate.json index c86c5b2e..2bc30500 100644 --- a/src/AzureExtension/DevBox/Templates/WaitingForUserSessionTemplate.json +++ b/src/AzureExtension/DevBox/Templates/WaitingForUserSessionTemplate.json @@ -70,13 +70,15 @@ } ], "actions": [ + { + "type": "Action.Execute", + "title": "${LaunchText}", + "id": "launchAction" + }, { "type": "Action.Execute", "title": "${ResumeText}", - "id": "resumeAction", - "data": { - "id": "resumeAction" - } + "id": "resumeAction" } ] } \ No newline at end of file diff --git a/src/AzureExtension/Strings/en-US/Resources.resw b/src/AzureExtension/Strings/en-US/Resources.resw index 22070eb4..5a4cc9f6 100644 --- a/src/AzureExtension/Strings/en-US/Resources.resw +++ b/src/AzureExtension/Strings/en-US/Resources.resw @@ -699,4 +699,8 @@ Please check logs at C:\ProgramData\Microsoft\DevBoxAgent\Logs Shown for errors in config flow for Dev Boxes which don't have additional info + + Launch + Text shown in the button to launch Dev Box. + \ No newline at end of file