-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add file-based program API for use by IDE #48749
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This pull request adds a new file-based program API intended for IDE integration by introducing a new run-api command, updating parser and project conversion logic, and expanding test coverage.
- Introduces new tests for API behavior and diagnostic responses in RunFileTests.
- Refactors VirtualProjectBuildingCommand to use static helpers and updated error reporting.
- Updates TestCommand and SdkCommandSpec to support standard input redirection and adds the run-api command to the parser.
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
test/dotnet.Tests/CommandTests/Run/RunFileTests.cs | Added tests for API functionality and diagnostics for file-based programs. |
test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs | Updated project conversion calls to use revised directive methods. |
test/Microsoft.NET.TestFramework/Commands/TestCommand.cs | Added support for redirecting standard input in tests. |
test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs | Updated ProcessStartInfo configuration for standard input redirection. |
src/Cli/dotnet/Parser.cs | Integrated the new run-api command into the parser. |
src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs | Refactored methods to use static helpers and improved error reporting with a diagnostics builder. |
src/Cli/dotnet/Commands/Run/Api/RunApiCommandParser.cs and RunApiCommand.cs | Added a new API command for handling file-based program inputs and outputs. |
src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs | Updated project conversion to call the revised directive methods correctly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. It would be good to provide a self contained sample usage of the CLI--just a simple case which shows, say, running the command from CLI, piping in some data, and getting a result out on the other end, which would indicate the expected schema of the command.
{ | ||
public override int Execute() | ||
{ | ||
for (string line; (line = Console.ReadLine()) != null;) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't seen this kind of pattern before, is this a norm for apps which communicate using json over stdio? To essentially put each message on its own line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON separated by newlines seemed simple and straightforward, but feel free to suggest alternatives (especially what IDE would like to communicate with). I guess JSON-RPC is more standard, but likely needs more code / libraries. Instead of stdio we could also use named pipes like compiler server does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have particular concerns about the current solution, just trying to make sure I understand the design. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd definitely consider JSON-RPC as it is a standard for this kind of stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I did the out-of-proc build host stuff, I implemented my own RPC via single-line as well. That said, it's a bit unclear to me why we're needing multiple lines like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, it's a bit unclear to me why we're needing multiple lines like this.
If you expect IDE is going to make only one-off requests (i.e., run new dotnet run-api
process for each project separately), we can definitely simplify this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are no preferences, should we proceed with the current state for starters? If needed, we can change the API later (the run-api
command is hidden, so I think we are free to change it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally have no concerns about the current state. I don't see any reason the command shouldn't handle sequenced requests and responses, even if IDE doesn't make use of that capability.
If we found that spinning a new subprocess to make a project, each time #:
s are edited or under other various "relatively infrequent" conditions (as compared to updates of ordinary source code), is problematic, then we could start keeping the subprocess alive for some period of time, and making use of this queueing capability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd definitely consider JSON-RPC as it is a standard for this kind of stuff.
@MiYanni Does this comment mean you want me to switch to json-rpc before merging this? There would be some questions like if we want a full compliance with the standard, then we probably need some library (which also means ship a new DLL along the sdk, and likely a new DLL load on the IDE side). Or we can implement this manually like Jason did for out-of-proc (@jasonmalinowski, can you point me to your implementation?). But perhaps we can follow up later since this is an internal API between SDK and IDE (and will need changing anyway for multi file support)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public sealed class GetProject : RunApiInput | ||
{ | ||
public string? ArtifactsPath { get; init; } | ||
public required string EntryPointFileFullPath { get; init; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This represents the primary bit of information that IDE must provide, right? This means that IDE must come up with a solution for searching, from a loose file that was opened, to find a "nearest relevant entry point", cracking the files as we go, and pass that in to dotnet cli. I think that's something we can do, just making sure of the expectation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API is for single-file currently, so it should be simple - just pass the current file as the EntryPointFileFullPath.
For multi-file scenarios, I agree you would need to do the search you describe. The API will then also take the list of files to exclude (i.e., the other entry points). The implementation can automatically discover the other .cs
files to include (which it needs to read for directives) based on the entry point or we can pass them to the API if that would be better for perf (since the IDE might have already discovered them via its more efficient file watchers).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, it might be useful if the IDE checks SDK's version to determine whether run-api is present and whether it supports single-file or multi-file scenarios (and perhaps more in the future) - so the IDE can give better error messages when the SDK being used is not in sync with the features implemented in the IDE.
We could also introduce some run-api call like "GetCapabilities" that the IDE could use (and for SDKs that don't contain run-api command at all, presumably the IDE could detect that from the error message SDK produces when trying to run dotnet run-api
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a version stamp in requests/responses would give us enough expressiveness here? That would be a bit like SymbolKey.FormatVersion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it might be useful if the IDE checks SDK's version to determine whether run-api is present
If we have to do things like that, then I'd expect we have some API call we do for "get entry point discovery algorithm" or something, rather than an explicit check on the SDK version directly, especially if we try something experimentally and then have to roll it back in the SDK for a few weeks or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and consider if/when we ever have a situation where the user's repo might opt them into some other behavior)
""")}},"Diagnostics": | ||
[{"Location":{ | ||
"Path":{{ToJson(programPath)}}, | ||
"Span":{"Start":{"Line":0,"Character":0},"End":{"Line":1,"Character":0}{{nop}}}{{nop}}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, is this nop breaking-up the }
s in order to keep distinct which ones are ending an interpolation and which ones are part of the content?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nop is to avoid compiler errors due to the closing brackets }}
inside an interpolated string $$"""
. (the errors are redundant IMO, the interpolated string could be compiled just fine even though it contains unmatched }}
.) See also the doc comment at the nop
constant.
Example:
var s1 = $$"""{"x":{{1}},"y":{"z":2}}"""; // error on the last }}
var s2 = $$$"""{"x":{{{1}}},"y":{"z":2}}"""; // ok but interpolation holes need soo many brackets (even more when the JSON is more nested)
const string nop = "";
var s3 = $$"""{"x":{{1}},"y":{"z":2}{{nop}}}"""; // workaround (we can keep the number of `$$` constant wrt nesting of the JSON)
/// (e.g., show an error message when an incompatible SDK version is being used). | ||
/// </summary> | ||
[JsonPropertyOrder(-1)] | ||
public int Version { get; } = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might also make sense for caller to pass in the "version" of the API they are using, implying the set of behaviors they expect, and the run-api command can reject the request in a well-defined way if it's not "compatible" with that version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it looks like this property is on the base type? So the caller can always just read the version that comes out. It would be good to verify that this property is present in an error case. No need for caller to pass in the version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just recreated this issue as it relates to output from the CLI. JSON will be one of the bespoke formats.
{ | ||
public override int Execute() | ||
{ | ||
for (string line; (line = Console.ReadLine()) != null;) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd definitely consider JSON-RPC as it is a standard for this kind of stuff.
@MiYanni for review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current design is fine. My comment about JSON-RPC is generally for if the implementation grows, it is definitely something that should be considered. But I'm not going to halt merging this on doing all that work.
I'd like to keep an eye out for a nightly SDK which has this included. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies I didn't have a chance to leave comments prior to this merging -- and worse off the few I had written I forgot to post.
{ | ||
Command command = new("run-api") | ||
{ | ||
Hidden = true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there precedent for other commands like this? Rather than having a new command can we/should we hide this behind just an --api flag on dotnet run?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not an expert on dotnet.exe CLI, but I think hiding this under dotnet run --api
means all the other dotnet run
flags can still be passed and we would need to manually check and error on them, e.g., dotnet run --no-restore --api
and so on.
/// When the API shape or behavior changes, this should be incremented so the callers (IDEs) can act accordingly | ||
/// (e.g., show an error message when an incompatible SDK version is being used). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect that the version number isn't stated here -- we've already had to assume the shape of the output to even read this! Maybe we need to do a few more things:
- The version number could stated outside of the first JSON payload (maybe just on a line of it's own), simply because by the time we're reading this value we've already more or less had to assume the JSON output format to a certain extent. This would be critical if/when we have to break the communication format more generally (i.e. we need to switch to MessagePack because this payload is getting too big to use.)
- The invocation of the API command from the IDE includes as an input switch the max supported version. That way if we're in a situation where you need to ship a breaking change in the SDK to fix an issue, you can still potentially return an older version for a downlevel client until clients catch up. I recognize the SDK doesn't want to be supporting downlevel clients forever, but it could still at least be useful where if we have to ship a break in Preview n, you can drop the support in Preivew (n+1), but for that few week window while the preview is being built and folks have a mismatch of client versions, we can still negotiate a version that works.
{ | ||
public override int Execute() | ||
{ | ||
for (string? line; (line = Console.ReadLine()) != null;) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you haven't already created one, add a test for non-ASCII stuff being included in the stream. One "fun" discovery we had when making our own JSON protocol similar to this for the MSBuildWorkspace build host stuff was that the console code page might gets screwed up in funny ways and mangle stuff. Since we're (currently) using Newtonsoft.Json our hack was just:
Not sure if you have a similar option here.
} | ||
catch (Exception ex) | ||
{ | ||
Respond(new RunApiOutput.Error { Message = ex.Message, Details = ex.ToString() }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just establishing the contract -- should we also be listening to stderr as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we also be listening to stderr as well?
You mean from the IDE? I think you should, although the code inside this try
/catch
will report all its errors as stdout json message, other code above this (like command-line parsing) might not.
} | ||
} | ||
|
||
[JsonDerivedType(typeof(GetProject), nameof(GetProject))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to specify a TypeDescriminator here so it's very clear we're not using the class name (which might get renamed) as the actual API contract?
[JsonDerivedType(typeof(Error), nameof(Error))] | ||
[JsonDerivedType(typeof(Project), nameof(Project))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same potential comment as earlier: let's have explicit type discriminators here rather than implicitly encoding a type name.
errors.Add(new SimpleDiagnostic | ||
{ | ||
Location = sourceFile.GetFileLinePositionSpan(trivia.Span), | ||
Message = string.Format(CliCommandStrings.CannotConvertDirective, location), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which language is this being localized in? I wonder if we'll need to pass something along to give you a hint.
|
||
public sealed class GetProject : RunApiInput | ||
{ | ||
public string? ArtifactsPath { get; init; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What it does mean for us to pass this in? I would have imagined you were returning it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is optional, the SDK normally computes it, but you can overwrite it which is useful for tests. I will add a clarifying comment.
@@ -967,6 +1009,27 @@ private Package() { } | |||
} | |||
} | |||
|
|||
internal sealed class SimpleDiagnostic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need some sort of error/warning concept?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not right now, we can always add a severity property later, that shouldn't break compatibility
"Path":{{ToJson(programPath)}}, | ||
"Span":{"Start":{"Line":1,"Character":0},"End":{"Line":1,"Character":30}{{nop}}}{{nop}}}, | ||
"Message":{{ToJson(string.Format(CliCommandStrings.CannotConvertDirective, $"{programPath}:2"))}}}]} | ||
""".ReplaceLineEndings("")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSON output is always on a single line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
No description provided.