Jason Burgett
Jason Burgett

Reputation: 147

Promises confusion in NodeJS

I'm having a lot of trouble wrapping my head around the asynchronous nature of code execution on NodeJS. I have a simple function to fetch the output of ip a on a Linux machine and parse out the IP Subnet manually. I'd simply like to console.log() the IP Subnet after that's done.

I understand that NodeJS mostly runs asynchronously so I can't expect the logic to be finished before I console.log() the variable. I understand to concept of Callbacks to combat this problem, but I'd prefer to have access to the variable outside the logic loop. I turned to Promises, which seems like a good solution for this, but I think I'm missing something and they're not working the way I expected. Here's my code below:

let subnetString = '';

function getIPLinux() {
  return new Promise((resolve) => {
    const ipOutput = spawn( 'ip', ['a'] );

    ipOutput.stdout.on('data', (data) => {
        String(data).split('\n').forEach( (line) => {

            if ( line.includes('inet') && line.indexOf('inet6') < 0 && line.indexOf('127.0.0.1') < 0 ) {
                const ipSubnet = line.split(' ')[5];
                const ipStringArray = ipSubnet.split('.');
                subnetString = ipStringArray[0] + '.' + ipStringArray[1] + '.' + ipStringArray[2] + '.*';
                console.log('Found subnet at end of if loop: ' + subnetString);
            }

        })
    })

    console.log('Found subnet at end of promise: ' + subnetString);
    resolve();
  })
}

getIPLinux().then( () => {
  console.log('Found subnet after then: ' + subnetString);
});

My output is as follows:

Found subnet at end of promise: 
Found subnet after then: 
Found subnet at end of if loop: 192.168.1.*

Only the last line logged is correct. I'm having trouble wrapping my mind around this non-blocking code execution. I'm open to other methodologies too if I'm coming at this the wrong way.

Upvotes: 0

Views: 139

Answers (3)

molamk
molamk

Reputation: 4116

  • You need to resolve your Promise from inside the .on('data', (data) => { // here }) callback
  • Instead of writing subnetString as a global variable, pass it inside the Promise. Like this resolve(subnetString)
  • Don't forget to catch if there's any errors. For example if no valid lines have been found
  • Since you're returning only one subnetString you can use Array.find instead of doing a forEach loop. It take a "validation" callback and returns the item or null if not found

const isValidLine = l => l.includes('inet') && !l.includes('inet6') && !l.includes('127.0.0.1');

const extractSubnet = l => {
  const ipStringArray = l.split(' ')[5].split('.');
  return ipStringArray[0] + '.' + ipStringArray[1] + '.' + ipStringArray[2] + '.*';
}

function getIPLinux() {
  return new Promise((resolve, reject) => {
    const ipOutput = spawn('ip', ['a']);

    ipOutput.stdout.on('data', data => {
      const line = data.split('\n').find(isValidLine);

      // Resolve or Reject your promise here
      if (!line) {
        reject('Line not found');
      } else {
        const subnetString = extractSubnet(line);
        resolve(subnetString);
      }
    });
  });
}

getIPLinux().then(subnetString => {
  console.log('Found subnet after then: ' + subnetString);
});

Upvotes: 1

Estus Flask
Estus Flask

Reputation: 222309

This is a problem similar to this one that wasn't correctly addressed with promises.

resolve is synchronously called on promise construction, a promise resolves earlier than data arrives. A promise is supposed to resolve with data but it doesn't.

Considering that only single data should be processed, it should be:

function getIPLinux() {
  return new Promise((resolve) => {
    const ipOutput = spawn( 'ip', ['a'] );

   const handler = (data) => {
        String(data).split('\n').forEach((line) => {
            if (line.includes('inet') && line.indexOf('inet6') < 0 && line.indexOf('127.0.0.1') < 0) {
                const ipSubnet = line.split(' ')[5];
                const ipStringArray = ipSubnet.split('.');
                const subnetString = ipStringArray[0] + '.' + ipStringArray[1] + '.' + ipStringArray[2] + '.*';
                ipOutput.stdout.off('data', handler);
                resolve(subnetString);
            }
        })
    })

    ipOutput.stdout.on('data', handler);
  })
}

Upvotes: 0

Jeff Snider
Jeff Snider

Reputation: 135

spawn() is also async. You're using an event callback for the stdout which is good but you're resolving the promise immediately below that without waiting for the input to finish. Try

ipOutput.on('close', () => {
  console.log('Found subnet at end of promise: ' + subnetString);
  resolve(subnetString);
});

At the end of your promise.

https://nodejs.org/api/child_process.html#child_process_event_close

Upvotes: 2

Related Questions