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/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/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..933b51de2 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,11 +14,12 @@ https://github.com/github/copilot-sdk copilot.png github;copilot;sdk;jsonrpc;agent - true + true true snupkg true true + <_CopilotCliVersionTarget>_GetCopilotCliVersion @@ -37,15 +39,35 @@ + + + + + + + + + + + + + + + + + - + + + + <_VersionPropsContent> 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..80aaa5bbb --- /dev/null +++ b/dotnet/src/Polyfills/DownlevelExtensions.cs @@ -0,0 +1,591 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Buffers; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +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(1970, 1, 1, 0, 0, 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) + { + if (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; + } + } + else + { + KillDescendantProcesses(process.Id); + } + } + + 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) + { + using var _ = cancellationRegistration; + try + { + await waitTask.ConfigureAwait(false); + } + finally + { + 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 (Exception ex) when (ex is ArgumentException or InvalidOperationException or Win32Exception or PlatformNotSupportedException) + { + IgnoreBestEffortProcessException(ex); + } + } + } + + 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; + } + + 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()); + } +} + +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 (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 (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); + } + + 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 static bool IsFatal(Exception exception) => + exception is OutOfMemoryException or StackOverflowException or AccessViolationException or AppDomainUnloadedException; + + 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..5e86b5bf9 --- /dev/null +++ b/dotnet/src/Polyfills/Utf8.cs @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + } + + 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; + 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/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index d03a8deaa..94b6515ea 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) 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) {