From 3e85449ff03cb7405af1f6aae1244e9397f678b9 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 16:04:21 -0400 Subject: [PATCH 1/8] Add netstandard and net10 targets to C# SDK Multi-target the C# SDK for net8.0, net10.0, and netstandard2.0, with downlevel-only polyfills for APIs missing from netstandard2.0. Add Windows-only net472 test coverage so the netstandard2.0 asset is exercised, and keep net10.0 on the shared System.Text.Json surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Build.props | 1 - dotnet/Directory.Packages.props | 6 + dotnet/src/Client.cs | 11 +- dotnet/src/GitHub.Copilot.SDK.csproj | 20 +- dotnet/src/JsonRpc.cs | 36 +- dotnet/src/Polyfills/ArrayBufferWriter.cs | 92 ++++ dotnet/src/Polyfills/BclAttributes.cs | 29 ++ .../src/Polyfills/CodeAnalysisAttributes.cs | 70 +++ .../Polyfills/DataAnnotationsAttributes.cs | 32 ++ dotnet/src/Polyfills/DownlevelExtensions.cs | 485 ++++++++++++++++++ dotnet/src/Polyfills/IsExternalInit.cs | 17 + dotnet/src/Polyfills/TaskCompletionSource.cs | 23 + dotnet/src/Polyfills/Utf8.cs | 25 + dotnet/src/Types.cs | 2 +- dotnet/test/GitHub.Copilot.SDK.Test.csproj | 16 +- dotnet/test/Unit/JsonRpcTests.cs | 31 +- dotnet/test/Unit/SerializationTests.cs | 7 + 17 files changed, 878 insertions(+), 25 deletions(-) create mode 100644 dotnet/src/Polyfills/ArrayBufferWriter.cs create mode 100644 dotnet/src/Polyfills/BclAttributes.cs create mode 100644 dotnet/src/Polyfills/CodeAnalysisAttributes.cs create mode 100644 dotnet/src/Polyfills/DataAnnotationsAttributes.cs create mode 100644 dotnet/src/Polyfills/DownlevelExtensions.cs create mode 100644 dotnet/src/Polyfills/IsExternalInit.cs create mode 100644 dotnet/src/Polyfills/TaskCompletionSource.cs create mode 100644 dotnet/src/Polyfills/Utf8.cs diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index badf8483d..88c409e86 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -1,7 +1,6 @@ - net8.0 14 enable enable diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 822b36c93..c47ed4ff2 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -6,11 +6,17 @@ + + + + + + diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index b1e9dce0e..254f03af2 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -9,6 +9,7 @@ using System.Data; using System.Diagnostics; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -1239,7 +1240,7 @@ internal static async Task InvokeRpcAsync(JsonRpc rpc, string method, obje if (!string.IsNullOrEmpty(stderrOutput)) { - throw new IOException(FormatCliExitedMessage("CLI process exited unexpectedly.", stderrOutput), ex); + throw new IOException(FormatCliExitedMessage("CLI process exited unexpectedly.", stderrOutput!), ex); } throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex); } @@ -1560,7 +1561,7 @@ private static bool IsUnsupportedConnectMethod(RemoteRpcException ex) // Always use portable RID (e.g., linux-x64) to match the build-time placement, // since distro-specific RIDs (e.g., ubuntu.24.04-x64) are normalized at build time. var rid = GetPortableRid() - ?? Path.GetFileName(System.Runtime.InteropServices.RuntimeInformation.RuntimeIdentifier); + ?? Path.GetFileName(RuntimeInformation.RuntimeIdentifier); searchedPath = Path.Combine(AppContext.BaseDirectory, "runtimes", rid, "native", binaryName); return File.Exists(searchedPath) ? searchedPath : null; } @@ -2143,8 +2144,14 @@ internal record PermissionRequestResponseV2( [JsonSerializable(typeof(UserInputResponse))] internal partial class ClientJsonContext : JsonSerializerContext; +#if NET8_0_OR_GREATER [GeneratedRegex(@"listening on port ([0-9]+)", RegexOptions.IgnoreCase)] private static partial Regex ListeningOnPortRegex(); +#else + private static readonly Regex s_listeningOnPortRegex = new(@"listening on port ([0-9]+)", RegexOptions.IgnoreCase); + + private static Regex ListeningOnPortRegex() => s_listeningOnPortRegex; +#endif } /// diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index abcb8a51a..44f3993a3 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -1,6 +1,7 @@  + net8.0;net10.0;netstandard2.0 true 0.1.0 SDK for programmatic control of GitHub Copilot CLI @@ -13,7 +14,7 @@ https://github.com/github/copilot-sdk copilot.png github;copilot;sdk;jsonrpc;agent - true + true true snupkg true @@ -37,9 +38,26 @@ + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index 7480aa8ff..0fb3e32ad 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -5,7 +5,6 @@ using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Text; @@ -609,12 +608,11 @@ private async Task HandleIncomingMethodAsync(string methodName, JsonElement mess return null; } - if (result is not null && registration.ReturnsValueTaskOfT) + if (result is not null && registration.ValueTaskAsTaskMethod is { } valueTaskAsTaskMethod) { - var resultType = result.GetType(); - var asTask = (Task)resultType.GetMethod("AsTask")!.Invoke(result, null)!; + var asTask = (Task)valueTaskAsTaskMethod.Invoke(result, null)!; await asTask.ConfigureAwait(false); - return asTask.GetType().GetProperty("Result")!.GetValue(asTask); + return registration.TaskResultGetter!.Invoke(asTask, null); } return result; @@ -756,6 +754,9 @@ await SendMessageAsync(new JsonRpcNotification private sealed class PendingRequest() : TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private static readonly MethodInfo s_taskGetResult = typeof(Task<>).GetProperty(nameof(Task.Result), BindingFlags.Instance | BindingFlags.Public)!.GetMethod!; + private static readonly MethodInfo s_valueTaskAsTask = typeof(ValueTask<>).GetMethod(nameof(ValueTask.AsTask), BindingFlags.Instance | BindingFlags.Public)!; + private sealed class MethodRegistration { public MethodRegistration(Delegate handler, bool singleObjectParam) @@ -763,15 +764,32 @@ public MethodRegistration(Delegate handler, bool singleObjectParam) Handler = handler; SingleObjectParam = singleObjectParam; Parameters = handler.Method.GetParameters(); - ReturnsValueTaskOfT = - handler.Method.ReturnType.IsGenericType && - handler.Method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>); + var returnType = handler.Method.ReturnType; + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + ValueTaskAsTaskMethod = GetMethodFromGenericMethodDefinition(returnType, s_valueTaskAsTask); + TaskResultGetter = GetMethodFromGenericMethodDefinition(ValueTaskAsTaskMethod.ReturnType, s_taskGetResult); + } } public Delegate Handler { get; } public bool SingleObjectParam { get; } public ParameterInfo[] Parameters { get; } - public bool ReturnsValueTaskOfT { get; } + public MethodInfo? ValueTaskAsTaskMethod { get; } + public MethodInfo? TaskResultGetter { get; } + } + + private static MethodInfo GetMethodFromGenericMethodDefinition(Type specializedType, MethodInfo genericMethodDefinition) + { + Debug.Assert( + specializedType.IsGenericType && specializedType.GetGenericTypeDefinition() == genericMethodDefinition.DeclaringType, + "Generic member definition doesn't match type."); +#if NET8_0_OR_GREATER + return (MethodInfo)specializedType.GetMemberWithSameMetadataDefinitionAs(genericMethodDefinition); +#else + const BindingFlags All = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; + return specializedType.GetMethods(All).First(m => m.MetadataToken == genericMethodDefinition.MetadataToken); +#endif } [JsonSourceGenerationOptions( diff --git a/dotnet/src/Polyfills/ArrayBufferWriter.cs b/dotnet/src/Polyfills/ArrayBufferWriter.cs new file mode 100644 index 000000000..fb684ce27 --- /dev/null +++ b/dotnet/src/Polyfills/ArrayBufferWriter.cs @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +namespace System.Buffers; + +internal sealed class ArrayBufferWriter : IBufferWriter +{ + private const int DefaultInitialBufferSize = 256; + private T[] _buffer; + private int _index; + + public ArrayBufferWriter() + : this(DefaultInitialBufferSize) + { + } + + public ArrayBufferWriter(int initialCapacity) + { + if (initialCapacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(initialCapacity)); + } + + _buffer = initialCapacity == 0 ? [] : new T[initialCapacity]; + } + + public ReadOnlyMemory WrittenMemory => _buffer.AsMemory(0, _index); + + public ReadOnlySpan WrittenSpan => _buffer.AsSpan(0, _index); + + public int WrittenCount => _index; + + public int Capacity => _buffer.Length; + + public int FreeCapacity => _buffer.Length - _index; + + public void Clear() + { + _buffer.AsSpan(0, _index).Clear(); + _index = 0; + } + + public void Advance(int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (count > FreeCapacity) + { + throw new InvalidOperationException("Cannot advance past the end of the buffer."); + } + + _index += count; + } + + public Memory GetMemory(int sizeHint = 0) + { + CheckAndResizeBuffer(sizeHint); + return _buffer.AsMemory(_index); + } + + public Span GetSpan(int sizeHint = 0) + { + CheckAndResizeBuffer(sizeHint); + return _buffer.AsSpan(_index); + } + + private void CheckAndResizeBuffer(int sizeHint) + { + if (sizeHint < 0) + { + throw new ArgumentOutOfRangeException(nameof(sizeHint)); + } + + if (sizeHint == 0) + { + sizeHint = 1; + } + + if (sizeHint <= FreeCapacity) + { + return; + } + + var growBy = Math.Max(sizeHint, _buffer.Length); + var newSize = checked(_buffer.Length + growBy); + Array.Resize(ref _buffer, newSize); + } +} diff --git a/dotnet/src/Polyfills/BclAttributes.cs b/dotnet/src/Polyfills/BclAttributes.cs new file mode 100644 index 000000000..333ff55b8 --- /dev/null +++ b/dotnet/src/Polyfills/BclAttributes.cs @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName; + + public string ParameterName { get; } +} + +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] +internal sealed class CompilerFeatureRequiredAttribute : Attribute +{ + public const string RefStructs = nameof(RefStructs); + public const string RequiredMembers = nameof(RequiredMembers); + + public CompilerFeatureRequiredAttribute(string featureName) => FeatureName = featureName; + + public string FeatureName { get; } + + public bool IsOptional { get; set; } +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +internal sealed class RequiredMemberAttribute : Attribute; diff --git a/dotnet/src/Polyfills/CodeAnalysisAttributes.cs b/dotnet/src/Polyfills/CodeAnalysisAttributes.cs new file mode 100644 index 000000000..6f63cc642 --- /dev/null +++ b/dotnet/src/Polyfills/CodeAnalysisAttributes.cs @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] +internal sealed class ExperimentalAttribute : Attribute +{ + public ExperimentalAttribute(string diagnosticId) => DiagnosticId = diagnosticId; + + public string DiagnosticId { get; } + + public string? UrlFormat { get; set; } +} + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class NotNullWhenAttribute : Attribute +{ + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + public bool ReturnValue { get; } +} + +[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] +internal sealed class SetsRequiredMembersAttribute : Attribute; + +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +internal sealed class StringSyntaxAttribute : Attribute +{ + public const string Uri = nameof(Uri); + + public StringSyntaxAttribute(string syntax) + { + Syntax = syntax; + Arguments = []; + } + + public StringSyntaxAttribute(string syntax, params object?[] arguments) + { + Syntax = syntax; + Arguments = arguments; + } + + public string Syntax { get; } + + public object?[] Arguments { get; } +} + +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] +internal sealed class UnconditionalSuppressMessageAttribute : Attribute +{ + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + Category = category; + CheckId = checkId; + } + + public string Category { get; } + + public string CheckId { get; } + + public string? Scope { get; set; } + + public string? Target { get; set; } + + public string? MessageId { get; set; } + + public string? Justification { get; set; } +} diff --git a/dotnet/src/Polyfills/DataAnnotationsAttributes.cs b/dotnet/src/Polyfills/DataAnnotationsAttributes.cs new file mode 100644 index 000000000..bf41e2095 --- /dev/null +++ b/dotnet/src/Polyfills/DataAnnotationsAttributes.cs @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +namespace System.ComponentModel.DataAnnotations; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +internal sealed class Base64StringAttribute : ValidationAttribute +{ + public override bool IsValid(object? value) + { + if (value is null) + { + return true; + } + + if (value is not string text) + { + return false; + } + + try + { + Convert.FromBase64String(text); + return true; + } + catch (FormatException) + { + return false; + } + } +} diff --git a/dotnet/src/Polyfills/DownlevelExtensions.cs b/dotnet/src/Polyfills/DownlevelExtensions.cs new file mode 100644 index 000000000..a2e996ffb --- /dev/null +++ b/dotnet/src/Polyfills/DownlevelExtensions.cs @@ -0,0 +1,485 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Buffers; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace System +{ + internal static class DownlevelArgumentNullExceptionExtensions + { + extension(ArgumentNullException) + { + public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (argument is null) + { + throw new ArgumentNullException(paramName); + } + } + } + } + + internal static class DownlevelArgumentExceptionExtensions + { + extension(ArgumentException) + { + public static void ThrowIfNullOrWhiteSpace(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (argument is null) + { + throw new ArgumentNullException(paramName); + } + + if (string.IsNullOrWhiteSpace(argument)) + { + throw new ArgumentException("The value cannot be an empty string or composed entirely of whitespace.", paramName); + } + } + } + } + + internal static class DownlevelDateTimeExtensions + { + extension(DateTime) + { + public static DateTime UnixEpoch => new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + } + } + + internal static class DownlevelDateTimeOffsetExtensions + { + extension(DateTimeOffset) + { + public static DateTimeOffset UnixEpoch => new(0, TimeSpan.Zero); + } + } + + internal static class DownlevelIntExtensions + { + extension(int) + { + public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFormatProvider? provider, out int result) + { + if (style == NumberStyles.None) + { + return TryParseNonNegativeInt32(utf8Text, out result); + } + + return int.TryParse(Encoding.UTF8.GetString(utf8Text.ToArray()), style, provider, out result); + } + } + + private static bool TryParseNonNegativeInt32(ReadOnlySpan utf8Text, out int result) + { + if (utf8Text.IsEmpty) + { + result = 0; + return false; + } + + var value = 0; + foreach (var c in utf8Text) + { + var digit = c - (byte)'0'; + if ((uint)digit > 9) + { + result = 0; + return false; + } + + if (value > (int.MaxValue - digit) / 10) + { + result = 0; + return false; + } + + value = (value * 10) + digit; + } + + result = value; + return true; + } + } + + internal static class DownlevelOperatingSystemExtensions + { + extension(OperatingSystem) + { + public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + } + + internal static class DownlevelDisposableExtensions + { + extension(IDisposable disposable) + { + public ValueTask DisposeAsync() + { + disposable.Dispose(); + return default; + } + } + } +} + +namespace System.Collections.Generic +{ + internal static class DownlevelKeyValuePairExtensions + { + extension(KeyValuePair pair) + { + public void Deconstruct(out TKey key, out TValue value) + { + key = pair.Key; + value = pair.Value; + } + } + } +} + +namespace System.Diagnostics +{ + internal static class DownlevelStopwatchExtensions + { + extension(Stopwatch) + { + public static TimeSpan GetElapsedTime(long startingTimestamp) => + GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp()); + + public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) + { + var elapsedTicks = endingTimestamp - startingTimestamp; + return TimeSpan.FromTicks((long)(elapsedTicks * ((double)TimeSpan.TicksPerSecond / Stopwatch.Frequency))); + } + } + } + + internal static class DownlevelProcessExtensions + { + extension(Process process) + { + public void Kill(bool entireProcessTree) + { + if (entireProcessTree && OperatingSystem.IsWindows()) + { + using var taskKill = Process.Start(new ProcessStartInfo + { + FileName = "taskkill.exe", + Arguments = string.Format(CultureInfo.InvariantCulture, "/PID {0} /T /F", process.Id), + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + }); + + if (taskKill is not null && + taskKill.WaitForExit(milliseconds: 30_000) && + (taskKill.ExitCode == 0 || process.HasExited)) + { + return; + } + } + + if (!process.HasExited) + { + process.Kill(); + } + } + + public Task WaitForExitAsync(Threading.CancellationToken cancellationToken = default) + { + if (process.HasExited) + { + return Task.CompletedTask; + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + EventHandler handler = (_, _) => completion.TrySetResult(null); + process.EnableRaisingEvents = true; + process.Exited += handler; + + if (process.HasExited) + { + completion.TrySetResult(null); + } + + var cancellationRegistration = cancellationToken.CanBeCanceled + ? cancellationToken.Register(static state => ((TaskCompletionSource)state!).TrySetCanceled(), completion) + : default; + + return WaitForExitAsyncCore(process, completion.Task, handler, cancellationRegistration); + } + } + + private static async Task WaitForExitAsyncCore( + Process process, + Task waitTask, + EventHandler handler, + Threading.CancellationTokenRegistration cancellationRegistration) + { + try + { + await waitTask.ConfigureAwait(false); + } + finally + { + process.Exited -= handler; + cancellationRegistration.Dispose(); + } + } + } +} + +namespace System.IO +{ + internal static class DownlevelStreamExtensions + { + extension(Stream stream) + { + public ValueTask ReadAsync(Memory buffer, Threading.CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(buffer, out ArraySegment segment)) + { + return new ValueTask(stream.ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + + return ReadAsyncSlow(stream, buffer, cancellationToken); + } + + public ValueTask WriteAsync(ReadOnlyMemory buffer, Threading.CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(buffer, out ArraySegment segment)) + { + return new ValueTask(stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + + return WriteAsyncSlow(stream, buffer, cancellationToken); + } + + public async ValueTask ReadExactlyAsync(Memory buffer, Threading.CancellationToken cancellationToken = default) + { + var totalRead = 0; + while (totalRead < buffer.Length) + { + var bytesRead = await stream.ReadAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false); + if (bytesRead <= 0) + { + throw new EndOfStreamException(); + } + + totalRead += bytesRead; + } + } + } + + private static async ValueTask ReadAsyncSlow(Stream stream, Memory buffer, Threading.CancellationToken cancellationToken) + { + var rented = ArrayPool.Shared.Rent(buffer.Length); + try + { + var bytesRead = await stream.ReadAsync(rented, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + rented.AsMemory(0, bytesRead).CopyTo(buffer); + return bytesRead; + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + + private static async ValueTask WriteAsyncSlow(Stream stream, ReadOnlyMemory buffer, Threading.CancellationToken cancellationToken) + { + var rented = ArrayPool.Shared.Rent(buffer.Length); + try + { + buffer.CopyTo(rented); + await stream.WriteAsync(rented, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + } + + internal static class DownlevelTextReaderExtensions + { + extension(TextReader reader) + { + public Task ReadLineAsync(Threading.CancellationToken cancellationToken) + { + var task = reader.ReadLineAsync(); + return cancellationToken.CanBeCanceled + ? WaitAsync(task, cancellationToken) + : task; + } + } + + private static async Task WaitAsync(Task task, Threading.CancellationToken cancellationToken) + { + if (task.IsCompleted || !cancellationToken.CanBeCanceled) + { + return await task.ConfigureAwait(false); + } + + var cancellationTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var registration = cancellationToken.Register(static state => ((TaskCompletionSource)state!).TrySetCanceled(), cancellationTask); + if (await Task.WhenAny(task, cancellationTask.Task).ConfigureAwait(false) != task) + { + throw new OperationCanceledException(cancellationToken); + } + + return await task.ConfigureAwait(false); + } + } +} + +namespace System.Net.Sockets +{ + internal static class DownlevelSocketExtensions + { + extension(Socket socket) + { + public Task ConnectAsync(string host, int port, Threading.CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectState = new SocketConnectState(socket, completion); + try + { + socket.BeginConnect( + host, + port, + static asyncResult => + { + var connectState = (SocketConnectState)asyncResult.AsyncState!; + try + { + connectState.Socket.EndConnect(asyncResult); + connectState.Completion.TrySetResult(null); + } + catch (Exception ex) + { + connectState.Completion.TrySetException(ex); + } + }, + connectState); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + + return cancellationToken.CanBeCanceled + ? WaitAsync(completion.Task, socket.Dispose, cancellationToken) + : completion.Task; + } + } + + private static async Task WaitAsync(Task task, Action cancellationAction, Threading.CancellationToken cancellationToken) + { + if (task.IsCompleted) + { + await task.ConfigureAwait(false); + return; + } + + var cancellationTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var registration = cancellationToken.Register( + static state => + { + var cancellationState = (CancellationState)state!; + cancellationState.CancellationAction(); + cancellationState.Completion.TrySetCanceled(); + }, + new CancellationState(cancellationTask, cancellationAction)); + + if (await Task.WhenAny(task, cancellationTask.Task).ConfigureAwait(false) != task) + { + throw new OperationCanceledException(cancellationToken); + } + + await task.ConfigureAwait(false); + } + + private sealed record CancellationState(TaskCompletionSource Completion, Action CancellationAction); + + private sealed record SocketConnectState(Socket Socket, TaskCompletionSource Completion); + } +} + +namespace System.Runtime.InteropServices +{ + internal static class DownlevelRuntimeInformationExtensions + { + extension(RuntimeInformation) + { + public static string RuntimeIdentifier + { + get + { + var os = OperatingSystem.IsWindows() ? "win" : + OperatingSystem.IsLinux() ? "linux" : + OperatingSystem.IsMacOS() ? "osx" : + RuntimeInformation.OSDescription.ToLowerInvariant().Replace(' ', '-'); + + var arch = RuntimeInformation.OSArchitecture switch + { + Architecture.X64 => "x64", + Architecture.X86 => "x86", + Architecture.Arm => "arm", + Architecture.Arm64 => "arm64", + _ => RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), + }; + + return $"{os}-{arch}"; + } + } + } + } +} + +namespace System.Threading +{ + internal static class DownlevelCancellationTokenRegistrationExtensions + { + extension(CancellationTokenRegistration registration) + { + public ValueTask DisposeAsync() + { + registration.Dispose(); + return default; + } + } + } +} + +namespace System.Threading.Tasks +{ + internal static class DownlevelValueTaskExtensions + { + extension(ValueTask) + { + public static ValueTask FromResult(T result) => new(result); + } + } +} diff --git a/dotnet/src/Polyfills/IsExternalInit.cs b/dotnet/src/Polyfills/IsExternalInit.cs new file mode 100644 index 000000000..0dc8e729c --- /dev/null +++ b/dotnet/src/Polyfills/IsExternalInit.cs @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#if NET8_0_OR_GREATER +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] +#else +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +/// +/// Reserved to be used by the compiler for tracking metadata. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit; +#endif diff --git a/dotnet/src/Polyfills/TaskCompletionSource.cs b/dotnet/src/Polyfills/TaskCompletionSource.cs new file mode 100644 index 000000000..6bd1a2db9 --- /dev/null +++ b/dotnet/src/Polyfills/TaskCompletionSource.cs @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +namespace System.Threading.Tasks; + +internal sealed class TaskCompletionSource : TaskCompletionSource +{ + public TaskCompletionSource() + { + } + + public TaskCompletionSource(TaskCreationOptions creationOptions) + : base(creationOptions) + { + } + + public new Task Task => base.Task; + + public void SetResult() => base.SetResult(null); + + public bool TrySetResult() => base.TrySetResult(null); +} diff --git a/dotnet/src/Polyfills/Utf8.cs b/dotnet/src/Polyfills/Utf8.cs new file mode 100644 index 000000000..c2ca75b48 --- /dev/null +++ b/dotnet/src/Polyfills/Utf8.cs @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Text; + +namespace System.Text.Unicode; + +internal static class Utf8 +{ + public static bool TryWrite(Span destination, string value, out int bytesWritten) + { + var byteCount = Encoding.UTF8.GetByteCount(value); + if (byteCount > destination.Length) + { + bytesWritten = 0; + return false; + } + + var bytes = Encoding.UTF8.GetBytes(value); + bytes.CopyTo(destination); + bytesWritten = byteCount; + return true; + } +} diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 0775280e8..e387f91fe 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -28,7 +28,7 @@ internal static string ReadValue(ref Utf8JsonReader reader, Type typeToConvert) throw new JsonException($"Expected a non-empty string token when reading {typeToConvert.Name}."); } - return value; + return value!; } internal static void WriteValue(Utf8JsonWriter writer, string value, Type typeToConvert) diff --git a/dotnet/test/GitHub.Copilot.SDK.Test.csproj b/dotnet/test/GitHub.Copilot.SDK.Test.csproj index 5d7e3dd16..0eb5a626c 100644 --- a/dotnet/test/GitHub.Copilot.SDK.Test.csproj +++ b/dotnet/test/GitHub.Copilot.SDK.Test.csproj @@ -1,6 +1,8 @@ + net8.0 + net8.0;net472 false true $(NoWarn);GHCP001 @@ -9,11 +11,8 @@ - false + false @@ -33,4 +32,13 @@ + + + + + + + + + diff --git a/dotnet/test/Unit/JsonRpcTests.cs b/dotnet/test/Unit/JsonRpcTests.cs index e7a9a31b2..a62a3dbe8 100644 --- a/dotnet/test/Unit/JsonRpcTests.cs +++ b/dotnet/test/Unit/JsonRpcTests.cs @@ -234,7 +234,15 @@ public override void Flush() public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); - public override async ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + +#if NET8_0_OR_GREATER + public override +#else + internal +#endif + async ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) { while (true) { @@ -242,13 +250,14 @@ public override async ValueTask ReadAsync(Memory destination, Cancell { if (_buffer.Count > 0) { - var count = Math.Min(destination.Length, _buffer.Count); - for (var i = 0; i < count; i++) + var bytesRead = Math.Min(destination.Length, _buffer.Count); + var span = destination.Span; + for (var i = 0; i < bytesRead; i++) { - destination.Span[i] = _buffer.Dequeue(); + span[i] = _buffer.Dequeue(); } - return count; + return bytesRead; } if (_completed) @@ -264,11 +273,19 @@ public override async ValueTask ReadAsync(Memory destination, Cancell public override void Write(byte[] buffer, int offset, int count) => WriteAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); - public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + +#if NET8_0_OR_GREATER + public override +#else + internal +#endif + ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) { var peer = _peer ?? throw new ObjectDisposedException(nameof(InMemoryDuplexStream)); peer.Enqueue(source.Span); - return ValueTask.CompletedTask; + return default; } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index fd1d0228c..e18c10994 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -5,6 +5,9 @@ using Xunit; using System.Text.Json; using System.Text.Json.Serialization; +#if !NET8_0_OR_GREATER +using System.Runtime.Serialization; +#endif using GitHub.Copilot.SDK.Rpc; namespace GitHub.Copilot.SDK.Test.Unit; @@ -299,7 +302,11 @@ private static Type GetNestedType(Type containingType, string name) private static object CreateInternalRequest(Type type, params (string Name, object? Value)[] properties) { +#if NET8_0_OR_GREATER var instance = System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(type); +#else + var instance = FormatterServices.GetUninitializedObject(type); +#endif foreach (var (name, value) in properties) { From 34d6eba05fd9b3f39aa7dbc5f3e256afb1d4dd21 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 16:13:51 -0400 Subject: [PATCH 2/8] Address downlevel polyfill review feedback Narrow socket connect exception handling and use a using declaration for cancellation registration disposal in the downlevel polyfills. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Polyfills/DownlevelExtensions.cs | 37 ++++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Polyfills/DownlevelExtensions.cs b/dotnet/src/Polyfills/DownlevelExtensions.cs index a2e996ffb..d77574c5a 100644 --- a/dotnet/src/Polyfills/DownlevelExtensions.cs +++ b/dotnet/src/Polyfills/DownlevelExtensions.cs @@ -232,6 +232,7 @@ private static async Task WaitForExitAsyncCore( EventHandler handler, Threading.CancellationTokenRegistration cancellationRegistration) { + using var _ = cancellationRegistration; try { await waitTask.ConfigureAwait(false); @@ -239,7 +240,6 @@ private static async Task WaitForExitAsyncCore( finally { process.Exited -= handler; - cancellationRegistration.Dispose(); } } } @@ -377,14 +377,38 @@ public Task ConnectAsync(string host, int port, Threading.CancellationToken canc connectState.Socket.EndConnect(asyncResult); connectState.Completion.TrySetResult(null); } - catch (Exception ex) + catch (SocketException ex) + { + connectState.Completion.TrySetException(ex); + } + catch (ObjectDisposedException ex) + { + connectState.Completion.TrySetException(ex); + } + catch (InvalidOperationException ex) + { + connectState.Completion.TrySetException(ex); + } + catch (Exception ex) when (!IsFatal(ex)) { connectState.Completion.TrySetException(ex); } }, connectState); } - catch (Exception ex) + catch (SocketException ex) + { + completion.TrySetException(ex); + } + catch (ObjectDisposedException ex) + { + completion.TrySetException(ex); + } + catch (InvalidOperationException ex) + { + completion.TrySetException(ex); + } + catch (Exception ex) when (!IsFatal(ex)) { completion.TrySetException(ex); } @@ -418,8 +442,11 @@ private static async Task WaitAsync(Task task, Action cancellationAction, Thread throw new OperationCanceledException(cancellationToken); } - await task.ConfigureAwait(false); - } + await task.ConfigureAwait(false); + } + + private static bool IsFatal(Exception exception) => + exception is OutOfMemoryException or StackOverflowException or AccessViolationException or AppDomainUnloadedException; private sealed record CancellationState(TaskCompletionSource Completion, Action CancellationAction); From 4b1a66ad50ba272920c630e3cddbff6a10a7acca Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 16:32:33 -0400 Subject: [PATCH 3/8] Fix .NET multi-target restore Add an explicit target framework for the sample project now that the shared default was removed, guard target-framework compatibility checks during outer builds, and fix formatting in the downlevel polyfill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/samples/Chat.csproj | 1 + dotnet/src/GitHub.Copilot.SDK.csproj | 2 +- dotnet/src/Polyfills/DownlevelExtensions.cs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/Chat.csproj b/dotnet/samples/Chat.csproj index ad90a6062..44cccb694 100644 --- a/dotnet/samples/Chat.csproj +++ b/dotnet/samples/Chat.csproj @@ -1,5 +1,6 @@ + net8.0 Exe diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index 44f3993a3..919f121e0 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -40,7 +40,7 @@ - + diff --git a/dotnet/src/Polyfills/DownlevelExtensions.cs b/dotnet/src/Polyfills/DownlevelExtensions.cs index d77574c5a..90e5b7ca4 100644 --- a/dotnet/src/Polyfills/DownlevelExtensions.cs +++ b/dotnet/src/Polyfills/DownlevelExtensions.cs @@ -442,8 +442,8 @@ private static async Task WaitAsync(Task task, Action cancellationAction, Thread throw new OperationCanceledException(cancellationToken); } - await task.ConfigureAwait(false); - } + await task.ConfigureAwait(false); + } private static bool IsFatal(Exception exception) => exception is OutOfMemoryException or StackOverflowException or AccessViolationException or AppDomainUnloadedException; From 63b1c71841a1007fcdf15c75745d74058068a057 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 16:37:44 -0400 Subject: [PATCH 4/8] Skip Copilot CLI targets in outer builds Multi-targeted projects run an outer build with no TargetFramework. Skip the CLI download/copy targets there so clean CI builds do not require CopilotCliVersion before the generated props file is available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/build/GitHub.Copilot.SDK.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index d03a8deaa..6f4665c68 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -75,7 +75,7 @@ - + @@ -114,7 +114,7 @@ Runs whenever we have a binary to place in the output: either the user provided CopilotCliBinaryPath, or the default download path is in effect. Skipped only when CopilotSkipCliDownload=true and no CopilotCliBinaryPath was supplied. --> - + <_CopilotCacheDir Condition="'$(_CopilotCacheDir)' == ''">$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform) <_CopilotCliBinaryPath Condition="'$(_CopilotCliBinaryPath)' == ''">$(_CopilotCacheDir)\$(_CopilotBinary) @@ -127,7 +127,7 @@ - + <_CopilotCacheDir Condition="'$(_CopilotCacheDir)' == ''">$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform) <_CopilotCliBinaryPath Condition="'$(_CopilotCliBinaryPath)' == ''">$(_CopilotCacheDir)\$(_CopilotBinary) From 42fe641adf1099a7b5cd7f2789d6dc8197e70263 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 16:43:35 -0400 Subject: [PATCH 5/8] Address Copilot review feedback Fix downlevel Unix epoch and UTF-8 writing behavior, add best-effort non-Windows process tree cleanup, and avoid parallel version props generation across target frameworks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/GitHub.Copilot.SDK.csproj | 2 +- dotnet/src/Polyfills/DownlevelExtensions.cs | 119 +++++++++++++++++--- dotnet/src/Polyfills/Utf8.cs | 11 ++ 3 files changed, 116 insertions(+), 16 deletions(-) diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index 919f121e0..a93c8042d 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -59,7 +59,7 @@ - + diff --git a/dotnet/src/Polyfills/DownlevelExtensions.cs b/dotnet/src/Polyfills/DownlevelExtensions.cs index 90e5b7ca4..1935b43da 100644 --- a/dotnet/src/Polyfills/DownlevelExtensions.cs +++ b/dotnet/src/Polyfills/DownlevelExtensions.cs @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ using System.Buffers; +using System.ComponentModel; using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -56,7 +57,7 @@ internal static class DownlevelDateTimeOffsetExtensions { extension(DateTimeOffset) { - public static DateTimeOffset UnixEpoch => new(0, TimeSpan.Zero); + public static DateTimeOffset UnixEpoch => new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); } } @@ -170,23 +171,30 @@ internal static class DownlevelProcessExtensions { public void Kill(bool entireProcessTree) { - if (entireProcessTree && OperatingSystem.IsWindows()) + if (entireProcessTree) { - using var taskKill = Process.Start(new ProcessStartInfo + if (OperatingSystem.IsWindows()) { - FileName = "taskkill.exe", - Arguments = string.Format(CultureInfo.InvariantCulture, "/PID {0} /T /F", process.Id), - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - UseShellExecute = false, - }); - - if (taskKill is not null && - taskKill.WaitForExit(milliseconds: 30_000) && - (taskKill.ExitCode == 0 || process.HasExited)) + using var taskKill = Process.Start(new ProcessStartInfo + { + FileName = "taskkill.exe", + Arguments = string.Format(CultureInfo.InvariantCulture, "/PID {0} /T /F", process.Id), + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + }); + + if (taskKill is not null && + taskKill.WaitForExit(milliseconds: 30_000) && + (taskKill.ExitCode == 0 || process.HasExited)) + { + return; + } + } + else { - return; + KillDescendantProcesses(process.Id); } } @@ -242,6 +250,87 @@ private static async Task WaitForExitAsyncCore( process.Exited -= handler; } } + + private static void KillDescendantProcesses(int parentProcessId) + { + foreach (var childProcessId in GetChildProcessIds(parentProcessId)) + { + KillDescendantProcesses(childProcessId); + + try + { + using var childProcess = Process.GetProcessById(childProcessId); + if (!childProcess.HasExited) + { + childProcess.Kill(); + } + } + catch (ArgumentException) + { + } + catch (InvalidOperationException) + { + } + catch (Win32Exception) + { + } + catch (PlatformNotSupportedException) + { + } + } + } + + private static List GetChildProcessIds(int parentProcessId) + { + var childProcessIds = new List(); + + try + { + using var pgrep = Process.Start(new ProcessStartInfo + { + FileName = "pgrep", + Arguments = string.Format(CultureInfo.InvariantCulture, "-P {0}", parentProcessId), + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + }); + + if (pgrep is null) + { + return childProcessIds; + } + + var output = pgrep.StandardOutput.ReadToEnd(); + if (!pgrep.WaitForExit(milliseconds: 5_000)) + { + pgrep.Kill(); + return childProcessIds; + } + + foreach (var line in output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) + { + if (int.TryParse(line, NumberStyles.None, CultureInfo.InvariantCulture, out var childProcessId)) + { + childProcessIds.Add(childProcessId); + } + } + } + catch (ObjectDisposedException) + { + } + catch (InvalidOperationException) + { + } + catch (Win32Exception) + { + } + catch (PlatformNotSupportedException) + { + } + + return childProcessIds; + } } } diff --git a/dotnet/src/Polyfills/Utf8.cs b/dotnet/src/Polyfills/Utf8.cs index c2ca75b48..5e86b5bf9 100644 --- a/dotnet/src/Polyfills/Utf8.cs +++ b/dotnet/src/Polyfills/Utf8.cs @@ -17,6 +17,17 @@ public static bool TryWrite(Span destination, string value, out int bytesW return false; } + if (byteCount == value.Length) + { + for (var i = 0; i < value.Length; i++) + { + destination[i] = (byte)value[i]; + } + + bytesWritten = byteCount; + return true; + } + var bytes = Encoding.UTF8.GetBytes(value); bytes.CopyTo(destination); bytesWritten = byteCount; From c3e18d9f9aa3522c4b6f924d2469181d76029fea Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 16:50:14 -0400 Subject: [PATCH 6/8] Order Copilot CLI version generation Ensure the generated Copilot CLI version property is populated before the CLI download target for every target framework, and fix remaining polyfill formatting caught by dotnet format. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/GitHub.Copilot.SDK.csproj | 2 +- dotnet/src/Polyfills/DownlevelExtensions.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index a93c8042d..e447d9f5a 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -59,7 +59,7 @@ - + diff --git a/dotnet/src/Polyfills/DownlevelExtensions.cs b/dotnet/src/Polyfills/DownlevelExtensions.cs index 1935b43da..7596c08c3 100644 --- a/dotnet/src/Polyfills/DownlevelExtensions.cs +++ b/dotnet/src/Polyfills/DownlevelExtensions.cs @@ -316,12 +316,12 @@ private static List GetChildProcessIds(int parentProcessId) } } } - catch (ObjectDisposedException) - { - } - catch (InvalidOperationException) - { - } + catch (ObjectDisposedException) + { + } + catch (InvalidOperationException) + { + } catch (Win32Exception) { } From a61a3a645c6a9a511136fdd51544e591353ca177 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 16:56:40 -0400 Subject: [PATCH 7/8] Fix C# SDK CI build Ensure the Copilot CLI version is read before local SDK build targets need it, while keeping generated props creation limited to pack. Also simplifies the downlevel pgrep error handling to avoid the Linux formatting issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/GitHub.Copilot.SDK.csproj | 6 +++++- dotnet/src/Polyfills/DownlevelExtensions.cs | 11 +---------- dotnet/src/build/GitHub.Copilot.SDK.targets | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index e447d9f5a..933b51de2 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -19,6 +19,7 @@ snupkg true true + <_CopilotCliVersionTarget>_GetCopilotCliVersion @@ -59,11 +60,14 @@ - + + + + <_VersionPropsContent> diff --git a/dotnet/src/Polyfills/DownlevelExtensions.cs b/dotnet/src/Polyfills/DownlevelExtensions.cs index 7596c08c3..c105419f6 100644 --- a/dotnet/src/Polyfills/DownlevelExtensions.cs +++ b/dotnet/src/Polyfills/DownlevelExtensions.cs @@ -316,16 +316,7 @@ private static List GetChildProcessIds(int parentProcessId) } } } - catch (ObjectDisposedException) - { - } - catch (InvalidOperationException) - { - } - catch (Win32Exception) - { - } - catch (PlatformNotSupportedException) + catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException or Win32Exception or PlatformNotSupportedException) { } diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 6f4665c68..94b6515ea 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -75,7 +75,7 @@ - + From a4902f10e74c3ecdb759c0b9f344aea73f2a9a74 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 16:59:40 -0400 Subject: [PATCH 8/8] Address process polyfill review feedback Make the descendant process parsing filter explicit and replace empty best-effort cleanup catches with debug diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Polyfills/DownlevelExtensions.cs | 33 ++++++++++----------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/dotnet/src/Polyfills/DownlevelExtensions.cs b/dotnet/src/Polyfills/DownlevelExtensions.cs index c105419f6..80aaa5bbb 100644 --- a/dotnet/src/Polyfills/DownlevelExtensions.cs +++ b/dotnet/src/Polyfills/DownlevelExtensions.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -265,17 +266,9 @@ private static void KillDescendantProcesses(int parentProcessId) childProcess.Kill(); } } - catch (ArgumentException) - { - } - catch (InvalidOperationException) - { - } - catch (Win32Exception) - { - } - catch (PlatformNotSupportedException) + catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or Win32Exception or PlatformNotSupportedException) { + IgnoreBestEffortProcessException(ex); } } } @@ -308,20 +301,26 @@ private static List GetChildProcessIds(int parentProcessId) return childProcessIds; } - foreach (var line in output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) - { - if (int.TryParse(line, NumberStyles.None, CultureInfo.InvariantCulture, out var childProcessId)) - { - childProcessIds.Add(childProcessId); - } - } + childProcessIds.AddRange( + output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(static line => + { + var success = int.TryParse(line, NumberStyles.None, CultureInfo.InvariantCulture, out var childProcessId); + return (success, childProcessId); + }) + .Where(static result => result.success) + .Select(static result => result.childProcessId)); } catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException or Win32Exception or PlatformNotSupportedException) { + IgnoreBestEffortProcessException(ex); } return childProcessIds; } + + private static void IgnoreBestEffortProcessException(Exception exception) => + Debug.WriteLine(exception.ToString()); } }