From eefbea88cab0ecf50c9386ff74a6ea7a77f5af2a Mon Sep 17 00:00:00 2001 From: Divyanshu Agrawal Date: Wed, 25 Dec 2024 04:27:35 +0530 Subject: [PATCH] Add live user count. --- README.md | 5 +- package-lock.json | 4 +- package.json | 5 ++ server/README.md | 27 +++++++++++ server/server.js | 52 ++++++++++++++++++++ server/server.service | 15 ++++++ src/companion.ts | 26 ++++++---- src/compiler.ts | 20 ++++---- src/executions.ts | 20 ++++---- src/extension.ts | 40 ++++++++++++++-- src/judge.ts | 6 +-- src/parser.ts | 2 +- src/preferences.ts | 7 ++- src/runTestCases.ts | 10 ++-- src/submit.ts | 4 +- src/tests/judge.test.ts | 1 + src/tests/utilsPure.test.ts | 1 + src/types.ts | 20 ++++++-- src/utils.ts | 4 +- src/webview/JudgeView.ts | 34 +++++++++---- src/webview/editorChange.ts | 6 +-- src/webview/frontend/App.tsx | 84 ++++++++++++++++++++++++++++++--- src/webview/frontend/app.css | 10 ++++ src/webview/processRunAll.ts | 4 +- src/webview/processRunSingle.ts | 8 ++-- webpack.config.js | 4 +- 26 files changed, 336 insertions(+), 83 deletions(-) create mode 100644 server/README.md create mode 100644 server/server.js create mode 100644 server/server.service diff --git a/README.md b/README.md index c0cd46f..658abcc 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,8 @@ approach. It makes reviewing and accepting the PR much easier.** ## Telemetry -The extension collects basic events defined in `src/telmetry.ts`. To disable, -modify the setting `telemetry.telemetryLevel` (applies to all VSCode -extensions). +To show live user count, the extension sends a request to the server every few +seconds. No information is sent with the request. ## License diff --git a/package-lock.json b/package-lock.json index c3e2ce5..f1a8d6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "competitive-programming-helper", - "version": "2024.6.1717519178", + "version": "2077.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "competitive-programming-helper", - "version": "2024.6.1717519178", + "version": "2077.0.0", "license": "GPL-3.0-or-later", "dependencies": { "@vscode/extension-telemetry": "^0.9.0", diff --git a/package.json b/package.json index 27a2564..385dad5 100644 --- a/package.json +++ b/package.json @@ -355,6 +355,11 @@ "type": "boolean", "default": true, "description": "Automatically show the judge view when opening a file that has a problem associated with it" + }, + "cph.general.remoteServerAddress": { + "type": "string", + "default": "http://20.244.105.138:4546", + "description": "The address of the remote server to which the extension will send requests. (Currently used for live user count)" } } } diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..90dec50 --- /dev/null +++ b/server/README.md @@ -0,0 +1,27 @@ +# Remote webserver for CPH + +Currently implements a heartbeat server for live user count. + +## Installation on a VM + +- Install node +- Copy `server.js` to `/opt/` +- Install the service: + + ``` + sudo cp server.service /etc/systemd/system + sudo systemctl daemon-reload + sudo systemctl start server.service + sudo systemctl enable server.service + ``` + +- Check status using `sudo systemctl status server.service` +- In CPH, configure the address + port of the server. + +## Uninstall + +``` +sudo systemctl stop server.service +sudo systemctl disable server.service +sudo rm /etc/systemd/system/server.service +``` diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..c2262c8 --- /dev/null +++ b/server/server.js @@ -0,0 +1,52 @@ +const http = require('http'); +const { env } = require('process'); + +class HeartbeatServer { + constructor() { + console.log("Creating new HeartbeatServer instance"); + + this.ips = new Map(); + this.refresh_interval_seconds = parseInt(env.REFRESH_INTERVAL) || 30; + this.port = parseInt(env.PORT) || 8080; + console.log(`Refresh interval: ${this.refresh_interval_seconds} seconds`); + + this.runServer(this.port); + + setInterval(() => { + this.runCleanup(); + }, this.refresh_interval_seconds * 1000); + } + + runServer(port) { + console.log("Starting heartbeat server on port " + port); + this.serverInstance = http.createServer((req, res) => { + this.processHeartbeat(req.socket.remoteAddress); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end(this.ips.size.toString()); + }); + this.serverInstance.listen(port); + console.log("Heartbeat server started."); + } + + runCleanup() { + const oldLen = this.ips.size; + const now = Date.now(); + for (const [ip, timestamp] of this.ips.entries()) { + if (now - timestamp > this.refresh_interval_seconds * 1000) { + this.ips.delete(ip); + console.log("Deleted stale IP: " + ip); + } + } + const newLen = this.ips.size; + console.log(`Cleaned up ${oldLen - newLen} stale IPs. New count: ${newLen}`); + } + + // Called when a heartbeat is received from an IP + processHeartbeat(ip) { + this.ips.set(ip, Date.now()); + } +} + +console.log("Starting app"); +new HeartbeatServer(); diff --git a/server/server.service b/server/server.service new file mode 100644 index 0000000..e38c55c --- /dev/null +++ b/server/server.service @@ -0,0 +1,15 @@ +[Unit] +Description=Node.js heartbeat server +After=network.target + +[Service] +ExecStart=/usr/bin/env node /opt/server.js +Restart=always +RestartSec=5 +Environment=PORT=4546 +User=server +Group=server +WorkingDirectory=/opt + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/src/companion.ts b/src/companion.ts index 1d4ad0b..276f38b 100644 --- a/src/companion.ts +++ b/src/companion.ts @@ -62,7 +62,7 @@ export const submitKattisProblem = (problem: Problem) => { pyshell.stdin.end(); pyshell.stdout.on('data', function (data) { - console.log(data.toString()); + globalThis.logger.log(data.toString()); getJudgeViewProvider().extensionToJudgeViewMessage({ command: 'new-problem', problem, @@ -70,7 +70,7 @@ export const submitKattisProblem = (problem: Problem) => { ({ command: 'submit-finished' }); }); pyshell.stderr.on('data', function (data) { - console.log(data.tostring()); + globalThis.logger.log(data.tostring()); vscode.window.showErrorMessage(data); }); }; @@ -89,7 +89,7 @@ export const storeSubmitProblem = (problem: Problem) => { languageId, }; globalThis.reporter.sendTelemetryEvent(telmetry.SUBMIT_TO_CODEFORCES); - console.log('Stored savedResponse', savedResponse); + globalThis.logger.log('Stored savedResponse', savedResponse); }; export const setupCompanionServer = () => { @@ -99,7 +99,8 @@ export const setupCompanionServer = () => { let rawProblem = ''; req.on('data', (chunk) => { - COMPANION_LOGGING && console.log('Companion server got data'); + COMPANION_LOGGING && + globalThis.logger.log('Companion server got data'); rawProblem += chunk; }); req.on('close', function () { @@ -110,7 +111,9 @@ export const setupCompanionServer = () => { const problem: Problem = JSON.parse(rawProblem); handleNewProblem(problem); COMPANION_LOGGING && - console.log('Companion server closed connection.'); + globalThis.logger.log( + 'Companion server closed connection.', + ); } catch (e) { vscode.window.showErrorMessage( `Error parsing problem from companion "${e}. Raw problem: '${rawProblem}'"`, @@ -120,7 +123,7 @@ export const setupCompanionServer = () => { res.write(JSON.stringify(savedResponse)); if (headers['cph-submit'] == 'true') { COMPANION_LOGGING && - console.log( + globalThis.logger.log( 'Request was from the cph-submit extension; sending savedResponse and clearing it', savedResponse, ); @@ -140,10 +143,13 @@ export const setupCompanionServer = () => { `Are multiple VSCode windows open? CPH will work on the first opened window. CPH server encountered an error: "${err.message}" , companion may not work.`, ); }); - console.log('Companion server listening on port', config.port); + globalThis.logger.log( + 'Companion server listening on port', + config.port, + ); return server; } catch (e) { - console.error('Companion server error :', e); + globalThis.logger.error('Companion server error :', e); } }; @@ -151,7 +157,7 @@ export const getProblemFileName = (problem: Problem, ext: string) => { if (isCodeforcesUrl(new URL(problem.url)) && useShortCodeForcesName()) { return `${getProblemName(problem.url)}.${ext}`; } else { - console.log( + globalThis.logger.log( isCodeforcesUrl(new URL(problem.url)), useShortCodeForcesName(), ); @@ -204,7 +210,7 @@ const handleNewProblem = async (problem: Problem) => { try { url = new URL(problem.url); } catch (err) { - console.error(err); + globalThis.logger.error(err); return null; } if (url.hostname == 'open.kattis.com') { diff --git a/src/compiler.ts b/src/compiler.ts index 4e790c6..407399a 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -15,7 +15,7 @@ export let onlineJudgeEnv = false; export const setOnlineJudgeEnv = (value: boolean) => { onlineJudgeEnv = value; - console.log('online judge env:', onlineJudgeEnv); + globalThis.logger.log('online judge env:', onlineJudgeEnv); }; /** @@ -211,7 +211,7 @@ const createDotnetProject = async ( getDotnetProjectLocation(language, srcPath), ); - console.log('Creating new .NET project'); + globalThis.logger.log('Creating new .NET project'); const args = ['new', 'console', '--force', '-o', projDir]; const newProj = spawn(language.compiler, args); @@ -244,7 +244,7 @@ const createDotnetProject = async ( } const destPath = path.join(projDir, 'Program.cs'); - console.log( + globalThis.logger.log( 'Copying source code to the project', srcPath, destPath, @@ -265,7 +265,7 @@ const createDotnetProject = async ( } resolve(true); } catch (err) { - console.error('Error while copying source code', err); + globalThis.logger.error('Error while copying source code', err); ocWrite('Errors while creating new .NET project:\n' + err); ocShow(); resolve(false); @@ -273,7 +273,7 @@ const createDotnetProject = async ( }); newProj.on('error', (err) => { - console.log(err); + globalThis.logger.log(err); ocWrite('Errors while creating new .NET project:\n' + err); ocShow(); resolve(false); @@ -294,7 +294,7 @@ const createDotnetProject = async ( * @param srcPath location of the source code */ export const compileFile = async (srcPath: string): Promise => { - console.log('Compilation Started'); + globalThis.logger.log('Compilation Started'); await vscode.workspace.openTextDocument(srcPath).then((doc) => doc.save()); ocHide(); const language: Language = getLanguage(srcPath); @@ -332,7 +332,7 @@ export const compileFile = async (srcPath: string): Promise => { command: 'compiling-start', }); const flags: string[] = getFlags(language, srcPath); - console.log('Compiling with flags', flags); + globalThis.logger.log('Compiling with flags', flags); const result = new Promise((resolve) => { let compiler; try { @@ -350,7 +350,7 @@ export const compileFile = async (srcPath: string): Promise => { }); compiler.on('error', (err) => { - console.error(err); + globalThis.logger.error(err); ocWrite( 'Errors while compiling:\n' + err.message + @@ -375,7 +375,7 @@ export const compileFile = async (srcPath: string): Promise => { `Exit code: ${exitCode} Errors while compiling:\n` + error, ); ocShow(); - console.error('Compilation failed'); + globalThis.logger.error('Compilation failed'); getJudgeViewProvider().extensionToJudgeViewMessage({ command: 'compiling-stop', }); @@ -394,7 +394,7 @@ export const compileFile = async (srcPath: string): Promise => { ocShow(); } - console.log('Compilation passed'); + globalThis.logger.log('Compilation passed'); getJudgeViewProvider().extensionToJudgeViewMessage({ command: 'compiling-stop', }); diff --git a/src/executions.ts b/src/executions.ts index abf8021..c5f628c 100644 --- a/src/executions.ts +++ b/src/executions.ts @@ -21,7 +21,7 @@ export const runTestCase = ( binPath: string, input: string, ): Promise => { - console.log('Running testcase', language, binPath, input); + globalThis.logger.log('Running testcase', language, binPath, input); const result: Run = { stdout: '', stderr: '', @@ -120,7 +120,7 @@ export const runTestCase = ( } process.on('error', (err) => { - console.error(err); + globalThis.logger.error(err); vscode.window.showErrorMessage( `Could not launch testcase process. Is '${language.compiler}' in your PATH?`, ); @@ -136,7 +136,7 @@ export const runTestCase = ( result.signal = signal; result.time = end - begin; runningBinaries.pop(); - console.log('Run Result:', result); + globalThis.logger.log('Run Result:', result); resolve(result); }); @@ -145,11 +145,11 @@ export const runTestCase = ( }); process.stderr.on('data', (data) => (result.stderr += data)); - console.log('Wrote to STDIN'); + globalThis.logger.log('Wrote to STDIN'); try { process.stdin.write(input); } catch (err) { - console.error('WRITEERROR', err); + globalThis.logger.error('WRITEERROR', err); } process.stdin.end(); @@ -160,7 +160,7 @@ export const runTestCase = ( result.signal = err.name; result.time = end - begin; runningBinaries.pop(); - console.log('Run Error Result:', result); + globalThis.logger.log('Run Error Result:', result); resolve(result); }); }); @@ -171,12 +171,12 @@ export const runTestCase = ( /** Remove the generated binary from the file system, if present */ export const deleteBinary = (language: Language, binPath: string) => { if (language.skipCompile) { - console.log( + globalThis.logger.log( "Skipping deletion of binary as it's not a compiled language.", ); return; } - console.log('Deleting binary', binPath); + globalThis.logger.log('Deleting binary', binPath); try { const isLinux = platform() == 'linux'; const isFile = path.extname(binPath); @@ -200,13 +200,13 @@ export const deleteBinary = (language: Language, binPath: string) => { } } } catch (err) { - console.error('Error while deleting binary', err); + globalThis.logger.error('Error while deleting binary', err); } }; /** Kill all running binaries. Usually, only one should be running at a time. */ export const killRunning = () => { globalThis.reporter.sendTelemetryEvent(telmetry.KILL_RUNNING); - console.log('Killling binaries'); + globalThis.logger.log('Killling binaries'); runningBinaries.forEach((process) => process.kill()); }; diff --git a/src/extension.ts b/src/extension.ts index 6931862..096ca1b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,3 +1,30 @@ +/************************************************************************************/ +globalThis.storedLogs = ''; +function customLogger( + originalMethod: (...args: any[]) => void, + ...args: any[] +) { + originalMethod(...args); + + globalThis.storedLogs += new Date().toISOString() + ' '; + globalThis.storedLogs += + args + .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg)) + .join(' ') + '\n'; +} + +globalThis.logger = {}; +globalThis.logger.log = (...args: any[]) => customLogger(console.log, ...args); +globalThis.logger.error = (...args: any[]) => + customLogger(console.error, ...args); +globalThis.logger.warn = (...args: any[]) => + customLogger(console.warn, ...args); +globalThis.logger.info = (...args: any[]) => + customLogger(console.info, ...args); +globalThis.logger.debug = (...args: any[]) => + customLogger(console.debug, ...args); +/************************************************************************************/ + import * as vscode from 'vscode'; import { setupCompanionServer } from './companion'; import runTestCases from './runTestCases'; @@ -19,7 +46,7 @@ export const getJudgeViewProvider = () => { }; const registerCommands = (context: vscode.ExtensionContext) => { - console.log('Registering commands'); + globalThis.logger.log('Registering commands'); const disposable = vscode.commands.registerCommand( 'cph.runTestCases', () => { @@ -70,7 +97,7 @@ const registerCommands = (context: vscode.ExtensionContext) => { // This method is called when the extension is activated export function activate(context: vscode.ExtensionContext) { - console.log('cph: activate() execution started'); + globalThis.logger.log('cph: activate() execution started'); globalThis.context = context; downloadRemoteMessage(); @@ -111,7 +138,7 @@ export function activate(context: vscode.ExtensionContext) { async function downloadRemoteMessage() { try { - console.log('Fetching remote message'); + globalThis.logger.log('Fetching remote message'); globalThis.remoteMessage = await ( await fetch(config.remoteMessageUrl) ).text(); @@ -119,8 +146,11 @@ async function downloadRemoteMessage() { command: 'remote-message', message: globalThis.remoteMessage, }); - console.log('Remote message fetched', globalThis.remoteMessage); + globalThis.logger.log( + 'Remote message fetched', + globalThis.remoteMessage, + ); } catch (e) { - console.error('Error fetching remote message', e); + globalThis.logger.error('Error fetching remote message', e); } } diff --git a/src/judge.ts b/src/judge.ts index 256f251..5f0a3d7 100644 --- a/src/judge.ts +++ b/src/judge.ts @@ -15,9 +15,9 @@ export const isResultCorrect = ( const expectedLines = expected.trim().split('\n'); const resultLines = result.trim().split('\n'); - console.log('res', resultLines); + globalThis.logger.log('res', resultLines); if (expectedLines.length !== resultLines.length) { - console.log('Failed precheck', expectedLines, resultLines); + globalThis.logger.log('Failed precheck', expectedLines, resultLines); return false; } @@ -25,7 +25,7 @@ export const isResultCorrect = ( for (let i = 0; i < len; i++) { if (expectedLines[i].trim() !== resultLines[i].trim()) { - console.log( + globalThis.logger.log( 'Judge Failed here: ', expectedLines[i].trim(), resultLines[i].trim(), diff --git a/src/parser.ts b/src/parser.ts index 0fef8d5..10c6df2 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -46,7 +46,7 @@ export const saveProblem = (srcPath: string, problem: Problem) => { const cphFolder = path.join(srcFolder, '.cph'); if (getSaveLocationPref() === '' && !fs.existsSync(cphFolder)) { - console.log('Making .cph folder'); + globalThis.logger.log('Making .cph folder'); fs.mkdirSync(cphFolder); } diff --git a/src/preferences.ts b/src/preferences.ts index b530d7f..9010349 100644 --- a/src/preferences.ts +++ b/src/preferences.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; const getPreference = (section: prefSection): any => { const ret = workspace.getConfiguration('cph').get(section); - console.log('Read preference for ', section, ret); + globalThis.logger.log('Read preference for ', section, ret); return ret; }; @@ -83,6 +83,9 @@ export const getCSharpArgsPref = (): string[] => export const getFirstTimePref = (): boolean => getPreference('general.firstTime') || 'true'; +export const getRemoteServerAddressPref = (): string => + getPreference('general.remoteServerAddress') || ''; + export const getDefaultLangPref = (): string | null => { const pref = getPreference('general.defaultLanguage'); if (pref === 'none' || pref == ' ' || !pref) { @@ -187,6 +190,6 @@ export const getLanguageId = (srcPath: string): number => { return id; } } - console.error("Couldn't find id for compiler " + compiler); + globalThis.logger.error("Couldn't find id for compiler " + compiler); return -1; }; diff --git a/src/runTestCases.ts b/src/runTestCases.ts index 3efc716..944c02c 100644 --- a/src/runTestCases.ts +++ b/src/runTestCases.ts @@ -16,7 +16,7 @@ import telmetry from './telmetry'; */ export default async () => { globalThis.reporter.sendTelemetryEvent(telmetry.RUN_ALL_TESTCASES); - console.log('Running command "runTestCases"'); + globalThis.logger.log('Running command "runTestCases"'); const editor = vscode.window.activeTextEditor; if (editor === undefined) { checkUnsupported(''); @@ -30,7 +30,7 @@ export default async () => { const problem = getProblem(srcPath); if (!problem) { - console.log('No problem saved.'); + globalThis.logger.log('No problem saved.'); createLocalProblem(editor); return; } @@ -38,7 +38,7 @@ export default async () => { const didCompile = await compileFile(srcPath); if (!didCompile) { - console.error('Could not compile', srcPath); + globalThis.logger.error('Could not compile', srcPath); return; } await editor.document.save(); @@ -53,7 +53,7 @@ export default async () => { const createLocalProblem = async (editor: vscode.TextEditor) => { globalThis.reporter.sendTelemetryEvent(telmetry.NEW_LOCAL_PROBLEM); - console.log('Creating local problem'); + globalThis.logger.log('Creating local problem'); const srcPath = editor.document.fileName; if (checkUnsupported(srcPath)) { return; @@ -76,7 +76,7 @@ const createLocalProblem = async (editor: vscode.TextEditor) => { group: 'local', local: true, }; - console.log(newProblem); + globalThis.logger.log(newProblem); saveProblem(srcPath, newProblem); getJudgeViewProvider().focus(); getJudgeViewProvider().extensionToJudgeViewMessage({ diff --git a/src/submit.ts b/src/submit.ts index cae77cd..d0252e5 100644 --- a/src/submit.ts +++ b/src/submit.ts @@ -29,7 +29,7 @@ export const submitToKattis = async () => { try { url = new URL(problem.url); } catch (err) { - console.error(err); + globalThis.logger.error(err); vscode.window.showErrorMessage('Not a kattis problem.'); return; } @@ -70,7 +70,7 @@ export const submitToCodeForces = async () => { try { url = new URL(problem.url); } catch (err) { - console.error(err); + globalThis.logger.error(err); vscode.window.showErrorMessage('Not a codeforces problem.'); return; } diff --git a/src/tests/judge.test.ts b/src/tests/judge.test.ts index 11f49d5..f0f1363 100644 --- a/src/tests/judge.test.ts +++ b/src/tests/judge.test.ts @@ -1,3 +1,4 @@ +globalThis.logger = { ...console }; import { isResultCorrect } from '../judge'; describe('saved problem parser', () => { diff --git a/src/tests/utilsPure.test.ts b/src/tests/utilsPure.test.ts index 0f3311c..bbd927c 100644 --- a/src/tests/utilsPure.test.ts +++ b/src/tests/utilsPure.test.ts @@ -1,3 +1,4 @@ +globalThis.logger = { ...console }; import { words_in_text } from '../utilsPure'; describe('problem name parser', () => { diff --git a/src/types.ts b/src/types.ts index 1d0e209..9162378 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,7 +46,8 @@ export type prefSection = | 'language.haskell.Command' | 'general.retainWebviewContext' | 'general.autoShowJudge' - | 'general.defaultLanguageTemplateFileLocation'; + | 'general.defaultLanguageTemplateFileLocation' + | 'general.remoteServerAddress'; export type Language = { name: LangNames; @@ -156,6 +157,10 @@ export type OpenUrl = { url: string; }; +export type GetExtLogs = { + command: 'get-ext-logs'; +}; + export type WebviewToVSEvent = | RunAllCommand | GetInitialProblem @@ -167,7 +172,8 @@ export type WebviewToVSEvent = | SubmitCf | OnlineJudgeEnv | SubmitKattis - | OpenUrl; + | OpenUrl + | GetExtLogs; export type RunningCommand = { command: 'running'; @@ -213,6 +219,11 @@ export type RemoteMessageCommand = { message: string; }; +export type ExtLogsCommand = { + command: 'ext-logs'; + logs: string; +}; + export type VSToWebViewMessage = | ResultCommand | RunningCommand @@ -223,7 +234,8 @@ export type VSToWebViewMessage = | SubmitFinishedCommand | NotRunningCommand | RemoteMessageCommand - | NewProblemCommand; + | NewProblemCommand + | ExtLogsCommand; export type CphEmptyResponse = { empty: true; @@ -245,4 +257,6 @@ declare global { var reporter: TelemetryReporter; var context: vscode.ExtensionContext; var remoteMessage: string | undefined; + var storedLogs: string; + var logger: any; } diff --git a/src/utils.ts b/src/utils.ts index 9b87503..0f4c16f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -182,7 +182,7 @@ export const checkUnsupported = (srcPath: string): boolean => { export const deleteProblemFile = (srcPath: string) => { globalThis.reporter.sendTelemetryEvent(telmetry.DELETE_ALL_TESTCASES); const probPath = getProbSaveLocation(srcPath); - console.log('Deleting problem file', probPath); + globalThis.logger.log('Deleting problem file', probPath); try { if (platform() === 'win32') { spawn('cmd.exe', ['/c', 'del', probPath]); @@ -190,7 +190,7 @@ export const deleteProblemFile = (srcPath: string) => { spawn('rm', [probPath]); } } catch (error) { - console.error('Error while deleting problem file ', error); + globalThis.logger.error('Error while deleting problem file ', error); } }; diff --git a/src/webview/JudgeView.ts b/src/webview/JudgeView.ts index 4f59ebd..f9fcdea 100644 --- a/src/webview/JudgeView.ts +++ b/src/webview/JudgeView.ts @@ -9,6 +9,7 @@ import runAllAndSave from './processRunAll'; import runTestCases from '../runTestCases'; import { getAutoShowJudgePref, + getRemoteServerAddressPref, getRetainWebviewContextPref, } from '../preferences'; import { setOnlineJudgeEnv } from '../compiler'; @@ -39,7 +40,7 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { webviewView.webview.onDidReceiveMessage( async (message: WebviewToVSEvent) => { - console.log('Got from webview', message); + globalThis.logger.log('Got from webview', message); switch (message.command) { case 'run-single-and-save': { const problem = message.problem; @@ -64,6 +65,11 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { break; } + case 'get-ext-logs': { + this.sendExtLogs(); + break; + } + case 'delete-tcs': { this.extensionToJudgeViewMessage({ command: 'new-problem', @@ -103,13 +109,22 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { } default: { - console.error('Unknown event received from webview'); + globalThis.logger.error( + 'Unknown event received from webview', + ); } } }, ); } + private sendExtLogs() { + this.extensionToJudgeViewMessage({ + command: 'ext-logs', + logs: globalThis.storedLogs, + }); + } + private getInitialProblem() { const doc = vscode.window.activeTextEditor?.document; this.extensionToJudgeViewMessage({ @@ -119,7 +134,7 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { // also load any messages from before that were lost. this.messageBuffer.forEach((message) => { - console.log('Restored buffer command', message.command); + globalThis.logger.log('Restored buffer command', message.command); this._view?.webview.postMessage(message); }); @@ -131,7 +146,7 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { public problemPath: string | undefined; public async focus() { - console.log('focusing'); + globalThis.logger.log('focusing'); if (!this._view) { await vscode.commands.executeCommand('cph.judgeView.focus'); } else { @@ -140,7 +155,7 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { } private focusIfNeeded = (message: VSToWebViewMessage) => { - console.log(message.command); + globalThis.logger.log(message.command); switch (message.command) { case 'waiting-for-submit': @@ -172,7 +187,7 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { // this._view.show?.(true); // `show` is not implemented in 1.49 but is for 1.50 insiders this._view.webview.postMessage(message); if (message.command !== 'submit-finished') { - console.log('View got message', message); + globalThis.logger.log('View got message', message); } if (message.command === 'new-problem') { if (message.problem === undefined) { @@ -183,7 +198,7 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { } } else { if (message.command !== 'new-problem') { - console.log('Pushing to buffer', message.command); + globalThis.logger.log('Pushing to buffer', message.command); this.messageBuffer.push(message); } else { this.messageBuffer = []; @@ -196,6 +211,8 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { vscode.Uri.joinPath(this._extensionUri, 'dist', 'app.css'), ); + const remoteServerAddress = getRemoteServerAddressPref(); + const codiconsUri = webview.asWebviewUri( vscode.Uri.joinPath(this._extensionUri, 'dist', 'codicon.css'), ); @@ -243,6 +260,7 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { window.vscodeApi = acquireVsCodeApi(); window.remoteMessage = '${remoteMessage}'; window.generatedJsonUri = '${generatedJsonUri}'; + window.remoteServerAddress = '${remoteServerAddress}'; document.addEventListener( 'DOMContentLoaded', @@ -254,7 +272,7 @@ class JudgeViewProvider implements vscode.WebviewViewProvider { command: 'online-judge-env', value:false, }); - console.log("Requested initial problem"); + globalThis.logger.log("Requested initial problem"); }, ); diff --git a/src/webview/editorChange.ts b/src/webview/editorChange.ts index 0f30487..7fbaf13 100644 --- a/src/webview/editorChange.ts +++ b/src/webview/editorChange.ts @@ -16,7 +16,7 @@ import { setOnlineJudgeEnv } from '../compiler'; * @param context The activation context */ export const editorChanged = async (e: vscode.TextEditor | undefined) => { - console.log('Changed editor to', e?.document.fileName); + globalThis.logger.log('Changed editor to', e?.document.fileName); if (e === undefined) { getJudgeViewProvider().extensionToJudgeViewMessage({ @@ -50,7 +50,7 @@ export const editorChanged = async (e: vscode.TextEditor | undefined) => { vscode.commands.executeCommand('cph.judgeView.focus'); } - console.log('Sent problem @', Date.now()); + globalThis.logger.log('Sent problem @', Date.now()); getJudgeViewProvider().extensionToJudgeViewMessage({ command: 'new-problem', problem, @@ -58,7 +58,7 @@ export const editorChanged = async (e: vscode.TextEditor | undefined) => { }; export const editorClosed = (e: vscode.TextDocument) => { - console.log('Closed editor:', e.uri.fsPath); + globalThis.logger.log('Closed editor:', e.uri.fsPath); const srcPath = e.uri.fsPath; const probPath = getProbSaveLocation(srcPath); diff --git a/src/webview/frontend/App.tsx b/src/webview/frontend/App.tsx index 8515ce1..eace083 100644 --- a/src/webview/frontend/App.tsx +++ b/src/webview/frontend/App.tsx @@ -13,6 +13,21 @@ import { import CaseView from './CaseView'; import Page from './Page'; +let storedLogs = ''; +const originalConsole = { ...window.console }; +function customLogger( + originalMethod: (...args: any[]) => void, + ...args: any[] +) { + originalMethod(...args); + + storedLogs += new Date().toISOString() + ' '; + storedLogs += + args + .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg)) + .join(' ') + '\n'; +} + declare const vscodeApi: { postMessage: (message: WebviewToVSEvent) => void; getState: () => WebViewpersistenceState | undefined; @@ -22,9 +37,17 @@ declare const vscodeApi: { interface CustomWindow extends Window { generatedJsonUri: string; remoteMessage: string | null; + remoteServerAddress: string; + console: Console; } declare const window: CustomWindow; +window.console.log = customLogger.bind(window.console, originalConsole.log); +window.console.error = customLogger.bind(window.console, originalConsole.error); +window.console.warn = customLogger.bind(window.console, originalConsole.warn); +window.console.info = customLogger.bind(window.console, originalConsole.info); +window.console.debug = customLogger.bind(window.console, originalConsole.debug); + // Original: www.paypal.com/ncp/payment/CMLKCFEJEMX5L const payPalUrl = 'https://rb.gy/5iiorz'; @@ -45,8 +68,32 @@ function Judge(props: { const [notification, setNotification] = useState(null); const [waitingForSubmit, setWaitingForSubmit] = useState(false); const [onlineJudgeEnv, setOnlineJudgeEnv] = useState(false); - const [showInfoPage, setShowInfoPage] = useState(false); + const [infoPageVisible, setInfoPageVisible] = useState(false); const [generatedJson, setGeneratedJson] = useState(null); + const [liveUserCount, setLiveUserCount] = useState(0); + const [extLogs, setExtLogs] = useState(''); + + useEffect(() => { + const interval = setInterval(() => { + console.log('Fetching live users'); + fetch(window.remoteServerAddress) + .then((res) => res.text()) + .then((text) => { + const userCount = Number(text); + if (isNaN(userCount)) { + console.error('Invalid live user count', text); + setLiveUserCount(0); + } else { + setLiveUserCount(userCount); + } + console.log('Live users:', text); + }) + .catch((err) => + console.error('Failed to fetch live users', err), + ); + }, 5000); + return () => clearInterval(interval); + }, []); useEffect(() => { fetch(window.generatedJsonUri) @@ -132,6 +179,10 @@ function Judge(props: { setWaitingForSubmit(true); break; } + case 'ext-logs': { + setExtLogs(data.logs); + break; + } default: { console.log('Invalid event', event.data); } @@ -400,6 +451,13 @@ function Judge(props: { } }; + const showInfoPage = () => { + sendMessageToVSCode({ + command: 'get-ext-logs', + }); + setInfoPageVisible(true); + }; + const renderDonateButton = () => { const diff = new Date().getTime() - webviewState.dialogCloseDate; const diffInDays = diff / (1000 * 60 * 60 * 24); @@ -436,7 +494,7 @@ function Judge(props: { }; const renderInfoPage = () => { - if (showInfoPage === false) { + if (infoPageVisible === false) { return null; } @@ -445,12 +503,16 @@ function Judge(props: { setShowInfoPage(false)} + closePage={() => setInfoPageVisible(false)} /> ); } + const logs = storedLogs; const contents = (
+ A VS Code extension to make competitive programming easier, + created by Divyanshu Agrawal +

🤖 Enable AI compilation

Get 100x faster compilation using AI, please opt-in below. Your data will be used to train cats to write JavaScript. @@ -483,8 +545,14 @@ function Judge(props: {

License

{generatedJson.licenseString}

- Created by Divyanshu Agrawal +

Live user count

+ {liveUserCount} user(s) online.
+

UI Logs

+
{logs}
+
+

Extension Logs

+
{extLogs}
); @@ -492,7 +560,7 @@ function Judge(props: { setShowInfoPage(false)} + closePage={() => setInfoPageVisible(false)} /> ); }; @@ -566,6 +634,10 @@ function Judge(props: { }} /> +
+ {' '} + {liveUserCount} users online +
@@ -604,7 +676,7 @@ function Judge(props: {