Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
49808b9
Save conductor exampel
yytelliot Apr 5, 2026
d56f3e3
Merge branch 'conductor-example' into status-channel
yytelliot Apr 5, 2026
871c7f0
Hook status to stop run button
yytelliot Apr 5, 2026
4958f59
Refactor WorkspaceReducer to seperate output and state changes
yytelliot Apr 5, 2026
c2288d3
Add new intepreter actions
yytelliot Apr 5, 2026
f88997c
Add status handling to evalCode.ts
yytelliot Apr 5, 2026
0dc9bca
Add test cases for new reducers and actions
yytelliot Apr 6, 2026
d0f981e
Update dummy evaluator
yytelliot Apr 6, 2026
a57c193
Merge branch 'master' into status-channel
yytelliot Apr 6, 2026
24586bb
Revert test changes in dummy conductor and implement stricter type in…
yytelliot Apr 6, 2026
e5f1431
Revert publicFlags.ts
yytelliot Apr 6, 2026
e839ce4
Fix typo
yytelliot Apr 6, 2026
c13993a
Fix formatting
yytelliot Apr 6, 2026
d650659
Delete tsconfig.tsbuildinfo
yytelliot Apr 6, 2026
24aae6f
Merge branch 'master' into status-channel
martin-henz Apr 7, 2026
6373e89
Merge branch 'master' into status-channel
martin-henz Apr 8, 2026
2f3b034
Merge branch 'master' into status-channel
martin-henz Apr 8, 2026
2718f54
Merge branch 'master' into status-channel
yytelliot Apr 8, 2026
2e51710
Merge branch 'master' into status-channel
yytelliot Apr 8, 2026
bd9834f
CSE Machine : ClearDeadFrames cause elongated Frames - fix (#3731)
Akshay-2007-1 Apr 8, 2026
1d2f4e6
Merge branch 'master' into status-channel
martin-henz Apr 9, 2026
2d51bbc
Merge branch 'master' into status-channel
martin-henz Apr 13, 2026
5caa88a
Bump conductor version
yytelliot Apr 13, 2026
e12d54e
Update yarn.lock
yytelliot Apr 13, 2026
3d01bd0
Fix race condition
yytelliot Apr 13, 2026
46be087
Merge branch 'master' into status-channel
martin-henz Apr 13, 2026
ebc62e7
Fix npm url
yytelliot Apr 13, 2026
91b3524
Merge branch 'master' into status-channel
martin-henz Apr 13, 2026
2c3d3c2
Merge branch 'master' into status-channel
RichDom2185 Apr 13, 2026
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: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@reduxjs/toolkit": "^1.9.7",
"@sentry/react": "^10.5.0",
"@sourceacademy/c-slang": "^1.0.21",
"@sourceacademy/conductor": "https://github.com/source-academy/conductor.git#0.3.0",
"@sourceacademy/conductor": "https://github.com/source-academy/conductor.git#0.4.0",
"@sourceacademy/language-directory": "https://github.com/source-academy/language-directory.git#0.0.6",
"@sourceacademy/plugin-directory": "https://github.com/source-academy/plugin-directory.git#0.0.2",
"@sourceacademy/sharedb-ace": "2.1.1",
Expand Down
249 changes: 130 additions & 119 deletions public/evaluators/debug/dummy-conductor-evaluator.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,130 @@
/* Dummy evaluator written in class style using this.conductor.send* calls. */
(function () {
/* Dummy evaluator using conductor runner API */
(async function () {
'use strict';

const CHANNEL = {
CHUNK: '__chunk',
SERVICE: '__service',
STDIO: '__stdio',
RESULT: '__result',
ERROR: '__error'
};

const SERVICE = {
HELLO: 0,
ENTRY: 2
};

const ports = Object.create(null);
const chunkQueue = [];
const chunkWaiters = [];

function post(channelName, payload) {
const port = ports[channelName];
if (port) {
port.postMessage(payload);
}
// The worker can receive channel-attach messages before async imports resolve.
// Buffer and replay them once the conductor runner has been initialised.
const earlyMessages = [];
function captureEarlyMessage(event) {
earlyMessages.push(event);
}
self.addEventListener('message', captureEarlyMessage);

// Fixed version of conductor runner that is known to work with the current conductor API.
const CONDUCTOR_RUNNER_URL =
'https://cdn.jsdelivr.net/npm/@sourceacademy/conductor@0.3.0/dist/conductor/runner/index.js';
Comment thread
yytelliot marked this conversation as resolved.
Outdated
const CONDUCTOR_TYPES_URL =
'https://cdn.jsdelivr.net/npm/@sourceacademy/conductor@0.3.0/dist/conductor/types/index.js';
Comment thread
yytelliot marked this conversation as resolved.
Outdated

function parseResult(text) {
const value = text.trim();
if (value.length === 0) {
return '';
}

try {
return JSON.parse(value);
} catch (e) {
return value;
}
}

function pushChunk(message) {
const waiter = chunkWaiters.shift();
if (waiter) {
waiter(message);
return;
function normaliseError(err, fallbackName) {
if (err && typeof err === 'object') {
return {
name: typeof err.name === 'string' ? err.name : fallbackName,
message: typeof err.message === 'string' ? err.message : String(err)
};
}
chunkQueue.push(message);

return { name: fallbackName, message: String(err) };
}

function popChunk() {
if (chunkQueue.length > 0) {
return Promise.resolve(chunkQueue.shift());
let runnerConductor;
let activeEvaluator;

function failActiveExecution(error) {
if (!runnerConductor) {
return;
}
return new Promise(resolve => {
chunkWaiters.push(resolve);
});

runnerConductor.sendError(normaliseError(error, 'DummyEvaluatorFatalError'));
activeEvaluator?.failExecution();
}

class BasicEvaluator {
class DummyEvaluator {
constructor(conductor) {
this.conductor = conductor;
runnerConductor = conductor;
activeEvaluator = this;
this.conductor.updateStatus(RunnerStatus.EVAL_READY, true);
}

beginExecution() {
this.conductor.updateStatus(RunnerStatus.EVAL_READY, false);
this.conductor.updateStatus(RunnerStatus.WAITING, false);
this.conductor.updateStatus(RunnerStatus.RUNNING, true);
}

finishExecution() {
this.conductor.updateStatus(RunnerStatus.RUNNING, false);
this.conductor.updateStatus(RunnerStatus.WAITING, true);
}

stopExecution() {
this.conductor.updateStatus(RunnerStatus.RUNNING, false);
this.conductor.updateStatus(RunnerStatus.WAITING, false);
this.conductor.updateStatus(RunnerStatus.STOPPED, true);
}

failExecution() {
this.conductor.updateStatus(RunnerStatus.RUNNING, false);
this.conductor.updateStatus(RunnerStatus.WAITING, false);
this.conductor.updateStatus(RunnerStatus.ERROR, true);
}

sendDisplayResult(result) {
this.conductor.sendResult(result);
}

sendDisplayError(error) {
this.conductor.sendError(error);
}

async startEvaluator(entryPoint) {
await this.evaluateFile(entryPoint, '');
const fileContent = await this.conductor.requestFile(entryPoint);
if (!fileContent) {
throw new Error('Cannot load entrypoint file');
}

this.beginExecution();
const shouldContinue = await this.evaluateFile(entryPoint, fileContent);
if (shouldContinue === false) {
return;
}
this.finishExecution();

while (true) {
this.conductor.updateStatus(RunnerStatus.WAITING, true);
const chunk = await this.conductor.requestChunk();
await this.evaluateChunk(chunk);
this.beginExecution();
const shouldContinue = await this.evaluateChunk(chunk);
if (shouldContinue === false) {
return;
}
this.finishExecution();
}
}

async evaluateFile(fileName, fileContent) {
return this.evaluateChunk(fileContent);
}
}

class DummyEvaluator extends BasicEvaluator {
async evaluateFile(fileName, fileContent) {
this.conductor.sendOutput('[dummy] output message');
this.conductor.sendResult('[dummy] result message');
this.conductor.sendError({ name: 'DummyEvaluatorError', message: '[dummy] error message' });
this.conductor.sendOutput(`[dummy] loaded file 1`);
this.conductor.sendOutput(`[dummy] loaded file 2`);
this.sendDisplayError({ name: 'DummyEvaluatorError', message: '[dummy] error message' });
this.conductor.sendOutput(`[dummy] loaded file 3`);
this.sendDisplayResult('[dummy] result message');

return true;
}

async evaluateChunk(chunk) {
Expand All @@ -90,89 +136,54 @@
}

if (text.startsWith('result ')) {
this.conductor.sendResult(parseResult(text.slice(7)));
return;
this.sendDisplayResult(parseResult(text.slice(7)));
return true;
}

if (text.startsWith('error ')) {
this.conductor.sendError({ name: 'DummyEvaluatorError', message: text.slice(6) });
return;
this.sendDisplayError({ name: 'DummyEvaluatorError', message: text.slice(6) });
return true;
}

this.conductor.sendOutput('[dummy] try: output ..., result ..., error ...');
}
}

const conductor = {
async requestChunk() {
const message = await popChunk();
return typeof message?.chunk === 'string' ? message.chunk : '';
},
sendOutput(message) {
post(CHANNEL.STDIO, { message: String(message) });
},
sendResult(value) {
post(CHANNEL.RESULT, { result: value });
},
sendError(error) {
post(CHANNEL.ERROR, { error });
}
};

const evaluator = new DummyEvaluator(conductor);

function onService(message) {
if (!message || typeof message.type !== 'number') {
return;
}
if (text === 'stop') {
this.stopExecution();
return false;
}

if (message.type === SERVICE.HELLO) {
post(CHANNEL.SERVICE, { type: SERVICE.HELLO, data: { version: 0 } });
return;
}
if (text.startsWith('fatal ')) {
this.sendDisplayError({ name: 'DummyEvaluatorError', message: text.slice(6) });
this.failExecution();
return false;
}

if (message.type === SERVICE.ENTRY) {
evaluator.startEvaluator(message.data).catch(function (err) {
conductor.sendError({
name: 'DummyEvaluatorFatalError',
message: err && err.message ? err.message : String(err)
});
});
this.conductor.sendOutput('[dummy] try: output ..., result ..., error ..., stop, fatal ...');
return true;
}
}

self.addEventListener('message', function (event) {
const data = event.data;
if (!Array.isArray(data) || data.length !== 2) {
return;
}
const runner = await import(CONDUCTOR_RUNNER_URL);
const { RunnerStatus } = await import(CONDUCTOR_TYPES_URL);
const initialise = runner.initialise;

const channelName = data[0];
const port = data[1];
if (typeof channelName !== 'string' || !port) {
return;
}

ports[channelName] = port;
if (typeof initialise !== 'function') {
throw new Error('Failed to load conductor runner initialise()');
}

if (channelName === CHANNEL.SERVICE) {
port.addEventListener('message', function (e) {
onService(e.data);
});
port.start();
return;
}
self.addEventListener('unhandledrejection', function (event) {
event.preventDefault();
failActiveExecution(event.reason);
});

if (channelName === CHANNEL.CHUNK) {
port.addEventListener('message', function (e) {
pushChunk(e.data);
});
port.start();
return;
}
initialise(DummyEvaluator);

if (typeof port.start === 'function') {
port.start();
}
});
})();
self.removeEventListener('message', captureEarlyMessage);
for (const event of earlyMessages) {
self.dispatchEvent(
new MessageEvent('message', {
data: event.data
})
);
}
Comment on lines +181 to +185
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: When replaying buffered MessageEvent objects, the ports property is omitted, causing any transferred MessagePort objects to be lost.
Severity: HIGH

Suggested Fix

When creating the new MessageEvent for replay, ensure that the ports property from the original buffered event is included in the event's constructor options, alongside the data property. This will preserve any transferred MessagePort objects.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: public/evaluators/debug/dummy-conductor-evaluator.js#L181-L185

Potential issue: When replaying buffered `MessageEvent` objects that arrive before
asynchronous imports are complete, the `ports` property is not copied from the original
event to the replayed event. This leads to the permanent loss of any `MessagePort`
objects that were transferred. As a consequence, communication handshakes that rely on
these ports, such as channel-attach, can fail silently, as the necessary communication
channel is never established.

})().catch(function (err) {
console.error('Failed to bootstrap dummy conductor evaluator:', err);
});
14 changes: 14 additions & 0 deletions src/commons/application/actions/InterpreterActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ const InterpreterActions = createActions('interpreter', {
logString,
workspaceLocation
}),
appendInterpreterResult: (value: Value, workspaceLocation: WorkspaceLocation) => ({
type: 'result',
value,
workspaceLocation
}),
Comment thread
yytelliot marked this conversation as resolved.
evalInterpreterSuccess: (value: Value, workspaceLocation: WorkspaceLocation) => ({
type: 'result',
value,
Expand All @@ -31,6 +36,15 @@ const InterpreterActions = createActions('interpreter', {
errors,
workspaceLocation
}),
appendInterpreterError: (errors: SourceError[], workspaceLocation: WorkspaceLocation) => ({
type: 'errors',
errors,
workspaceLocation
}),
setIsRunning: (isRunning: boolean, workspaceLocation: WorkspaceLocation) => ({
isRunning,
workspaceLocation
}),
beginInterruptExecution: (workspaceLocation: WorkspaceLocation) => ({ workspaceLocation }),
endInterruptExecution: (workspaceLocation: WorkspaceLocation) => ({ workspaceLocation }),
beginDebuggerPause: (workspaceLocation: WorkspaceLocation) => ({ workspaceLocation }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ test('evalInterpreterSuccess generates correct action object', () => {
});
});

test('appendInterpreterResult generates correct action object', () => {
const value = 'value';
const action = InterpreterActions.appendInterpreterResult(value, gradingWorkspace);
expect(action).toEqual({
type: InterpreterActions.appendInterpreterResult.type,
payload: {
type: 'result',
value,
workspaceLocation: gradingWorkspace
}
});
});

test('evalTestcaseSuccess generates correct action object', () => {
const value = 'another value';
const index = 3;
Expand Down Expand Up @@ -58,6 +71,30 @@ test('evalInterpreterError generates correct action object', () => {
});
});

test('appendInterpreterError generates correct action object', () => {
const errors: any = [];
const action = InterpreterActions.appendInterpreterError(errors, assessmentWorkspace);
expect(action).toEqual({
type: InterpreterActions.appendInterpreterError.type,
payload: {
type: 'errors',
errors,
workspaceLocation: assessmentWorkspace
}
});
});

test('setIsRunning generates correct action object', () => {
const action = InterpreterActions.setIsRunning(false, playgroundWorkspace);
expect(action).toEqual({
type: InterpreterActions.setIsRunning.type,
payload: {
isRunning: false,
workspaceLocation: playgroundWorkspace
}
});
});

test('beginInterruptExecution generates correct action object', () => {
const action = InterpreterActions.beginInterruptExecution(gradingWorkspace);
expect(action).toEqual({
Expand Down
Loading
Loading