Skip to content

Commit

Permalink
Add launch button, reduce wait, and better checking for status of Dev…
Browse files Browse the repository at this point in the history
… Boxes for config flow (#195)

* Added status recheck

* Added shorter wait for user session

* Added timer for retrival

* Add launch button, remove check, add reset

* PR comment changes

---------

Co-authored-by: Huzaifa Danish <modanish@microsoft.com>
  • Loading branch information
huzaifa-d and huzaifa-msft authored May 31, 2024
1 parent 52d7e7e commit 9f3aa02
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 45 deletions.
3 changes: 1 addition & 2 deletions src/AzureExtension/DevBox/DevBoxInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -630,8 +630,7 @@ public IAsyncOperation<IEnumerable<ComputeSystemProperty>> 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
Expand Down
5 changes: 3 additions & 2 deletions src/AzureExtension/DevBox/DevBoxProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,12 @@ public IAsyncOperation<ComputeSystemsResult> 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)
Expand Down
27 changes: 27 additions & 0 deletions src/AzureExtension/DevBox/Helpers/AdaptiveCardJSONToCSClass.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<IExtensionAdaptiveCardSession2, ExtensionAdaptiveCardSessionStoppedEventArgs> Stopped = (s, e) => { };
Expand All @@ -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") },
};

Expand Down Expand Up @@ -70,21 +90,43 @@ private static string ConvertIconToDataString(string fileName)
public IAsyncOperation<ProviderOperationResult> 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<AdaptiveCardJSONToCSClass>(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();
}
}
76 changes: 56 additions & 20 deletions src/AzureExtension/DevBox/Helpers/WingetConfigWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -75,35 +78,39 @@ public class WingetConfigWrapper : IApplyConfigurationOperation, IDisposable

private bool _pendingNotificationShown;

private ManualResetEvent _launchEvent = new(false);

private ManualResetEvent _resumeEvent = new(false);

private ComputeSystemState _computeSystemState;

private Func<string, IAsyncOperation<ComputeSystemOperationResult>> _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(
new WingetConfigurationException("Runtime Failure"), string.Empty, string.Empty, ConfigurationUnitResultSource.UnitProcessing);

public WingetConfigWrapper(
string configuration,
string taskAPI,
string baseAPI,
IDevBoxManagementService devBoxManagementService,
IDeveloperId associatedDeveloperId,
Serilog.ILogger log,
ComputeSystemState computeSystemState)
ComputeSystemState computeSystemState,
Func<string, IAsyncOperation<ComputeSystemOperationResult>> 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)
Expand Down Expand Up @@ -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}";

Expand Down Expand Up @@ -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;
Expand All @@ -306,19 +336,25 @@ IAsyncOperation<ApplyConfigurationResult> 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<DevBoxMachineState>(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<TaskJSONToCSClasses.BaseClass>(rawResponse, _taskJsonSerializerOptions);
setStatus = response?.Status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,15 @@
}
],
"actions": [
{
"type": "Action.Execute",
"title": "${LaunchText}",
"id": "launchAction"
},
{
"type": "Action.Execute",
"title": "${ResumeText}",
"id": "resumeAction",
"data": {
"id": "resumeAction"
}
"id": "resumeAction"
}
]
}
4 changes: 4 additions & 0 deletions src/AzureExtension/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -699,4 +699,8 @@
<value>Please check logs at C:\ProgramData\Microsoft\DevBoxAgent\Logs</value>
<comment>Shown for errors in config flow for Dev Boxes which don't have additional info</comment>
</data>
<data name="DevBox_AdaptiveCard_LaunchText" xml:space="preserve">
<value>Launch</value>
<comment>Text shown in the button to launch Dev Box.</comment>
</data>
</root>

0 comments on commit 9f3aa02

Please sign in to comment.