Repository navigation
Add process-backed resource commands #16923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
David Fowler (davidfowl)
merged 7 commits into
main
from
davidfowl/with-process-command-helper
May 11, 2026
+4,240
−118
Merged
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0bc31db
Add process-backed resource commands
davidfowl 6b2e3b5
Document process command examples
davidfowl b7fb698
Address process command review feedback
davidfowl 92995e0
Fix YARP polyglot validation AppHosts
davidfowl 29e79e5
Use map for process command environment variables
davidfowl ef5b252
Address process command result feedback
davidfowl d0a05af
Harden process command path lookup
davidfowl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
18 changes: 18 additions & 0 deletions
18
playground/Stress/Stress.AppHost/process-command-scripts/backup-restore.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| const fs = require("node:fs"); | ||
|
|
||
| const [backupPath = "sample-backup.dump", containerName = "db-server", databaseName = "mainDb"] = process.argv.slice(2); | ||
|
|
||
| console.log("scenario=#12906/#7786 backup-restore"); | ||
| console.log(`backup-path=${backupPath}`); | ||
| console.log(`container=${containerName}`); | ||
| console.log(`database=${databaseName}`); | ||
|
|
||
| if (!fs.existsSync(backupPath)) { | ||
| console.error(`backup-missing=${backupPath}`); | ||
| process.exit(2); | ||
| } | ||
|
|
||
| const backupSize = fs.statSync(backupPath).size; | ||
| console.log(`backup-size-bytes=${backupSize}`); | ||
| console.log(`docker-cp-shape=docker cp "${backupPath}" "${containerName}:/tmp/restore.dump"`); | ||
| console.log(`docker-exec-shape=docker exec "${containerName}" pg_restore -U postgres -d "${databaseName}" /tmp/restore.dump`); |
12 changes: 12 additions & 0 deletions
12
playground/Stress/Stress.AppHost/process-command-scripts/container-exec.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| const [containerName = "db-server", command = "pg_isready -U postgres", workingDirectory = "/"] = process.argv.slice(2); | ||
|
|
||
| console.log("scenario=#10301 container-exec"); | ||
| console.log(`container=${containerName}`); | ||
| console.log(`working-directory=${workingDirectory}`); | ||
| console.log(`command=${command}`); | ||
| console.log(`docker-exec-shape=docker exec -w "${workingDirectory}" "${containerName}" ${command}`); | ||
|
|
||
| if (command.includes("fail")) { | ||
| console.error("simulated-container-exec-failure"); | ||
| process.exit(3); | ||
| } |
1 change: 1 addition & 0 deletions
1
playground/Stress/Stress.AppHost/process-command-scripts/environment.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| console.log(`env=${process.env.PROCESS_COMMAND_SAMPLE ?? ""}`); |
3 changes: 3 additions & 0 deletions
3
playground/Stress/Stress.AppHost/process-command-scripts/output-limit.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| for (let i = 1; i <= 20; i++) { | ||
| console.log(`line-${i}`); | ||
| } |
1 change: 1 addition & 0 deletions
1
playground/Stress/Stress.AppHost/process-command-scripts/sample-backup.dump
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| sample backup content for process command restore scenario |
3 changes: 3 additions & 0 deletions
3
playground/Stress/Stress.AppHost/process-command-scripts/stderr-failure.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| console.log("stdout-line"); | ||
| console.error("stderr-line"); | ||
| process.exit(3); |
7 changes: 7 additions & 0 deletions
7
playground/Stress/Stress.AppHost/process-command-scripts/stdin.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| let input = ""; | ||
|
|
||
| process.stdin.setEncoding("utf8"); | ||
| process.stdin.on("data", chunk => input += chunk); | ||
| process.stdin.on("end", () => { | ||
| console.log(`stdin-${input.trim()}`); | ||
| }); |
9 changes: 9 additions & 0 deletions
9
playground/Stress/Stress.AppHost/process-command-scripts/working-directory.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const fs = require("node:fs"); | ||
|
|
||
| const projectFiles = fs.readdirSync(process.cwd()) | ||
| .filter(entry => entry.endsWith(".csproj")) | ||
| .sort(); | ||
|
|
||
| for (const projectFile of projectFiles) { | ||
| console.log(projectFile); | ||
| } |
6 changes: 6 additions & 0 deletions
6
playground/Stress/process-command-file-apps/arguments-and-stdin.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| #:sdk Microsoft.NET.Sdk | ||
| var input = await Console.In.ReadToEndAsync(); | ||
| var name = args.Length > 0 ? args[0] : "unknown"; | ||
|
|
||
| Console.WriteLine($"csharp-argument-{name}"); | ||
| Console.WriteLine($"csharp-stdin-{input.Trim()}"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| #:sdk Microsoft.NET.Sdk | ||
|
|
||
| var mode = args.Length > 0 ? args[0] : "unknown"; | ||
|
|
||
| Console.WriteLine($"scenario={Environment.GetEnvironmentVariable("ASPIRE_COMMAND_SCENARIO") ?? "manual"}"); | ||
| Console.WriteLine($"mode={mode}"); | ||
|
|
||
| if (mode == "seed") | ||
| { | ||
| var dataset = args.Length > 1 ? args[1] : "small"; | ||
| var customers = args.Length > 2 ? args[2] : "25"; | ||
|
|
||
| Console.WriteLine($"seed-dataset={dataset}"); | ||
| Console.WriteLine($"seed-customers={customers}"); | ||
| Console.WriteLine($"connection-set={!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ConnectionStrings__mainDb"))}"); | ||
| } | ||
| else if (mode == "test") | ||
| { | ||
| var filter = args.Length > 1 ? args[1] : "smoke"; | ||
|
|
||
| Console.WriteLine($"test-filter={filter}"); | ||
| Console.WriteLine($"configuration={Environment.GetEnvironmentVariable("ASPIRE_TEST_CONFIGURATION") ?? "unknown"}"); | ||
| } | ||
| else if (mode == "job") | ||
| { | ||
| var jobName = args.Length > 1 ? args[1] : "daily-import"; | ||
| var stdin = await Console.In.ReadToEndAsync(); | ||
|
|
||
| Console.WriteLine($"job-name={jobName}"); | ||
| Console.WriteLine($"job-payload={stdin.Trim()}"); | ||
| } | ||
| else | ||
| { | ||
| Console.Error.WriteLine($"unknown-mode={mode}"); | ||
| Environment.Exit(2); | ||
| } |
13 changes: 13 additions & 0 deletions
13
playground/Stress/process-command-file-apps/working-directory-failure.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| #:sdk Microsoft.NET.Sdk | ||
|
|
||
| var projectFiles = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.csproj") | ||
| .Select(Path.GetFileName) | ||
| .OrderBy(static fileName => fileName); | ||
|
|
||
| foreach (var projectFile in projectFiles) | ||
| { | ||
| Console.WriteLine(projectFile); | ||
| } | ||
|
|
||
| Console.Error.WriteLine("csharp-stderr-line"); | ||
| Environment.Exit(4); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
playground/TypeScriptAppHost/process-command-scripts/node-process-check.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| let input = ""; | ||
|
|
||
| process.stdin.setEncoding("utf8"); | ||
| process.stdin.on("data", chunk => input += chunk); | ||
| process.stdin.on("end", () => { | ||
| console.log(`ts-process-command-arg=${process.argv[2]}`); | ||
| console.log(`ts-process-command-env=${process.env.TS_PROCESS_COMMAND_SAMPLE}`); | ||
| console.log(`ts-process-command-stdin=${input.trim()}`); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
src/Aspire.Hosting/ApplicationModel/ProcessCommandOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| namespace Aspire.Hosting.ApplicationModel; | ||
|
|
||
| /// <summary> | ||
| /// Optional configuration for resource process commands added with <see cref="ResourceBuilderExtensions.WithProcessCommand{TResource}(IResourceBuilder{TResource}, string, string, Func{ExecuteCommandContext, ValueTask{ProcessCommandSpec}}, ProcessCommandOptions?)"/>. | ||
| /// </summary> | ||
| [Experimental("ASPIREPROCESSCOMMAND001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] | ||
| public class ProcessCommandOptions : CommandOptions | ||
|
davidfowl marked this conversation as resolved.
Resolved
|
||
| { | ||
| private int _maxOutputLineCount = 50; | ||
|
|
||
|
davidfowl marked this conversation as resolved.
Resolved
|
||
| internal static new ProcessCommandOptions Default { get; } = new(); | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the maximum number of stdout and stderr output lines returned as command result data. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Standard output and standard error are captured together in the order observed by the process runner. The returned | ||
| /// command result contains the retained tail of the combined output as plain text. | ||
| /// </para> | ||
| /// </remarks> | ||
| public int MaxOutputLineCount | ||
| { | ||
| get => _maxOutputLineCount; | ||
| set | ||
| { | ||
| ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); | ||
| _maxOutputLineCount = value; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether returned command output should be displayed immediately in the dashboard. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The default value is <see langword="true"/>. | ||
| /// </remarks> | ||
| public bool DisplayImmediately { get; set; } = true; | ||
|
davidfowl marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// ATS-friendly configuration for resource process commands. | ||
| /// </summary> | ||
| [AspireDto] | ||
| internal sealed class ProcessCommandExportOptions | ||
| { | ||
| /// <summary> | ||
| /// The executable path or command name to start. | ||
| /// </summary> | ||
| public string? ExecutablePath { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The command-line arguments for the process. | ||
| /// </summary> | ||
| public IReadOnlyList<string>? Arguments { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The working directory for the process. | ||
| /// </summary> | ||
| public string? WorkingDirectory { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The environment variables to set for the process. | ||
| /// </summary> | ||
| public IReadOnlyList<ProcessCommandEnvironmentVariable>? EnvironmentVariables { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// A value indicating whether the process should inherit the current environment variables. | ||
| /// </summary> | ||
| public bool InheritEnvironmentVariables { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// Standard input content to write to the process after it starts. | ||
| /// </summary> | ||
| public string? StandardInputContent { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// A value indicating whether the entire process tree should be killed when the process is disposed. | ||
| /// </summary> | ||
| public bool KillEntireProcessTree { get; set; } = true; | ||
|
|
||
|
davidfowl marked this conversation as resolved.
Resolved
|
||
| /// <summary> | ||
| /// Optional command configuration. | ||
| /// </summary> | ||
| public CommandOptions? CommandOptions { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The maximum number of stdout and stderr output lines returned as command result data. | ||
| /// </summary> | ||
| public int? MaxOutputLineCount { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// A value indicating whether returned command output should be displayed immediately in the dashboard. | ||
| /// </summary> | ||
| public bool? DisplayImmediately { get; set; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Represents an environment variable to set for a resource process command. | ||
| /// </summary> | ||
| [AspireDto] | ||
| internal sealed class ProcessCommandEnvironmentVariable | ||
| { | ||
| /// <summary> | ||
| /// The environment variable name. | ||
| /// </summary> | ||
| public string? Name { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The environment variable value. | ||
| /// </summary> | ||
| public string? Value { get; set; } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| namespace Aspire.Hosting.ApplicationModel; | ||
|
|
||
| /// <summary> | ||
| /// Describes a local process that is started when a process-backed resource command executes. | ||
| /// </summary> | ||
| /// <param name="executablePath"> | ||
| /// The executable path or command name to start. Command names are resolved from the AppHost process PATH. | ||
| /// </param> | ||
| [Experimental("ASPIREPROCESSCOMMAND001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] | ||
| public sealed class ProcessCommandSpec(string executablePath) | ||
| { | ||
| /// <summary> | ||
| /// Gets the executable path or command name to start. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Command names without directory separators are resolved from the AppHost process PATH before starting the process. | ||
| /// </para> | ||
| /// </remarks> | ||
| public string ExecutablePath { get; } = !string.IsNullOrWhiteSpace(executablePath) | ||
| ? executablePath | ||
| : throw new ArgumentException("The executable path cannot be null, empty, or whitespace.", nameof(executablePath)); | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the working directory for the process. | ||
| /// </summary> | ||
| public string? WorkingDirectory { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the command-line arguments for the process. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Arguments are passed using <see cref="System.Diagnostics.ProcessStartInfo.ArgumentList"/> so that each item is | ||
| /// escaped according to the current platform's process-start rules. | ||
| /// </para> | ||
| /// </remarks> | ||
| public IReadOnlyList<string> Arguments { get; init; } = []; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the environment variables to set for the process. | ||
| /// </summary> | ||
| public IDictionary<string, string> EnvironmentVariables { get; init; } = new Dictionary<string, string>(); | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the process should inherit the current environment variables. | ||
| /// </summary> | ||
| public bool InheritEnvironmentVariables { get; init; } = true; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets standard input content to write to the process after it starts. | ||
| /// </summary> | ||
| public string? StandardInputContent { get; init; } | ||
|
davidfowl marked this conversation as resolved.
Resolved
|
||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the entire process tree should be killed when the process is disposed. | ||
| /// </summary> | ||
| public bool KillEntireProcessTree { get; init; } = true; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Aspire.Hosting.Dcp.Process; | ||
|
|
||
| /// <summary> | ||
| /// Runs external processes. | ||
| /// </summary> | ||
| internal interface IProcessRunner | ||
| { | ||
| /// <summary> | ||
| /// Runs a process with the specified configuration. | ||
| /// </summary> | ||
| (Task<ProcessResult>, IAsyncDisposable) Run(ProcessSpec processSpec); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Runs external processes by delegating to <see cref="ProcessUtil"/>. | ||
| /// </summary> | ||
| internal sealed class DefaultProcessRunner : IProcessRunner | ||
| { | ||
| public (Task<ProcessResult>, IAsyncDisposable) Run(ProcessSpec processSpec) | ||
| => ProcessUtil.Run(processSpec); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.