/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ using GitHub.Copilot.Rpc; 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; } } /// /// 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); } /// /// 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 => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [], Columns = result?.Columns ?? [], RowsAffected = result?.RowsAffected ?? 0, LastInsertRowid = result?.LastInsertRowid, }; } catch (Exception ex) { return new SessionFsSqliteQueryResult { Error = ToSessionFsError(ex) }; } } 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 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(), }; }