skip to Main Content

I have a weird situation. I am building an Electron application, and need to return a function that will run an executable in the background and then return the output to the Renderer process. I have the IPC part working but can’t seem to figure out how to return the stdout of spawn as a variable that I can use within my React code. Here is the script I am currently using:

import { spawn } from 'child_process'
import path from 'path'
import { fileURLToPath } from 'url'
import { dirname } from 'path'

const _dirname = dirname(fileURLToPath(import.meta.url))

const exePath = path.resolve(_dirname, 'pict.exe')
const demoPath = path.resolve(_dirname, 'demo.txt')

const demo = () => {
  let output = '';

  try {
    const child = spawn(exePath, [demoPath]);

    child.stdout.on('data', (chunk) => {
      output += chunk.toString();
    });

    child.on('exit', () => {
      console.log(output);
    });

    return output;
  } catch (error) {
    console.log(error);
    return error
  }
}

const final = pict_demo();
console.log(final);

I know the spawn call is working because the console.log(output) is giving me the expected result. However, the "final" variable is empty and the console.log is outputting nothing. How can I properly store this result as a variable?

I have tried as many solutions on StackOverflow as I could, but many of the answers are quite outdated. I want to know the best way to do this now.

2

Answers


  1. the output variable is empty because you are mixing synchronous and asynchronous code. In the example you have shown:

    This runs first

    const child = spawn(exePath, [demoPath]);
    

    Adds your functions to an array of event handlers. So that when that event fires your function can fire.

    child.stdout.on('data', (chunk) => {
      output += chunk.toString();
    });
    
    child.on('exit', () => {
      console.log(output);
    });
    

    Returns the output:

    return output;
    

    So it returns the output even before any of the events are handled. (Can check even loop if you need more information)

    To handle any asynchronous scenario you may need to wrap them in Promise. But, for current solution you can just use spawnSync.

    An example:

    const ls = cp.spawnSync('ls', ['-l'], { encoding : 'utf8' });
    console.log('stdout here: n' + ls.stdout);
    

    Additional info:

    Just giving an example how you would wrap this in promise.

    const demo = () => {
      return new Promise((res, rej) => {
        let output = '';
        try {
          const child = spawn(exePath, [demoPath]);
    
          child.stdout.on('data', (chunk) => {
            output += chunk.toString();
          });
    
          child.on('exit', () => {
            console.log(output);
            res(output); //resolve here
          });
        } catch (error) {
          console.log(error);
          rej(error); //reject here.
        }
      })
    }
    

    Then just can do:

    demo().then(output => console.log(output)).catch(error => console.log(error));
    

    or in a async function:

    async runner() {
      try{
        const output = await demo();
        console.log(output)
      }catch(err){
        console.log(err)
      }
    }
    
    Login or Signup to reply.
  2. The issue you’re encountering is due to the asynchronous nature of the spawn method in Node.js. When you call spawn, it runs asynchronously, meaning the function demo will return before the process has completed and before the stdout data has been fully collected.

    To handle this properly, you should use Promises or async/await to ensure you wait for the process to complete before returning the output. Here’s how you can refactor your code to use Promises, like:

    import { spawn } from 'child_process';
    import path from 'path';
    import { fileURLToPath } from 'url';
    import { dirname } from 'path';
    
    const _dirname = dirname(fileURLToPath(import.meta.url));
    
    const exePath = path.resolve(_dirname, 'pict.exe');
    const demoPath = path.resolve(_dirname, 'demo.txt');
    
    const demo = () => {
      return new Promise((resolve, reject) => {
        let output = '';
    
        try {
          const child = spawn(exePath, [demoPath]);
    
          child.stdout.on('data', (chunk) => {
            output += chunk.toString();
          });
    
          child.on('error', (error) => {
            reject(error);
          });
    
          child.on('exit', (code) => {
            if (code === 0) {
              resolve(output);
            } else {
              reject(new Error(`Process exited with code ${code}`));
            }
          });
        } catch (error) {
          reject(error);
        }
      });
    };
    
    const runDemo = async () => {
      try {
        const finalOutput = await demo();
        console.log(finalOutput);
      } catch (error) {
        console.error(error);
      }
    };
    
    runDemo();

    I refactored the code:

    The demo function now returns a Promise. The Promise resolves with the output when the child process exits successfully or rejects with an error if something goes wrong.

    The runDemo function is declared as an async function and uses await to wait for the Promise returned by the demo to resolve before logging the output.
    Error handling is added to ensure any errors are caught and logged.

    This approach ensures that the function waits for the asynchronous operation to complete before attempting to use the result.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search