Skip to content

Commit 6cfa0c3

Browse files
committed
work in progress apphost debug
1 parent 0663e43 commit 6cfa0c3

17 files changed

+296
-113
lines changed

extension/src/server/interactionService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { yesLabel, noLabel, directLink, codespacesLink, openAspireDashboard, fai
55
import { ICliRpcClient } from './rpcClient';
66
import { formatText } from '../utils/strings';
77
import { IOutputChannelWriter } from '../utils/logging';
8+
import { runNewSession, RunSessionRequest } from '../dcp/processRunner';
89

910
export interface IInteractionService {
1011
showStatus: (statusText: string | null) => void;
@@ -22,6 +23,7 @@ export interface IInteractionService {
2223
displayCancellationMessage: (message: string) => void;
2324
openProject: (projectPath: string) => void;
2425
logMessage: (logLevel: string, message: string) => void;
26+
runSession: (request: RunSessionRequest) => Promise<boolean>;
2527
}
2628

2729
type DashboardUrls = {
@@ -226,6 +228,11 @@ export class InteractionService implements IInteractionService {
226228
this._outputChannelWriter.appendLine('cli', `[${logLevel}] ${formatText(message)}`);
227229
}
228230

231+
runSession(request: RunSessionRequest): Promise<boolean> {
232+
this._outputChannelWriter.appendLine('interaction', `Running session with request: ${JSON.stringify(request)}`);
233+
return runNewSession(request);
234+
}
235+
229236
clearStatusBar() {
230237
if (this._statusBarItem) {
231238
this._statusBarItem.hide();
@@ -251,4 +258,5 @@ export function addInteractionServiceEndpoints(connection: MessageConnection, in
251258
connection.onRequest("displayCancellationMessage", withAuthentication(interactionService.displayCancellationMessage.bind(interactionService)));
252259
connection.onRequest("openProject", withAuthentication(interactionService.openProject.bind(interactionService)));
253260
connection.onRequest("logMessage", withAuthentication(interactionService.logMessage.bind(interactionService)));
261+
connection.onRequest("runSession", withAuthentication(interactionService.runSession.bind(interactionService)));
254262
}

extension/src/server/rpcClient.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { logAsyncOperation } from '../utils/logging';
44
export interface ICliRpcClient {
55
getCliVersion(): Promise<string>;
66
validatePromptInputString(input: string): Promise<ValidationResult | null>;
7+
processExited(id: string, exitCode: number): Promise<void>;
78
}
89

910
export type ValidationResult = {
@@ -44,4 +45,19 @@ export class RpcClient implements ICliRpcClient {
4445
}
4546
);
4647
}
48+
49+
processExited(id: string, exitCode: number): Promise<void> {
50+
return logAsyncOperation(
51+
"interaction",
52+
`Notifying that process has exited`,
53+
() => `Process exit notification sent successfully`,
54+
async () => {
55+
return await this._messageConnection.sendRequest<void>('processExited', {
56+
token: this._token,
57+
id,
58+
exitCode
59+
});
60+
}
61+
);
62+
}
4763
}

extension/src/test/rpc/interactionServiceTests.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ class TestOutputChannelWriter implements IOutputChannelWriter {
212212
}
213213

214214
class TestCliRpcClient implements ICliRpcClient {
215+
processExited(id: string, exitCode: number): Promise<void> {
216+
throw new Error('Method not implemented.');
217+
}
215218
getCliVersion(): Promise<string> {
216219
return Promise.resolve('1.0.0');
217220
}

src/Aspire.Cli/Backchannel/BackchannelDataTypes.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Text.Json.Serialization;
5+
46
namespace Aspire.Cli.Backchannel;
57

68
/// <summary>
@@ -45,3 +47,18 @@ internal enum InputType
4547
/// </summary>
4648
Number
4749
}
50+
51+
internal class RunSessionRequest
52+
{
53+
[JsonPropertyName("id")]
54+
public required string Id { get; init; }
55+
56+
[JsonPropertyName("launch_configurations")]
57+
public required ProjectLaunchConfiguration[] LaunchConfigurations { get; init; }
58+
59+
[JsonPropertyName("env")]
60+
public required EnvVar[]? Env { get; init; }
61+
62+
[JsonPropertyName("args")]
63+
public required string[] Args { get; init; }
64+
}

src/Aspire.Cli/Backchannel/BackchannelJsonSerializerContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace Aspire.Cli.Backchannel;
2424
[JsonSerializable(typeof(RequestId))]
2525
[JsonSerializable(typeof(IEnumerable<DisplayLineState>))]
2626
[JsonSerializable(typeof(ValidationResult))]
27+
[JsonSerializable(typeof(RunSessionRequest))]
2728
internal partial class BackchannelJsonSerializerContext : JsonSerializerContext
2829
{
2930
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Using the Json source generator.")]

src/Aspire.Cli/Backchannel/ExtensionBackchannel.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,23 @@ internal interface IExtensionBackchannel
3737
Task OpenProjectAsync(string projectPath, CancellationToken cancellationToken);
3838
Task LogMessageAsync(LogLevel logLevel, string message, CancellationToken cancellationToken);
3939
Task<IReadOnlyList<VersionedCapability>> GetCapabilitiesAsync(CancellationToken cancellationToken);
40+
bool TryGetProcessExitCode(string id, [NotNullWhen(true)] out int? exitCode);
41+
Task<bool> RunExecutableAsync(RunSessionRequest runSessionRequest, CancellationToken cancellationToken);
4042
}
4143

4244
internal record VersionedCapability(string Name, Version Version)
4345
{
46+
public bool IsCompatible(VersionedCapability other)
47+
{
48+
if (Name != other.Name)
49+
{
50+
return false;
51+
}
52+
53+
// If the version is the same or higher, it is compatible.
54+
return Version.CompareTo(other.Version) >= 0;
55+
}
56+
4457
public override string ToString()
4558
{
4659
return $"{Name}.v{Version}";
@@ -238,7 +251,7 @@ static void AddLocalRpcTarget(JsonRpc rpc, ExtensionRpcTarget target)
238251
Capabilities.Add(new VersionedCapability(capabilityName, version));
239252
}
240253

241-
if (!Capabilities.Any(capability => capability.Name == s_baselineCapability.Name && capability.Version.CompareTo(s_baselineCapability.Version) <= 0))
254+
if (!Capabilities.Any(capability => capability.Name == s_baselineCapability.Name && s_baselineCapability.IsCompatible(capability)))
242255
{
243256
throw new ExtensionIncompatibleException(
244257
string.Format(CultureInfo.CurrentCulture, ErrorStrings.ExtensionIncompatibleWithCli, s_baselineCapability.ToString()),
@@ -534,6 +547,36 @@ public async Task<IReadOnlyList<VersionedCapability>> GetCapabilitiesAsync(Cance
534547
return Capabilities;
535548
}
536549

550+
public bool TryGetProcessExitCode(string id, [NotNullWhen(true)] out int? exitCode)
551+
{
552+
if (target.ProcessExitCodes.TryGetValue(id, out var code))
553+
{
554+
exitCode = code;
555+
return true;
556+
}
557+
558+
exitCode = null;
559+
return false;
560+
}
561+
562+
public async Task<bool> RunExecutableAsync(RunSessionRequest runSessionRequest, CancellationToken cancellationToken)
563+
{
564+
await ConnectAsync(cancellationToken);
565+
566+
using var activity = _activitySource.StartActivity();
567+
568+
var rpc = await _rpcTaskCompletionSource.Task;
569+
570+
logger.LogDebug("Sending executable run session request: {RequestId}", runSessionRequest.Id);
571+
572+
var result = await rpc.InvokeWithCancellationAsync<bool>(
573+
"runSession",
574+
[_token, runSessionRequest],
575+
cancellationToken);
576+
577+
return result;
578+
}
579+
537580
private X509Certificate2 GetCertificate()
538581
{
539582
var serverCertificate = configuration[KnownConfigNames.ExtensionCert];
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire.Cli.Backchannel;
5+
6+
internal static class ExtensionCapabilities
7+
{
8+
public static readonly VersionedCapability CSharpRunner = new("csharp", new Version(1, 0));
9+
}

src/Aspire.Cli/Backchannel/ExtensionRpcTarget.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ internal interface IExtensionRpcTarget
1919
internal class ExtensionRpcTarget(IConfiguration configuration) : IExtensionRpcTarget
2020
{
2121
public Func<string, ValidationResult>? ValidationFunction { get; set; }
22+
public Dictionary<string, int> ProcessExitCodes { get; } = [];
23+
24+
[JsonRpcMethod("processExited")]
25+
public Task ProcessExitedAsync(string id, int exitCode)
26+
{
27+
if (string.IsNullOrEmpty(id) || exitCode < 0)
28+
{
29+
return Task.CompletedTask;
30+
}
31+
32+
ProcessExitCodes[id] = exitCode;
33+
return Task.CompletedTask;
34+
}
2235

2336
[JsonRpcMethod("getCliVersion")]
2437
public Task<string> GetCliVersionAsync(string token)
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics;
5-
64
namespace Aspire.Cli.Backchannel;
75

8-
internal sealed class FailedToConnectBackchannelConnection(string message, Process process, Exception innerException) : Exception(message, innerException)
9-
{
10-
public Process Process => process;
11-
}
6+
internal sealed class FailedToConnectBackchannelConnection(string message, Exception innerException) : Exception(message, innerException);

src/Aspire.Cli/Configuration/ConfigurationService.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public async Task<bool> DeleteConfigurationAsync(string key, bool isGlobal = fal
6363

6464
// Delete using dot notation support and return whether deletion occurred
6565
var deleted = DeleteNestedValue(settings, key);
66-
66+
6767
if (deleted)
6868
{
6969
// Write the updated settings
@@ -79,7 +79,7 @@ public async Task<bool> DeleteConfigurationAsync(string key, bool isGlobal = fal
7979
}
8080
}
8181

82-
private string GetSettingsFilePath(bool isGlobal)
82+
public string GetSettingsFilePath(bool isGlobal)
8383
{
8484
if (isGlobal)
8585
{
@@ -129,7 +129,7 @@ private static async Task LoadConfigurationFromFileAsync(string filePath, Dictio
129129
{
130130
var content = await File.ReadAllTextAsync(filePath, cancellationToken);
131131
var settings = JsonNode.Parse(content)?.AsObject();
132-
132+
133133
if (settings is not null)
134134
{
135135
FlattenJsonObject(settings, config, string.Empty);
@@ -154,13 +154,13 @@ private static void SetNestedValue(JsonObject settings, string key, string value
154154
for (int i = 0; i < keyParts.Length - 1; i++)
155155
{
156156
var part = keyParts[i];
157-
157+
158158
// If the property doesn't exist or isn't an object, replace it with a new object
159159
if (!currentObject.ContainsKey(part) || currentObject[part] is not JsonObject)
160160
{
161161
currentObject[part] = new JsonObject();
162162
}
163-
163+
164164
currentObject = currentObject[part]!.AsObject();
165165
}
166166

@@ -184,17 +184,17 @@ private static bool DeleteNestedValue(JsonObject settings, string key)
184184
{
185185
var part = keyParts[i];
186186
objectPath.Add((currentObject, part));
187-
187+
188188
if (!currentObject.ContainsKey(part) || currentObject[part] is not JsonObject)
189189
{
190190
return false; // Path doesn't exist
191191
}
192-
192+
193193
currentObject = currentObject[part]!.AsObject();
194194
}
195195

196196
var finalKey = keyParts[keyParts.Length - 1];
197-
197+
198198
// Check if the final key exists
199199
if (!currentObject.ContainsKey(finalKey))
200200
{
@@ -208,7 +208,7 @@ private static bool DeleteNestedValue(JsonObject settings, string key)
208208
for (int i = objectPath.Count - 1; i >= 0; i--)
209209
{
210210
var (parentObject, parentKey) = objectPath[i];
211-
211+
212212
// If the current object is empty, remove it from its parent
213213
if (currentObject.Count == 0)
214214
{
@@ -232,7 +232,7 @@ private static void FlattenJsonObject(JsonObject obj, Dictionary<string, string>
232232
foreach (var kvp in obj)
233233
{
234234
var key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}";
235-
235+
236236
if (kvp.Value is JsonObject nestedObj)
237237
{
238238
FlattenJsonObject(nestedObj, result, key);

0 commit comments

Comments
 (0)