Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3c9ead0
SED-4581 Implementing agent forking in Node.js agent and support for …
jeromecomte Mar 6, 2026
1a76f3a
SED-4581 Fixing copying of agent forker files
jeromecomte Mar 6, 2026
38f8fb9
SED-4581 Refactoring Node.js agent code
jeromecomte Mar 7, 2026
d563c0f
SED-4581 Aligning Node.js KW API with Java
jeromecomte Mar 7, 2026
a12119c
SED-4581 Adding debug logs
jeromecomte Mar 7, 2026
0afb116
SED-4581 Replacing callback in OutputBuilder by async KW functions
jeromecomte Mar 7, 2026
cd4efbb
SED-4581 Fixing forked process leak in linux
jeromecomte Mar 7, 2026
4ed4fdc
SED-4581 Migrating tests to jest
jeromecomte Mar 9, 2026
eda0ee6
SED-4581 Migrating tests to jest
jeromecomte Mar 9, 2026
a5bdc4a
SED-4581 Implementing attachment of npm install and agent forker logs
jeromecomte Mar 11, 2026
ed1404c
SED-4581 Refactoring logging
jeromecomte Mar 12, 2026
23815bf
SED-4581 Implementing KW timeout handling, improving attachment of pr…
jeromecomte Mar 13, 2026
8a6a87e
SED-4581 Improving error handling when keyword dir is missing
jeromecomte Mar 13, 2026
8b68411
SED-4581 Fixing error when agent properties not set and improving log…
jeromecomte Mar 13, 2026
706dba3
SED-4581 Supporting concurrency via npm project workspaces
jeromecomte Mar 16, 2026
fe0038e
SED-4581 PR feedbacks
jeromecomte Mar 19, 2026
f12d0ec
SED-4581 Fixing vulnerabilities
jeromecomte Mar 19, 2026
81c4041
SED-4581 Fixing eslint configuration
jeromecomte Mar 20, 2026
4e607c6
SED-4581 Deprecating output.send()
jeromecomte Mar 20, 2026
d4e78ae
SED-4581 Adding test coverage for properties
jeromecomte Mar 20, 2026
c05b688
SED-4581 Implementing async close
jeromecomte Mar 21, 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
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,21 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class AutomationPackageResourceUploader {

private static final Logger logger = LoggerFactory.getLogger(AutomationPackageResourceUploader.class);

private final Map<String, String> uniqueResourceReferences = new ConcurrentHashMap<>();

public String applyUniqueResourceReference(String resourceReference,
String resourceType,
StagingAutomationPackageContext context) {
return uniqueResourceReferences.computeIfAbsent(resourceReference, key -> applyResourceReference(resourceReference, resourceType, context));
};

public String applyResourceReference(String resourceReference,
String resourceType,
StagingAutomationPackageContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
public class StagingAutomationPackageContext extends AutomationPackageContext {

private final AutomationPackageArchive automationPackageArchive;
private final AutomationPackageResourceUploader resourceUploader = new AutomationPackageResourceUploader();

public StagingAutomationPackageContext(AutomationPackage automationPackage, AutomationPackageOperationMode operationMode,
ResourceManager resourceManager, AutomationPackageArchive automationPackageArchive,
Expand All @@ -19,4 +20,8 @@ public StagingAutomationPackageContext(AutomationPackage automationPackage, Auto
public AutomationPackageArchive getAutomationPackageArchive() {
return automationPackageArchive;
}

public AutomationPackageResourceUploader getResourceUploader() {
return resourceUploader;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ public void setJsfile(DynamicValue<String> jsfile) {
@Override
protected void fillDeclaredFields(NodeFunction function, StagingAutomationPackageContext context) {
super.fillDeclaredFields(function, context);
AutomationPackageResourceUploader resourceUploader = new AutomationPackageResourceUploader();
AutomationPackageResourceUploader resourceUploader = context.getResourceUploader();

String filePath = jsfile.get();
String fileRef = resourceUploader.applyResourceReference(filePath, ResourceManager.RESOURCE_TYPE_FUNCTIONS, context);
String fileRef = resourceUploader.applyUniqueResourceReference(filePath, ResourceManager.RESOURCE_TYPE_FUNCTIONS, context);
if (fileRef != null) {
function.setJsFile(new DynamicValue<>(fileRef));
}
Expand Down
3 changes: 0 additions & 3 deletions step-node/step-node-agent/.eslintrc.js

This file was deleted.

4 changes: 3 additions & 1 deletion step-node/step-node-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
node_modules/
.npm
!/bin
filemanager/work/
filemanager/work/
/coverage/
/npm-project-workspaces/
117 changes: 117 additions & 0 deletions step-node/step-node-agent/api/controllers/agent-fork.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (C) 2026, exense GmbH
*
* This file is part of Step
*
* Step is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Step is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Step. If not, see <http://www.gnu.org/licenses/>.
*/

const { OutputBuilder } = require("./output");
const Session = require("./session");
const fs = require("fs");
const path = require('path')
const session = new Session();

process.on('message', async ({ type, projectPath, functionName, input, properties, keywordDirectory }) => {
if (type === 'KEYWORD') {
console.log("[Agent fork] Calling keyword " + functionName)
const outputBuilder = new OutputBuilder();
try {
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 {
const kwModules = await importAllKeywords(projectPath, keywordDirectory);
let keywordSearchResult = searchKeyword(kwModules, functionName);
if (!keywordSearchResult) {
console.log('[Agent fork] Unable to find Keyword ' + functionName + "'");
outputBuilder.fail("Unable to find Keyword '" + functionName + "'");
} else {
const module = keywordSearchResult.keywordModule;
const keyword = keywordSearchResult.keywordFunction;

try {
const beforeKeyword = module['beforeKeyword'];
if(beforeKeyword) {
await beforeKeyword(functionName);
}
await keyword(input, outputBuilder, session, properties);
} catch (e) {
const onError = module['onError'];
if (onError) {
if (await onError(e, input, outputBuilder, session, properties)) {
console.log('[Agent fork] Keyword execution failed and onError hook returned \'true\'')
outputBuilder.fail(e)
} else {
console.log('[Agent fork] Keyword execution failed and onError hook returned \'false\'')
}
} else {
console.log('[Agent fork] Keyword execution failed. No onError hook defined')

Choose a reason for hiding this comment

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

medium

Direct console.log calls are used here. For consistency with the rest of the agent and to leverage the new structured logging capabilities, please use the logger utility instead.

Suggested change
console.log('[Agent fork] Keyword execution failed. No onError hook defined')
logger.info('[Agent fork] Keyword execution failed. No onError hook defined')

outputBuilder.fail(e)
}
} finally {
let afterKeyword = module['afterKeyword'];
if (afterKeyword) {
await afterKeyword(functionName);
}
}
}
}
} finally {
console.log("[Agent fork] Returning output")

Choose a reason for hiding this comment

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

medium

Direct console.log calls are used here. For consistency with the rest of the agent and to leverage the new structured logging capabilities, please use the logger utility instead.

Suggested change
console.log("[Agent fork] Returning output")
logger.info("[Agent fork] Returning output")

process.send(outputBuilder.build());
}
} else if (type === 'KILL') {
console.log("[Agent fork] Exiting...")

Choose a reason for hiding this comment

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

medium

Direct console.log calls are used here. For consistency with the rest of the agent and to leverage the new structured logging capabilities, please use the logger utility instead.

Suggested change
console.log("[Agent fork] Exiting...")
logger.info("[Agent fork] Exiting...")

await session.asyncDispose();
process.exit(1)

Choose a reason for hiding this comment

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

medium

Using process.exit(1) indicates an abnormal termination. If the KILL message is intended for a graceful shutdown, process.exit(0) would be more appropriate to signal successful termination.

Suggested change
process.exit(1)
process.exit(0)

}

function keywordDirectoryExists(projectPath, keywordDirectory) {
return fs.existsSync(path.resolve(projectPath, keywordDirectory))
}

async function importAllKeywords(projectPath, keywordDirectory) {
const kwModules = [];
const kwDir = path.resolve(projectPath, keywordDirectory);
console.log("[Agent fork] Searching keywords in: " + kwDir)

Choose a reason for hiding this comment

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

medium

Direct console.log calls are used here. For consistency with the rest of the agent and to leverage the new structured logging capabilities, please use the logger utility instead.

Suggested change
console.log("[Agent fork] Searching keywords in: " + kwDir)
logger.info("[Agent fork] Searching keywords in: " + kwDir)

const kwFiles = fs.readdirSync(kwDir);
for (const kwFile of kwFiles) {
if (kwFile.endsWith('.js')) {
let kwModule = "file://" + path.resolve(kwDir, kwFile);
console.log("[Agent fork] Importing keywords from module: " + kwModule)

Choose a reason for hiding this comment

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

medium

Direct console.log calls are used here. For consistency with the rest of the agent and to leverage the new structured logging capabilities, please use the logger utility instead.

Suggested change
console.log("[Agent fork] Importing keywords from module: " + kwModule)
logger.info("[Agent fork] Importing keywords from module: " + kwModule)

let module = await import(kwModule);
kwModules.push(module);
}
}
return kwModules;
}

function searchKeyword(kwModules, keywordName) {
const kwModule = kwModules.find(m => m[keywordName]);
return kwModule ? {keywordFunction: kwModule[keywordName], keywordModule: kwModule} : undefined;
}
});

process.on('unhandledRejection', error => {
console.log('[Agent fork] Critical: an unhandled error (unhandled promise rejection) occurred and might not have been reported', error)

Choose a reason for hiding this comment

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

medium

Direct console.log calls are used here. For consistency with the rest of the agent and to leverage the new structured logging capabilities, please use the logger utility instead.

Suggested change
console.log('[Agent fork] Critical: an unhandled error (unhandled promise rejection) occurred and might not have been reported', error)
logger.error('[Agent fork] Critical: an unhandled error (unhandled promise rejection) occurred and might not have been reported', error)

})

process.on('uncaughtException', error => {

Choose a reason for hiding this comment

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

medium

Direct console.log calls are used here. For consistency with the rest of the agent and to leverage the new structured logging capabilities, please use the logger utility instead.

  logger.error('[Agent fork] Critical: an unhandled error (uncaught exception) occurred and might not have been reported', error)

console.log('[Agent fork] Critical: an unhandled error (uncaught exception) occurred and might not have been reported', error)
})

process.on('SIGTERM', () => {
console.log("[Agent fork] Received SIGTERM. Exiting...")

Choose a reason for hiding this comment

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

medium

Direct console.log calls are used here. For consistency with the rest of the agent and to leverage the new structured logging capabilities, please use the logger utility instead.

  logger.info("[Agent fork] Received SIGTERM. Exiting...")

process.exit(1);

Choose a reason for hiding this comment

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

medium

Using process.exit(1) indicates an abnormal termination. If SIGTERM is intended for a graceful shutdown, process.exit(0) would be more appropriate to signal successful termination.

  process.exit(0);

});
Loading