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
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@
"title": "Install/Repair CodeCarbon Python package",
"category": "Codecarbon",
"command": "codecarbon.installRepair"
},
{
"title": "Open CodeCarbon config",
"category": "Codecarbon",
"command": "codecarbon.openConfig"
}
]
},
Expand All @@ -126,4 +131,4 @@
"prettier": "^3.8.1",
"typescript": "^5.9.3"
}
}
}
38 changes: 38 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Main entry point for the CodeCarbon VSCode extension.
*/
import * as vscode from 'vscode';
import * as fs from 'fs/promises';
import * as path from 'path';
import { LogService } from './services/logService';
import { TrackerService } from './services/trackerService';
import { PythonService } from './services/pythonService';
Expand Down Expand Up @@ -43,6 +45,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
vscode.commands.registerCommand(COMMANDS.STOP, () => stopTracker()),
vscode.commands.registerCommand(COMMANDS.CHECK_VERSION, () => checkCodecarbonVersion()),
vscode.commands.registerCommand(COMMANDS.INSTALL_REPAIR, () => installRepairCodecarbon()),
vscode.commands.registerCommand(COMMANDS.OPEN_CONFIG, () => openCodecarbonConfig()),
);

await pythonService.checkInstallHealthOnStartup(ConfigService.getPythonPath());
Expand Down Expand Up @@ -84,3 +87,38 @@ async function installRepairCodecarbon(): Promise<void> {
const pythonPath = ConfigService.getPythonPath();
await pythonService.installOrRepairCodecarbon(pythonPath);
}

async function openCodecarbonConfig(): Promise<void> {
const workspacePath = ConfigService.getWorkspaceFolderPath();
if (!workspacePath) {
vscode.window.showErrorMessage('No workspace folder is open. Open a folder to create/edit .codecarbon.config.');
return;
}
const resolvedPath = path.join(workspacePath, '.codecarbon.config');

try {
await vscode.workspace.fs.stat(vscode.Uri.file(resolvedPath));
} catch {
const createAction = 'Create config';
const selected = await vscode.window.showInformationMessage(
`No CodeCarbon config found at ${resolvedPath}. Create a minimal template?`,
createAction,
);
if (selected !== createAction) {
return;
}

const template = [
'[codecarbon]',
'# Official docs: https://mlco2.github.io/codecarbon/usage.html#configuration',
'measure_power_secs = 5',
'',
].join('\n');

await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
await fs.writeFile(resolvedPath, template, 'utf8');
}

const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(resolvedPath));
await vscode.window.showTextDocument(doc, { preview: false });
}
1 change: 1 addition & 0 deletions src/scripts/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def signal_handler(sig, frame):
if len(sys.argv) < 2:
print("Please provide 'start' as an argument.")
sys.exit(1)

print("Starting the tracker...")
command = sys.argv[1].lower()
if command == "start":
Expand Down
27 changes: 27 additions & 0 deletions src/services/pythonLogRouting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type PythonLogLevel = 'info' | 'warn' | 'error';

export interface RoutedPythonLogLine {
level: PythonLogLevel;
message: string;
parsePayload: string;
}

export function routePythonLogLine(line: string, fromStderr: boolean): RoutedPythonLogLine {
const levelMatch = line.match(/^\[codecarbon\s+(debug|info|warning|error|critical)\s+@\s*[^\]]+\]\s*(.*)$/i);
const level = levelMatch?.[1]?.toLowerCase();
const message = (levelMatch ? levelMatch[2] : line).trim() || line;

if (level === 'debug' || level === 'info') {
return { level: 'info', message, parsePayload: `${line}\n` };
}
if (level === 'warning') {
return { level: 'warn', message, parsePayload: `${line}\n` };
}
if (level === 'error' || level === 'critical') {
return { level: 'error', message, parsePayload: `${line}\n` };
}
if (fromStderr) {
return { level: 'error', message: `Python error: ${line}`, parsePayload: `${line}\n` };
}
return { level: 'info', message: line, parsePayload: `${line}\n` };
}
28 changes: 24 additions & 4 deletions src/services/trackerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { LogService } from './logService';
import { PythonService } from './pythonService';
import { ConfigService } from '../utils/config';
import { MESSAGES } from '../utils/constants';
import { routePythonLogLine } from './pythonLogRouting';

const TRACKER_SCRIPT = path.resolve(__dirname, '../scripts/tracker.py');

Expand Down Expand Up @@ -54,6 +55,10 @@ export class TrackerService {
this.startInProgress = true;

const pythonPath = ConfigService.getPythonPath();
const workspacePath = ConfigService.getWorkspaceFolderPath();
if (workspacePath) {
this.logService.log(`Using workspace as tracker cwd for CodeCarbon config discovery: ${workspacePath}`);
}

try {
// Ensure codecarbon is installed
Expand All @@ -64,7 +69,7 @@ export class TrackerService {

// Start the tracker process
this.stoppingRequested = false;
this.pythonProcess = spawn(pythonPath, [TRACKER_SCRIPT, 'start']);
this.pythonProcess = spawn(pythonPath, [TRACKER_SCRIPT, 'start'], workspacePath ? { cwd: workspacePath } : undefined);

this.setupProcessHandlers();

Expand Down Expand Up @@ -102,12 +107,11 @@ export class TrackerService {
}

this.pythonProcess.stderr?.on('data', (data) => {
this.logService.logError(`Python error: ${data}`);
this.logPythonOutput(data.toString(), true);
});

this.pythonProcess.stdout?.on('data', (data) => {
this.logService.log(`Python output: ${data}`);
this.logService.parseLogs(data.toString());
this.logPythonOutput(data.toString(), false);
});

this.pythonProcess.on('close', (code) => {
Expand All @@ -128,4 +132,20 @@ export class TrackerService {
this.pythonProcess = null;
}
}

private logPythonOutput(chunk: string, fromStderr: boolean): void {
const text = chunk.toString();
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
for (const line of lines) {
const routed = routePythonLogLine(line, fromStderr);
if (routed.level === 'warn') {
this.logService.logWarning(routed.message);
} else if (routed.level === 'error') {
this.logService.logError(routed.message);
} else {
this.logService.log(routed.message);
}
this.logService.parseLogs(routed.parsePayload);
}
}
}
7 changes: 6 additions & 1 deletion src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import * as vscode from 'vscode';
import { CONFIGURATION_KEYS, INSTALL_STRATEGIES } from './constants';
import { resolvePythonPath, resolveLaunchOnStartup } from './configHelpers';
import { resolveLaunchOnStartup, resolvePythonPath } from './configHelpers';

export class ConfigService {
private static readonly EXTENSION_PREFIX = 'codecarbon';
Expand Down Expand Up @@ -48,6 +48,11 @@ export class ConfigService {
return config.get<string>(CONFIGURATION_KEYS.CUSTOM_PIP_ARGS, '').trim();
}

public static getWorkspaceFolderPath(): string | undefined {
const folder = vscode.workspace.workspaceFolders?.[0];
return folder?.uri.fsPath;
}

/**
* Update a configuration value
*/
Expand Down
1 change: 1 addition & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const COMMANDS = {
STOP: 'codecarbon.stop',
CHECK_VERSION: 'codecarbon.checkVersion',
INSTALL_REPAIR: 'codecarbon.installRepair',
OPEN_CONFIG: 'codecarbon.openConfig',
} as const;

export const CONFIGURATION_KEYS = {
Expand Down
48 changes: 48 additions & 0 deletions tests/ts/pythonLogRouting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { routePythonLogLine } from '../../src/services/pythonLogRouting';

test('routes codecarbon info and strips prefix', () => {
const line = '[codecarbon INFO @ 13:59:27] Codecarbon is taking the configuration from global file:';
const routed = routePythonLogLine(line, true);

assert.equal(routed.level, 'info');
assert.equal(routed.message, 'Codecarbon is taking the configuration from global file:');
assert.equal(routed.parsePayload, `${line}\n`);
});

test('routes codecarbon warning to warn', () => {
const line = '[codecarbon WARNING @ 13:59:28] No CPU tracking mode found.';
const routed = routePythonLogLine(line, false);

assert.equal(routed.level, 'warn');
assert.equal(routed.message, 'No CPU tracking mode found.');
assert.equal(routed.parsePayload, `${line}\n`);
});

test('routes codecarbon error to error', () => {
const line = '[codecarbon ERROR @ 13:59:29] Failed to read power data.';
const routed = routePythonLogLine(line, false);

assert.equal(routed.level, 'error');
assert.equal(routed.message, 'Failed to read power data.');
assert.equal(routed.parsePayload, `${line}\n`);
});

test('routes unknown stderr as python error', () => {
const line = 'Traceback (most recent call last):';
const routed = routePythonLogLine(line, true);

assert.equal(routed.level, 'error');
assert.equal(routed.message, `Python error: ${line}`);
assert.equal(routed.parsePayload, `${line}\n`);
});

test('routes plain stdout as info and preserves message', () => {
const line = 'METRICS:{"type":"metrics","timestamp":1}';
const routed = routePythonLogLine(line, false);

assert.equal(routed.level, 'info');
assert.equal(routed.message, line);
assert.equal(routed.parsePayload, `${line}\n`);
});