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
1 change: 1 addition & 0 deletions step-node/step-node-agent/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules/
filemanager/work/
/coverage/
/npm-project-workspaces/
/agent-fork-libs/
16 changes: 15 additions & 1 deletion step-node/step-node-agent/api/controllers/agent-fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

const { OutputBuilder } = require("./output");
const { createLiveReporting } = require("./live-reporting");
const Session = require("./session");
const fs = require("fs");
const path = require('path')
Expand All @@ -34,7 +35,11 @@ process.on('message', async ({ type, projectPath, functionName, input, propertie
pendingUncaughtException = null;
console.log("[Agent fork] Calling keyword " + functionName)
const outputBuilder = new OutputBuilder();
let liveReporting;
try {
Comment on lines 37 to 39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

To prevent any unexpected errors during createLiveReporting initialization from crashing the fork process, declare liveReporting using let and initialize it inside the try block. This ensures any initialization errors are caught and reported gracefully via outputBuilder.fail.

    const outputBuilder = new OutputBuilder();
    let liveReporting;
    try {
      liveReporting = createLiveReporting(properties);

@jeromecomte jeromecomte Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — liveReporting is now declared with let and initialized as the first statement inside the try, so any initialization error is reported via outputBuilder.fail instead of escaping the message handler.

// Initialize inside the try so any initialization error is reported via outputBuilder.fail
// (in the catch below) rather than escaping the message handler.
liveReporting = createLiveReporting(properties);
if (!keywordDirectoryExists(projectPath, keywordDirectory)) {
outputBuilder.fail("The keyword directory '" + keywordDirectory + "' doesn't exist in " + path.basename(projectPath) + ". Possible cause: If using TypeScript, the keywords may not have been compiled. Fix: Ensure your project is built before deploying to Step or during 'npm install'.")
} else {
Expand All @@ -52,7 +57,7 @@ process.on('message', async ({ type, projectPath, functionName, input, propertie
if(beforeKeyword) {
await beforeKeyword(functionName);
}
await keyword(input, outputBuilder, session, properties);
await keyword(input, outputBuilder, session, properties, liveReporting);
} catch (e) {
console.log("[Agent fork] Keyword execution failed with following error", e)
const onError = module['onError'];
Expand Down Expand Up @@ -82,6 +87,15 @@ process.on('message', async ({ type, projectPath, functionName, input, propertie
// Flush the event loop so unhandledRejection / uncaughtException from the keyword
// (e.g. fire-and-forget promises, nextTick throws) land before we send the result.
await new Promise(resolve => setImmediate(resolve));
// Close live reporting: flushes any buffered measures and waits for in-flight uploads.
// Guarded because initialization above may have failed before liveReporting was assigned.
try {
if (liveReporting) {
await liveReporting.close();
}
} catch (e) {
console.log("[Agent fork] Error while closing live reporting", e);
}
Comment on lines +92 to +98

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Add a safety check to ensure liveReporting is defined before attempting to call close(), preventing a TypeError if initialization failed or was skipped.

      try {
        if (liveReporting) {
          await liveReporting.close();
        }
      } catch (e) {
        console.log("[Agent fork] Error while closing live reporting", e);
      }
References
  1. In the agent forker process (agent-fork.js), use console.log for logging as the main logger utility is not available.

@jeromecomte jeromecomte Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — added the if (liveReporting) guard before await liveReporting.close().

// Surface inter-keyword errors first, labelled clearly as coming from a previous keyword.
if (prevUnhandledRejection) {
const sep = outputBuilder.hasError() ? '\n' : '';
Expand Down
19 changes: 18 additions & 1 deletion step-node/step-node-agent/api/controllers/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ const logger = require('../logger').child({ component: 'Agent' });

const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';

// Resolve the bundled `ws` module path once, so it can be injected into forked keyword processes
// (whose own require() resolves against the keyword project, not the agent). Used by live reporting
// for streaming file uploads. If ws is missing, file uploads degrade to discarding.
let wsModulePath = null;
try {
wsModulePath = require.resolve('ws');
} catch {
logger.warn('The ws module could not be resolved; live reporting file uploads will be disabled');
}

process.on('unhandledRejection', error => {
logger.error('Critical: an unhandled error (unhandled promise rejection) occurred and might not have been reported:', error)
})
Expand Down Expand Up @@ -349,9 +359,16 @@ class ForkedAgent {
fs.copyFileSync(path.resolve(__dirname, 'agent-fork.js'), path.join(agentForkerLibPath, 'agent-fork.js'));
fs.copyFileSync(path.join(__dirname, 'output.js'), path.join(agentForkerLibPath, 'output.js'));
fs.copyFileSync(path.join(__dirname, 'session.js'), path.join(agentForkerLibPath, 'session.js'));
// The live-reporting code is split across a folder of modules; copy the whole directory so adding
// new modules never requires updating this list.
fs.cpSync(path.join(__dirname, 'live-reporting'), path.join(agentForkerLibPath, 'live-reporting'), { recursive: true });
this.agentForkerLibPath = agentForkerLibPath;
this.startupChunks = [];
this.forkProcess = fork(path.join(agentForkerLibPath, 'agent-fork.js'), [], {cwd: keywordProjectPath, silent: true});
const forkEnv = { ...process.env };
if (wsModulePath) {
forkEnv.STEP_AGENT_WS_MODULE = wsModulePath;
}
this.forkProcess = fork(path.join(agentForkerLibPath, 'agent-fork.js'), [], {cwd: keywordProjectPath, silent: true, env: forkEnv});
// Capture stdout/stderr immediately so startup crashes are not silently lost
if (this.forkProcess.stdout) {
this.forkProcess.stdout.on('data', (data) => this.startupChunks.push(data));
Expand Down
Loading