/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ using GitHub.Copilot.Rpc; using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace GitHub.Copilot; /// /// Result of a SQLite query execution via . /// Same shape as but without the Error field, /// since providers signal errors by throwing. /// public sealed class SessionFsSqliteResult { /// Column names from the result set. public IList Columns { get; set; } = []; /// For SELECT: rows as column-keyed dictionaries. For others: empty. public IList> Rows { get; set; } = []; /// Number of rows affected (for INSERT/UPDATE/DELETE). public long RowsAffected { get; set; } /// Last inserted row ID (for INSERT). public long? LastInsertRowid { get; set; } } /// /// One statement in an atomic SQLite transaction passed to /// . /// [Experimental(Diagnostics.Experimental)] public sealed class SessionFsSqliteStatement { /// How to execute: "exec", "query", or "run". public SessionFsSqliteQueryType QueryType { get; set; } /// SQL statement to execute. public string Query { get; set; } = string.Empty; /// Optional named bind parameters. public IDictionary? Params { get; set; } } /// /// Optional interface for subclasses that support /// per-session SQLite databases. Implement this interface on your provider to enable /// the runtime's SQL tool to route queries through your SessionFs implementation. /// public interface ISessionFsSqliteProvider { /// /// Executes a SQLite query against the per-session database. /// /// How to execute: "exec" for DDL/multi-statement, "query" for SELECT, "run" for INSERT/UPDATE/DELETE. /// SQL query to execute. /// Optional named bind parameters. /// Cancellation token. /// The query result, or null for exec-type queries. Task QueryAsync( SessionFsSqliteQueryType queryType, string query, IDictionary? bindParams, CancellationToken cancellationToken); /// /// Checks whether the per-session SQLite database already exists, without creating it. /// /// Cancellation token. Task ExistsAsync(CancellationToken cancellationToken); } /// /// Optional capability for session filesystem providers that support atomic SQLite transactions. /// public interface ISessionFsSqliteTransactionProvider { /// /// Executes atomically against the per-session database. /// /// Statements to execute in order, inside a single transaction. /// Cancellation token. /// One result per statement, in the same order as . /// /// Thrown to tell the runtime how the failure should be classified. Any other exception /// is reported as . /// Task> TransactionAsync( IList statements, CancellationToken cancellationToken); } /// /// Thrown by an to classify a failed SQLite transaction. /// guarantees the transaction /// rolled back and is safe to retry; /// must never be retried. /// [Experimental(Diagnostics.Experimental)] public sealed class SessionFsSqliteTransactionException : Exception { /// Initializes a new instance of the class. /// Human-readable failure description. /// How the runtime should classify the failure. /// Optional underlying exception. public SessionFsSqliteTransactionException( string message, SessionFsSqliteTransactionErrorClass errorClass, Exception? innerException = null) : base(message, innerException) { ErrorClass = errorClass; } /// Gets the failure classification reported to the runtime. public SessionFsSqliteTransactionErrorClass ErrorClass { get; } } /// /// Base class for session filesystem providers. Subclasses override the /// virtual methods and use normal C# patterns (return values, throw exceptions). /// The base class catches exceptions and converts them to /// results expected by the runtime. /// To add SQLite support, also implement . /// public abstract class SessionFsProvider : ISessionFsHandler { /// Reads the full content of a file. Throw if the file does not exist. /// SessionFs-relative path. /// Cancellation token. /// The file content as a UTF-8 string. protected abstract Task ReadFileAsync(string path, CancellationToken cancellationToken); /// Writes content to a file, creating it (and parent directories) if needed. /// SessionFs-relative path. /// Content to write. /// Optional POSIX-style permission mode. Null means use OS default. /// Cancellation token. protected abstract Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken); /// Appends content to a file, creating it (and parent directories) if needed. /// SessionFs-relative path. /// Content to append. /// Optional POSIX-style permission mode. Null means use OS default. /// Cancellation token. protected abstract Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken); /// Checks whether a path exists. /// SessionFs-relative path. /// Cancellation token. /// true if the path exists, false otherwise. protected abstract Task ExistsAsync(string path, CancellationToken cancellationToken); /// Gets metadata about a file or directory. Throw if the path does not exist. /// SessionFs-relative path. /// Cancellation token. protected abstract Task StatAsync(string path, CancellationToken cancellationToken); /// Creates a directory (and optionally parents). Does not fail if it already exists. /// SessionFs-relative path. /// Whether to create parent directories. /// Optional POSIX-style permission mode (e.g., 0x1FF for 0777). Null means use OS default. /// Cancellation token. protected abstract Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken); /// Lists entry names in a directory. Throw if the directory does not exist. /// SessionFs-relative path. /// Cancellation token. protected abstract Task> ReadDirectoryAsync(string path, CancellationToken cancellationToken); /// Lists entries with type info in a directory. Throw if the directory does not exist. /// SessionFs-relative path. /// Cancellation token. protected abstract Task> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken); /// Removes a file or directory. Throw if the path does not exist (unless is true). /// SessionFs-relative path. /// Whether to remove directory contents recursively. /// If true, do not throw when the path does not exist. /// Cancellation token. protected abstract Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken); /// Renames/moves a file or directory. /// Source path. /// Destination path. /// Cancellation token. protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken); // ---- ISessionFsHandler implementation (private, handles error mapping) ---- async Task ISessionFsHandler.ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { var content = await ReadFileAsync(request.Path, cancellationToken).ConfigureAwait(false); return new SessionFsReadFileResult { Content = content }; } catch (Exception ex) { return new SessionFsReadFileResult { Error = ToSessionFsError(ex) }; } } async Task ISessionFsHandler.WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { await WriteFileAsync(request.Path, request.Content, (int?)request.Mode, cancellationToken).ConfigureAwait(false); return null; } catch (Exception ex) { return ToSessionFsError(ex); } } async Task ISessionFsHandler.AppendFileAsync(SessionFsAppendFileRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { await AppendFileAsync(request.Path, request.Content, (int?)request.Mode, cancellationToken).ConfigureAwait(false); return null; } catch (Exception ex) { return ToSessionFsError(ex); } } async Task ISessionFsHandler.ExistsAsync(SessionFsExistsRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { var exists = await ExistsAsync(request.Path, cancellationToken).ConfigureAwait(false); return new SessionFsExistsResult { Exists = exists }; } catch { return new SessionFsExistsResult { Exists = false }; } } async Task ISessionFsHandler.StatAsync(SessionFsStatRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { return await StatAsync(request.Path, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { return new SessionFsStatResult { Error = ToSessionFsError(ex) }; } } async Task ISessionFsHandler.MkdirAsync(SessionFsMkdirRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { await MakeDirectoryAsync(request.Path, request.Recursive ?? false, (int?)request.Mode, cancellationToken).ConfigureAwait(false); return null; } catch (Exception ex) { return ToSessionFsError(ex); } } async Task ISessionFsHandler.ReaddirAsync(SessionFsReaddirRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { var entries = await ReadDirectoryAsync(request.Path, cancellationToken).ConfigureAwait(false); return new SessionFsReaddirResult { Entries = entries }; } catch (Exception ex) { return new SessionFsReaddirResult { Error = ToSessionFsError(ex) }; } } async Task ISessionFsHandler.ReaddirWithTypesAsync(SessionFsReaddirWithTypesRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { var entries = await ReadDirectoryWithTypesAsync(request.Path, cancellationToken).ConfigureAwait(false); return new SessionFsReaddirWithTypesResult { Entries = entries }; } catch (Exception ex) { return new SessionFsReaddirWithTypesResult { Error = ToSessionFsError(ex) }; } } async Task ISessionFsHandler.RmAsync(SessionFsRmRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { await RemoveAsync(request.Path, request.Recursive ?? false, request.Force ?? false, cancellationToken).ConfigureAwait(false); return null; } catch (Exception ex) { return ToSessionFsError(ex); } } async Task ISessionFsHandler.RenameAsync(SessionFsRenameRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); try { await RenameAsync(request.Src, request.Dest, cancellationToken).ConfigureAwait(false); return null; } catch (Exception ex) { return ToSessionFsError(ex); } } async Task ISessionFsHandler.SqliteQueryAsync(SessionFsSqliteQueryRequest request, CancellationToken cancellationToken) { if (this is not ISessionFsSqliteProvider sqliteProvider) { return new SessionFsSqliteQueryResult { Error = new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "SQLite is not supported by this provider." }, }; } try { var bindParams = request.Params?.ToDictionary( kvp => kvp.Key, kvp => JsonElementToValue(kvp.Value)); var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, bindParams, cancellationToken).ConfigureAwait(false); return new SessionFsSqliteQueryResult { Rows = result?.Rows?.Select(row => (IDictionary)row.ToDictionary( kvp => kvp.Key, kvp => ToJsonElement(kvp.Value))).ToList() ?? [], Columns = result?.Columns ?? [], RowsAffected = result?.RowsAffected ?? 0, LastInsertRowid = result?.LastInsertRowid, }; } catch (Exception ex) { return new SessionFsSqliteQueryResult { Error = ToSessionFsError(ex) }; } } async Task ISessionFsHandler.SqliteTransactionAsync(SessionFsSqliteTransactionRequest request, CancellationToken cancellationToken) { if (this is not ISessionFsSqliteTransactionProvider transactionProvider) { return new SessionFsSqliteTransactionResult { Error = new SessionFsSqliteTransactionError { ErrorClass = SessionFsSqliteTransactionErrorClass.Fatal, Message = "SQLite is not supported by this provider.", }, }; } IList results; try { var statements = request.Statements.Select(statement => new SessionFsSqliteStatement { QueryType = statement.QueryType, Query = statement.Query, Params = statement.Params?.ToDictionary(kvp => kvp.Key, kvp => JsonElementToValue(kvp.Value)), }).ToList(); results = await transactionProvider.TransactionAsync(statements, cancellationToken).ConfigureAwait(false); } catch (SessionFsSqliteTransactionException ex) { return new SessionFsSqliteTransactionResult { Error = new SessionFsSqliteTransactionError { ErrorClass = ex.ErrorClass, Message = ex.Message }, }; } catch (Exception ex) { return new SessionFsSqliteTransactionResult { Error = new SessionFsSqliteTransactionError { ErrorClass = SessionFsSqliteTransactionErrorClass.Fatal, Message = ex.Message, }, }; } try { return new SessionFsSqliteTransactionResult { Results = results.Select(result => new SessionFsSqliteQueryResult { Rows = result.Rows?.Select(row => (IDictionary)row.ToDictionary( kvp => kvp.Key, kvp => ToJsonElement(kvp.Value))).ToList() ?? [], Columns = result.Columns ?? [], RowsAffected = result.RowsAffected, LastInsertRowid = result.LastInsertRowid, }).ToList(), }; } catch (Exception ex) { return new SessionFsSqliteTransactionResult { Error = new SessionFsSqliteTransactionError { ErrorClass = SessionFsSqliteTransactionErrorClass.PostCommitAmbiguous, Message = ex.Message, }, }; } } async Task ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken) { if (this is not ISessionFsSqliteProvider sqliteProvider) { return new SessionFsSqliteExistsResult { Exists = false }; } try { var exists = await sqliteProvider.ExistsAsync(cancellationToken).ConfigureAwait(false); return new SessionFsSqliteExistsResult { Exists = exists }; } catch { return new SessionFsSqliteExistsResult { Exists = false }; } } private static SessionFsError ToSessionFsError(Exception ex) { var code = ex is FileNotFoundException or DirectoryNotFoundException ? SessionFsErrorCode.ENOENT : SessionFsErrorCode.UNKNOWN; return new SessionFsError { Code = code, Message = ex.Message }; } private static JsonElement ToJsonElement(object? value) => CopilotClient.ToJsonElementForWire(value) ?? JsonElement.Parse("null"); private static object? JsonElementToValue(JsonElement element) => element.ValueKind switch { JsonValueKind.Null => null, JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.String => element.GetString(), JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(), _ => element.GetRawText(), }; }