diff --git a/package.json b/package.json index fb4270e..c9d20aa 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,11 @@ "title": "Install/Repair CodeCarbon Python package", "category": "Codecarbon", "command": "codecarbon.installRepair" + }, + { + "title": "Open CodeCarbon config", + "category": "Codecarbon", + "command": "codecarbon.openConfig" } ] }, @@ -126,4 +131,4 @@ "prettier": "^3.8.1", "typescript": "^5.9.3" } -} +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 25c3fd6..c1e4b22 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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'; @@ -43,6 +45,7 @@ export async function activate(context: vscode.ExtensionContext): Promise 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()); @@ -84,3 +87,38 @@ async function installRepairCodecarbon(): Promise { const pythonPath = ConfigService.getPythonPath(); await pythonService.installOrRepairCodecarbon(pythonPath); } + +async function openCodecarbonConfig(): Promise { + 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 }); +} diff --git a/src/scripts/tracker.py b/src/scripts/tracker.py index 0c25d36..a14e346 100644 --- a/src/scripts/tracker.py +++ b/src/scripts/tracker.py @@ -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": diff --git a/src/services/pythonLogRouting.ts b/src/services/pythonLogRouting.ts new file mode 100644 index 0000000..520cb38 --- /dev/null +++ b/src/services/pythonLogRouting.ts @@ -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` }; +} diff --git a/src/services/trackerService.ts b/src/services/trackerService.ts index 68425cd..26a68f1 100644 --- a/src/services/trackerService.ts +++ b/src/services/trackerService.ts @@ -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'); @@ -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 @@ -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(); @@ -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) => { @@ -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); + } + } } diff --git a/src/utils/config.ts b/src/utils/config.ts index 6a96375..e9aa074 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -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'; @@ -48,6 +48,11 @@ export class ConfigService { return config.get(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 */ diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 92cb17a..4b72e88 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -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 = { diff --git a/tests/ts/pythonLogRouting.test.ts b/tests/ts/pythonLogRouting.test.ts new file mode 100644 index 0000000..b95636b --- /dev/null +++ b/tests/ts/pythonLogRouting.test.ts @@ -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`); +});