Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
96 changes: 96 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const {
serializeMessage,
deserializeMessage,
} = require('./lib/serialization.js');
const { spawn } = require('child_process');

/**
* Get the version of the generator that was used for the currently present interfaces.
Expand Down Expand Up @@ -89,6 +90,79 @@ async function getCurrentGeneratorVersion() {

let _rosVersionChecked = false;

/**
* Run a ROS2 package executable using 'ros2 run' command.
* @param {string} packageName - The name of the ROS2 package.
* @param {string} executableName - The name of the executable to run.
* @param {string[]} [args=[]] - Additional arguments to pass to the executable.
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
*/
function ros2Run(packageName, executableName, args = []) {
return new Promise((resolve, reject) => {
if (typeof packageName !== 'string' || !packageName.trim()) {
reject(new Error('Package name must be a non-empty string'));
return;
}

if (typeof executableName !== 'string' || !executableName.trim()) {
reject(new Error('Executable name must be a non-empty string'));
return;
}

if (!Array.isArray(args)) {
reject(new Error('Arguments must be an array'));
return;
}

const command = 'ros2';
const cmdArgs = ['run', packageName, executableName, ...args];
const childProcess = spawn(command, cmdArgs);
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

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

The spawn call lacks proper validation of the command and arguments, which could lead to command injection if packageName, executableName, or args contain shell metacharacters. Consider sanitizing inputs or using shell: false option explicitly.

Suggested change
const childProcess = spawn(command, cmdArgs);
const childProcess = spawn(command, cmdArgs, { shell: false });

Copilot uses AI. Check for mistakes.

childProcess.on('error', (error) => {
reject(new Error(`Failed to start ros2 run: ${error.message}`));
});

resolve({
process: childProcess,
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

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

The function resolves immediately after spawning the process without waiting for it to start successfully. Consider waiting for the 'spawn' event or checking if the process is running before resolving to provide better error handling.

Suggested change
process: childProcess,
childProcess.once('error', (error) => {
reject(new Error(`Failed to start ros2 run: ${error.message}`));
});
childProcess.once('spawn', () => {
resolve({
process: childProcess,
});

Copilot uses AI. Check for mistakes.
});
});
}

/**
* Run a ROS2 launch file using 'ros2 launch' command.
* @param {string} packageName - The name of the ROS2 package.
* @param {string} launchFile - The name of the launch file to run.
* @param {string[]} [args=[]] - Additional arguments to pass to the launch file.
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
*/
function ros2Launch(packageName, launchFile, args = []) {
return new Promise((resolve, reject) => {
if (typeof packageName !== 'string' || !packageName.trim()) {
reject(new Error('Package name must be a non-empty string'));
return;
}
if (typeof launchFile !== 'string' || !launchFile.trim()) {
reject(new Error('Launch file name must be a non-empty string'));
return;
}
if (!Array.isArray(args)) {
reject(new Error('Arguments must be an array'));
return;
}
const command = 'ros2';
const cmdArgs = ['launch', packageName, launchFile, ...args];
const childProcess = spawn(command, cmdArgs);
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

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

The spawn call lacks proper validation of the command and arguments, which could lead to command injection if packageName, launchFile, or args contain shell metacharacters. Consider sanitizing inputs or using shell: false option explicitly.

Suggested change
const childProcess = spawn(command, cmdArgs);
const childProcess = spawn(command, cmdArgs, { shell: false });

Copilot uses AI. Check for mistakes.

childProcess.on('error', (error) => {
reject(new Error(`Failed to start ros2 launch: ${error.message}`));
});

resolve({
process: childProcess,
});
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

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

The function resolves immediately after spawning the process without waiting for it to start successfully. Consider waiting for the 'spawn' event or checking if the process is running before resolving to provide better error handling.

Suggested change
});
// Wait for immediate errors before resolving
childProcess.once('error', (error) => {
reject(new Error(`Failed to start ros2 launch: ${error.message}`));
});
setImmediate(() => {
// If no error occurred, resolve
resolve({
process: childProcess,
});
});

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

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

The 'spawn' event is not standard in Node.js child_process. Use 'childProcess.pid' check or a timeout to confirm successful spawn instead of relying on a non-existent event.

Suggested change
});
// The 'spawn' event is not standard; instead, check for childProcess.pid
if (childProcess.pid) {
resolve({
process: childProcess,
});
} else {
// Fallback: wait a short time to see if pid is set
setTimeout(() => {
if (childProcess.pid) {
resolve({
process: childProcess,
});
} else {
reject(new Error('Failed to spawn ros2 process'));
}
}, 50);
}

Copilot uses AI. Check for mistakes.
});
}

/**
* A module that exposes the rclnodejs interfaces.
* @exports rclnodejs
Expand Down Expand Up @@ -444,6 +518,28 @@ let rcl = {
// this will not throw even if the handler is already removed
process.removeListener('SIGINT', _sigHandler);
},

/**
* Run a ROS2 package executable using 'ros2 run' command.
* @param {string} packageName - The name of the ROS2 package.
* @param {string} executableName - The name of the executable to run.
* @param {string[]} [args=[]] - Additional arguments to pass to the executable.
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
*/
ros2Run(packageName, executableName, args = []) {
return ros2Run(packageName, executableName, args);
},

/**
* Run a ROS2 launch file using 'ros2 launch' command.
* @param {string} packageName - The name of the ROS2 package.
* @param {string} launchFile - The name of the launch file to run.
* @param {string[]} [args=[]] - Additional arguments to pass to the launch file.
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
*/
ros2Launch(packageName, launchFile, args = []) {
return ros2Launch(packageName, launchFile, args);
},
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

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

[nitpick] The method is a simple wrapper around the standalone function. Consider removing the duplication by either using the standalone function directly or implementing the logic only in the object method.

Suggested change
},

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

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

[nitpick] The method is a simple wrapper around the standalone function. Consider removing the duplication by either using the standalone function directly or implementing the logic only in the object method.

Suggested change
},
ros2Launch: ros2Launch,

Copilot uses AI. Check for mistakes.
};

const _sigHandler = () => {
Expand Down
7 changes: 7 additions & 0 deletions test/types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { expectType, expectAssignable } from 'tsd';
import * as rclnodejs from 'rclnodejs';
import { ChildProcess } from 'child_process';

const NODE_NAME = 'test_node';
const LIFECYCLE_NODE_NAME = 'lifecycle_test_node';
Expand All @@ -17,6 +18,12 @@ expectType<string | undefined>(rclnodejs.DistroUtils.getDistroName());
expectType<boolean>(rclnodejs.isShutdown());
expectType<void>(rclnodejs.shutdown());
expectType<void>(rclnodejs.removeSignalHandlers());
expectType<Promise<{ process: ChildProcess }>>(
rclnodejs.ros2Run('package_name', 'executable_name', ['arg1', 'arg2'])
);
expectType<Promise<{ process: ChildProcess }>>(
rclnodejs.ros2Launch('package_name', 'launch_file', ['arg1', 'arg2'])
);

// ---- DistroUtil ----
expectType<rclnodejs.DistroUtils.DistroId>(rclnodejs.DistroUtils.getDistroId());
Expand Down
28 changes: 28 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference path="./base.d.ts" />

import { ChildProcess } from 'child_process';

declare module 'rclnodejs' {
type Class = new (...args: any[]) => any;

Expand Down Expand Up @@ -207,4 +209,30 @@ declare module 'rclnodejs' {
* @returns An Object representing the deserialized message.
*/
function deserializeMessage(buffer: Buffer, typeClass: Class): object;

/**
* Run a ROS2 package executable using 'ros2 run' command.
* @param {string} packageName - The name of the ROS2 package.
* @param {string} executableName - The name of the executable to run.
* @param {string[]} [args=[]] - Additional arguments to pass to the executable.
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
*/
function ros2Run(
packageName: string,
executableName: string,
args: string[]
): Promise<{ process: ChildProcess }>;

/**
* Run a ROS2 launch file using 'ros2 launch' command.
* @param {string} packageName - The name of the ROS2 package.
* @param {string} launchFile - The name of the launch file to run.
* @param {string[]} [args=[]] - Additional arguments to pass to the launch file.
* @return {Promise<{process: ChildProcess}>} A Promise that resolves with the process.
*/
function ros2Launch(
packageName: string,
launchFile: string,
args: string[]
): Promise<{ process: ChildProcess }>;
}
Loading