Skip to content
Open
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
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ The extension activates when:
```json
"tridentCoverage.showExecutionCount": true, // Show execution counts
"tridentCoverage.executionCountColor": "CYAN", // Choose color
"tridentCoverage.coverageServerPort": 58432 // Server port for dynamic coverage
```

To display coverage, use the guidance on [this page](https://ackee.xyz/trident/docs/latest/trident-advanced/code-coverage/).
Expand Down Expand Up @@ -119,7 +118,6 @@ The extension will automatically find coverage reports in `trident-tests` and vi
- `server.path`: Path to the Solana language server binary (leave empty to use bundled version)
- `tridentCoverage.showExecutionCount`: Show execution count numbers next to covered statements
- `tridentCoverage.executionCountColor`: Color of the execution count display
- `tridentCoverage.coverageServerPort`: Port for the coverage server

## Feedback, help, and news

Expand Down
4 changes: 2 additions & 2 deletions extension/package-lock.json

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

5 changes: 0 additions & 5 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,6 @@
"default": "CYAN",
"description": "Color of the execution count display",
"format": "color"
},
"tridentCoverage.coverageServerPort": {
"type": "number",
"default": 58432,
"description": "Port for the coverage server"
}
}
}
Expand Down
3 changes: 1 addition & 2 deletions extension/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const ExtensionConfigurationConstants = {
CONFIG_ID: "tridentCoverage",
SHOW_EXECUTION_COUNT: "showExecutionCount",
EXECUTION_COUNT_COLOR: "executionCountColor",
COVERAGE_SERVER_PORT: "coverageServerPort",
}
};

export { ExtensionConfigurationConstants };
262 changes: 163 additions & 99 deletions extension/src/coverage/coverageServer.ts
Original file line number Diff line number Diff line change
@@ -1,125 +1,189 @@
import * as http from "http";
import * as vscode from "vscode";
import * as net from "net";
import { SOLANA_OUTPUT_CHANNEL } from "../output";
import { EventEmitter } from "events";

import { CoverageServerConstants } from "./constants";
import { ExtensionConfigurationConstants } from "../constants";

const { DEFAULT_COVERAGE_SERVER_PORT, UPDATE_DECORATIONS, SETUP_DYNAMIC_COVERAGE, DISPLAY_FINAL_REPORT } = CoverageServerConstants;
const { CONFIG_ID, COVERAGE_SERVER_PORT } = ExtensionConfigurationConstants;
const {
DEFAULT_COVERAGE_SERVER_PORT,
UPDATE_DECORATIONS,
SETUP_DYNAMIC_COVERAGE,
DISPLAY_FINAL_REPORT,
} = CoverageServerConstants;

/**
* HTTP server that receives coverage notifications from external processes
* Extends EventEmitter to notify coverage manager of incoming requests
* Handles JSON payloads and routes requests based on URL endpoints
*/
class CoverageServer extends EventEmitter {
/** HTTP server instance */
private server: http.Server;
/** Port number for the HTTP server */
private port: number;
/** Output channel for logging */
private outputChannel: vscode.OutputChannel;

/** HTTP server instance */
private server: http.Server;
/** Port number for the HTTP server */
private port: number;

/**
* Override emit to also emit an 'any' event with the event name
* This allows the coverage manager to listen for all events with a single listener
* @param event - The event name to emit
* @param args - Arguments to pass with the event
* @returns {boolean} True if the event had listeners, false otherwise
*/
emit(event: string | symbol, ...args: any[]): boolean {
const result = super.emit(event, ...args);
if (event !== 'any') {
super.emit('any', event, ...args);
}
return result;
/**
* Override emit to also emit an 'any' event with the event name
* This allows the coverage manager to listen for all events with a single listener
* @param event - The event name to emit
* @param args - Arguments to pass with the event
* @returns {boolean} True if the event had listeners, false otherwise
*/
emit(event: string | symbol, ...args: any[]): boolean {
const result = super.emit(event, ...args);
if (event !== "any") {
super.emit("any", event, ...args);
}
return result;
}

/**
* Creates a new CoverageServer instance and starts the HTTP server
* Reads the port configuration from VS Code settings
*/
constructor() {
super();
this.port = vscode.workspace.getConfiguration(CONFIG_ID).get(COVERAGE_SERVER_PORT, DEFAULT_COVERAGE_SERVER_PORT);
this.server = this.setupServer();
}
/**
* Creates a new CoverageServer instance and starts the HTTP server
* Reads the port configuration from VS Code settings
*/
constructor() {
super();
this.port = DEFAULT_COVERAGE_SERVER_PORT;
this.outputChannel = SOLANA_OUTPUT_CHANNEL;
this.server = this.setupServer();
}

/**
* Disposes of the HTTP server and closes all connections
* @public
*/
public dispose() {
this.server.close();
}
/**
* Disposes of the HTTP server and closes all connections
* @public
*/
public dispose() {
this.server.close();
}

/**
* Sets up and configures the HTTP server to handle POST requests
* Parses JSON request bodies and routes requests to appropriate handlers
* @private
* @returns {http.Server} The configured HTTP server instance
*/
private setupServer(): http.Server {
this.server = http.createServer((req, res) => {
if (req.method !== "POST") {
console.error(`Invalid request method: ${req.method}`);
res.writeHead(405);
res.end();
return;
}

let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});

req.on("end", () => {
try {
const data = body ? JSON.parse(body) : {};
this.handleNotification(req, data);
} catch (error) {
console.error("Error parsing JSON:", error);
this.handleNotification(req, {});
}

/**
* Sets up and configures the HTTP server to handle POST requests
* Parses JSON request bodies and routes requests to appropriate handlers
* @private
* @returns {http.Server} The configured HTTP server instance
*/
private setupServer(): http.Server {
this.server = http.createServer((req, res) => {
if (req.method !== 'POST') {
console.error(`Invalid request method: ${req.method}`);
res.writeHead(405);
res.end();
return;
}

let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});

req.on('end', () => {
try {
const data = body ? JSON.parse(body) : {};
this.handleNotification(req, data);
} catch (error) {
console.error('Error parsing JSON:', error);
this.handleNotification(req, {});
}

res.writeHead(200);
res.end();
});
res.writeHead(200);
res.end();
});
});

this.outputChannel.appendLine(
`Starting coverage server on port ${this.port}`
);

this.findAvailablePort(this.port)
.then((freePort) => {
this.port = freePort;
this.server.listen(this.port, "localhost", () => {
const message = `Coverage server running on port: ${this.port}`;
console.log(message);
this.outputChannel.appendLine(message);
vscode.window.showInformationMessage(message);
});

this.server.listen(this.port, 'localhost', () => {
console.log(`Coverage server running on port: ${this.port}`);
})
.catch((error) => {
console.error(`Failed to start coverage server: ${error}`);
this.outputChannel.appendLine(
`Failed to start coverage server: ${error}`
);
});

// Keep a generic error handler for runtime errors; ignore EADDRINUSE which is handled above
this.server.on("error", (error: any) => {
if ((error as NodeJS.ErrnoException).code === "EADDRINUSE") {
return;
}
console.error(`HTTP server error: ${error}`);
});

return this.server;
}

/**
* Finds an available port by iteratively probing upward from the starting port
* @param {number} startPort - The starting port to probe from
* @returns {Promise<number>} The available port number
*/
private async findAvailablePort(startPort: number): Promise<number> {
let port = startPort;
while (true) {
const available = await new Promise<boolean>((resolve) => {
const tester = net.createServer();
tester.once("error", (err: any) => {
if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") {
this.outputChannel.appendLine(
`Coverage server port ${port} in use; trying ${port + 1}...`
);
tester.close(() => resolve(false));
} else {
this.outputChannel.appendLine(
`HTTP port check error on ${port}: ${err}`
);
tester.close(() => resolve(false));
}
});

this.server.on('error', (error) => {
console.error(`HTTP server error: ${error}`);
tester.once("listening", () => {
tester.close(() => resolve(true));
});
tester.listen(port, "localhost");
});

return this.server;
if (available) {
return port;
}
port += 1;
}
}

/**
* Handles incoming HTTP notification requests and emits appropriate events
* Routes requests based on URL path and emits events for the coverage manager
* @private
* @param {http.IncomingMessage} req - The incoming HTTP request
* @param {any} data - Parsed JSON data from the request body
*/
private handleNotification(req: http.IncomingMessage, data: any) {
switch (req.url) {
case SETUP_DYNAMIC_COVERAGE:
this.emit(SETUP_DYNAMIC_COVERAGE);
break;
case UPDATE_DECORATIONS:
this.emit(UPDATE_DECORATIONS);
break;
case DISPLAY_FINAL_REPORT:
this.emit(DISPLAY_FINAL_REPORT, data);
break;
default:
console.error(`Invalid request URL: ${req.url}`);
}
}
/**
* Handles incoming HTTP notification requests and emits appropriate events
* Routes requests based on URL path and emits events for the coverage manager
* @private
* @param {http.IncomingMessage} req - The incoming HTTP request
* @param {any} data - Parsed JSON data from the request body
*/
private handleNotification(req: http.IncomingMessage, data: any) {
switch (req.url) {
case SETUP_DYNAMIC_COVERAGE:
this.emit(SETUP_DYNAMIC_COVERAGE);
break;
case UPDATE_DECORATIONS:
this.emit(UPDATE_DECORATIONS);
break;
case DISPLAY_FINAL_REPORT:
this.emit(DISPLAY_FINAL_REPORT, data);
break;
default:
console.error(`Invalid request URL: ${req.url}`);
}
}
}

export { CoverageServer };
export { CoverageServer };
3 changes: 2 additions & 1 deletion extension/src/detectors/detectorsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import * as vscode from 'vscode';
import { SOLANA_OUTPUT_CHANNEL } from '../output';

// Interface for scan summary data from language server
interface ScanSummary {
Expand All @@ -30,7 +31,7 @@ export class DetectorsManager {

constructor() {
console.log('Security Server initialized');
this.outputChannel = window.createOutputChannel('Solana Extension');
this.outputChannel = SOLANA_OUTPUT_CHANNEL;

// Improved server path resolution
const serverPath = this.resolveServerPath();
Expand Down
7 changes: 7 additions & 0 deletions extension/src/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as vscode from "vscode";

// Shared output channel used across the extension
const SOLANA_OUTPUT_CHANNEL =
vscode.window.createOutputChannel("Solana Extension");

export { SOLANA_OUTPUT_CHANNEL };
Loading