Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,39 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

---

## Third-Party Notices

### highlight.js

BSD 3-Clause License

Copyright (c) 2006, Ivan Sagalaev.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 changes: 26 additions & 0 deletions src/LightQueryProfiler.JsonRpc/JsonRpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using LightQueryProfiler.Shared.Services;
using LightQueryProfiler.Shared.Services.Interfaces;
using Microsoft.Extensions.Logging;
using StreamJsonRpc;

namespace LightQueryProfiler.JsonRpc;

Expand All @@ -29,6 +30,13 @@ public JsonRpcServer(ILogger<JsonRpcServer> logger)
/// <summary>
/// Starts a profiling session with the specified parameters
/// </summary>
/// <remarks>
/// UseSingleObjectParameterDeserialization = true is required because the TypeScript
/// client sends the three fields (SessionName, EngineType, ConnectionString) as a
/// single JSON object. Without it StreamJsonRpc tries to match them as three
/// positional parameters and throws "Unable to find method 'StartProfilingAsync/3'".
/// </remarks>
[JsonRpcMethod("StartProfilingAsync", UseSingleObjectParameterDeserialization = true)]
public async Task StartProfilingAsync(StartProfilingRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
Expand Down Expand Up @@ -92,6 +100,12 @@ await Task.Run(() => profilerService.StartProfiling(request.SessionName, templat
/// <summary>
/// Stops the specified profiling session
/// </summary>
/// <remarks>
/// UseSingleObjectParameterDeserialization = true is required because the TypeScript
/// client sends the fields as a single JSON object. Without it StreamJsonRpc tries
/// to match them as positional parameters and throws "Unable to find method".
/// </remarks>
[JsonRpcMethod("StopProfilingAsync", UseSingleObjectParameterDeserialization = true)]
public async Task StopProfilingAsync(StopProfilingRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
Expand Down Expand Up @@ -138,6 +152,12 @@ await Task.Run(() => profilerService.StopProfiling(request.SessionName), cancell
/// <summary>
/// Retrieves the latest events from the specified profiling session
/// </summary>
/// <remarks>
/// UseSingleObjectParameterDeserialization = true is required because the TypeScript
/// client sends the fields as a single JSON object. Without it StreamJsonRpc tries
/// to match them as positional parameters and throws "Unable to find method".
/// </remarks>
[JsonRpcMethod("GetLastEventsAsync", UseSingleObjectParameterDeserialization = true)]
public async Task<List<ProfilerEventDto>> GetLastEventsAsync(GetEventsRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
Expand Down Expand Up @@ -190,6 +210,12 @@ public async Task<List<ProfilerEventDto>> GetLastEventsAsync(GetEventsRequest re
/// <summary>
/// Pauses the specified profiling session (not yet implemented in ProfilerService)
/// </summary>
/// <remarks>
/// UseSingleObjectParameterDeserialization = true is required because the TypeScript
/// client sends the fields as a single JSON object. Without it StreamJsonRpc tries
/// to match them as positional parameters and throws "Unable to find method".
/// </remarks>
[JsonRpcMethod("PauseProfilingAsync", UseSingleObjectParameterDeserialization = true)]
public async Task PauseProfilingAsync(StopProfilingRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
Expand Down
16 changes: 11 additions & 5 deletions src/LightQueryProfiler.JsonRpc/Models/ProfilerEventDto.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
namespace LightQueryProfiler.JsonRpc.Models;

/// <summary>
/// Data transfer object for profiler events (JSON-RPC serializable)
/// Data transfer object for profiler events (JSON-RPC serializable).
/// </summary>
/// <remarks>
/// Property names are serialized as camelCase via the <c>CamelCasePropertyNamesContractResolver</c>
/// configured on the <c>JsonMessageFormatter</c> in <c>Program.cs</c>. This ensures the TypeScript
/// client receives <c>name</c>, <c>timestamp</c>, <c>fields</c>, <c>actions</c> to match the
/// <c>ProfilerEvent</c> interface.
/// </remarks>
public record ProfilerEventDto
{
/// <summary>
/// Event name
/// Event name (e.g., <c>sql_batch_completed</c>)
/// </summary>
public string? Name { get; init; }

/// <summary>
/// Event timestamp
/// Event timestamp in ISO 8601 format
/// </summary>
public string? Timestamp { get; init; }

/// <summary>
/// Event fields
/// Event fields containing query-specific data (keyed by field name, values are strings)
/// </summary>
public Dictionary<string, object?>? Fields { get; init; }

/// <summary>
/// Event actions
/// Event actions containing session and context data (keyed by action name, values are strings)
/// </summary>
public Dictionary<string, object?>? Actions { get; init; }
}
57 changes: 43 additions & 14 deletions src/LightQueryProfiler.JsonRpc/Program.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
using LightQueryProfiler.JsonRpc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
using StreamJsonRpc;
using System.Diagnostics;

// Setup logging
// ─────────────────────────────────────────────────────────────────────────────
// CRITICAL: Capture stdin/stdout raw streams BEFORE anything touches Console.
//
// Background:
// stdout is owned exclusively by the StreamJsonRpc framing protocol.
// The .NET Console class wraps the underlying OS streams with a TextWriter
// that, on Windows, is placed into "synchronised" (non-async) mode the first
// time Console.Out/Console.Write is called. Once that happens the underlying
// Stream returned by Console.OpenStandardOutput() reports CanWrite = false,
// which causes StreamJsonRpc to throw:
// "System.ArgumentException: Stream must be writable (Parameter 'stream')"
//
// Solution: open the raw streams immediately at program start, before ANY
// logging or Console API calls, then redirect all diagnostic output to stderr.
// ─────────────────────────────────────────────────────────────────────────────
var rawStdin = Console.OpenStandardInput();
var rawStdout = Console.OpenStandardOutput();

// Redirect stderr for diagnostic logging (stdout belongs to JSON-RPC).
Console.SetOut(System.IO.TextWriter.Null); // silence any accidental Console.Write

// Setup logging → stderr only
var services = new ServiceCollection();
services.AddLogging(builder =>
{
builder.AddConsole();
builder.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.SetMinimumLevel(LogLevel.Information);
});

var serviceProvider = services.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<Program>();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<Program>();

logger.LogInformation("LightQueryProfiler JSON-RPC Server starting...");

// Setup cancellation token for graceful shutdown
// Graceful-shutdown token
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (sender, e) =>
Console.CancelKeyPress += (_, e) =>
{
logger.LogInformation("Shutdown signal received");
e.Cancel = true;
Expand All @@ -29,21 +53,26 @@

try
{
// Create JSON-RPC server instance
var jsonRpcLogger = loggerFactory.CreateLogger<JsonRpcServer>();
var rpcServer = new JsonRpcServer(jsonRpcLogger);
var rpcServer = new JsonRpcServer(jsonRpcLogger);

// Setup JSON-RPC over stdin/stdout
using var jsonRpc = new JsonRpc(Console.OpenStandardInput(), Console.OpenStandardOutput(), rpcServer);
// Configure Newtonsoft.Json camelCase so property names on the wire match
// the TypeScript ProfilerEvent interface (name, timestamp, fields, actions).
var formatter = new JsonMessageFormatter();
formatter.JsonSerializer.ContractResolver = new CamelCasePropertyNamesContractResolver();

// Configure JSON-RPC options
// Use the pre-captured raw streams — both are guaranteed writable/readable.
using var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(rawStdout, rawStdin, formatter), rpcServer);
jsonRpc.CancelLocallyInvokedMethodsWhenConnectionIsClosed = true;

// Start listening
jsonRpc.StartListening();
logger.LogInformation("JSON-RPC Server listening on stdin/stdout");

// Wait for completion or cancellation
// Signal TypeScript host that the channel is ready.
// stderr is used because stdout is owned by StreamJsonRpc framing.
await Console.Error.WriteLineAsync("READY").ConfigureAwait(false);
await Console.Error.FlushAsync().ConfigureAwait(false);

await jsonRpc.Completion.WaitAsync(cts.Token).ConfigureAwait(false);

logger.LogInformation("JSON-RPC Server shutting down gracefully");
Expand Down
1 change: 1 addition & 0 deletions vscode-extension/media/highlight-vs2015.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"database"
],
"activationEvents": [
"onCommand:lightQueryProfiler.showProfiler"
"onStartupFinished"
],
"main": "./dist/extension.js",
"contributes": {
Expand Down
Loading
Loading