1

I'm trying to execute some shell commands synchronously to install npm dependencies, build packages and create a database in docker.

    ['api', 'front-end'].forEach(async (dir) => {
        await new Promise((resolve, reject) => {
          console.log(`Installing npm dependencies for ${dir}`);
          exec('npm install', { cwd: path.join(initDir, 'pushkin', dir) }, (err) => {
            if (err) console.error(`Failed to install npm dependencies for ${dir}: ${err}`);
            if (dir !== 'api' && dir !== 'front-end') return;
          });
          resolve(`${dir} installed...`);
        })
          .then(() => {
            console.log(`Building ${dir}`);
            exec('npm run build', { cwd: path.join(process.cwd(), 'pushkin', dir) }, (err) => {
              if (err) console.error(`Failed to build ${dir}: ${err}`);
              console.log(`${dir} is built`);
            });
          })
          .then(() => {
            shell.exec(startDbCommand);
          })
          .then(() => {
            shell.exec(createDbCommand);
          })
          .then(() => {
            shell.exec(stopDbCommand);
          });
      });

The docker commands are:

const startDbCommand = 'docker-compose -f pushkin/docker-compose.dev.yml up --no-start && docker-compose -f pushkin/docker-compose.dev.yml start test_db';
const createDbCommand = 'docker-compose -f pushkin/docker-compose.dev.yml exec -T test_db psql -U postgres -c "create database test_db"';
const stopDbCommand = 'docker-compose -f pushkin/docker-compose.dev.yml stop test_db';

When I ran it for the first time, I got this error:

No container found for test_db_1

Failed to build front-end: Error: Command failed: npm run build
sh: react-scripts: command not found

Failed to build api: Error: Command failed: npm run build
sh: babel: command not found

However, after I ran it again for the second time, everything seems to be fine. Is this a problem about the Promise chain I wrote? Thanks.

4
  • 3
    synchronously? Your questions is slightly mixed up, your using promises, did you mean asynchronously.. If you wanted synchronously you would use execSync Commented May 5, 2020 at 9:05
  • Promises don't make anything synchronous. They can help making asynchronous things sequential though Commented May 5, 2020 at 9:10
  • Can you post your Dockerfile? Commented May 5, 2020 at 9:21
  • I have no idea why would you do this in nodejs? Why not run everything in dockerfile / docker-compose file? Commented May 5, 2020 at 9:23

1 Answer 1

2

Two important things are the running commands in order one after the other (I believe that's what you mean by synchronously?) and also bailing when there is a failure.

The project directory loop also looks out of place. At the moment it loops over everything, including the db setup commands. It looks like you are doing test setup, so I believe the "synchronous" order is:

  • npm install/build for api
  • npm install/build for frontend
  • database setup

So first, create a promise out of nodes spawn so you can await it.

function runProcessToCompletion(cmd_array, options){
  return new Promise((resolve, reject) => {

    const result = {
      cmd: cmd_array,
      options,
      code: null,
      output: [],
    }

    const proc = spawn(cmd_array[0], cmd_array.slice(1), options)

    proc.on('error', (error) => {
      error.result = result
      reject(error)
    })

    proc.on('close', code => {
      result.code = code
      if (code !== 0) {
        const error = new Error(`PID "${proc.pid}" exited with code "${code}"`)
        error.result = result
        reject(error)
      }
      console.log(`Spawned PID "${proc.pid}" exited with code "${code}"`)
      resolve(result)
    })

    proc.stdout.on('data', (data) => {
      result.output.push(data.toString())
      process.stdout.write(data)
    })

    proc.stderr.on('data', (data) => {
      result.output.push(data.toString())
      process.stderr.write(data)
    })

    if (proc.pid) {
      console.log(`Spawned PID "${proc.pid}" for "${cmd_array.join(' ')}"`)
    }
  })
}

Then you can more easily structure your code as a simple list of commands. The benefit of using spawn is you can avoid all the shell-isms. The downside is you miss out on all the shell-isms.

For example the paths to executables need to be fully defined without a shells PATH

const path = require('path')
const initDir = process.cwd()
const project_dirs = ['api', 'front-end']
const setupDbCommand = ['/usr/local/bin/docker-compose','-f','pushkin/docker-compose.dev.yml','up','--no-start']
const startDbCommand = ['/usr/local/bin/docker-compose','-f','pushkin/docker-compose.dev.yml','start','test_db']
const createDbCommand = ['/usr/local/bin/docker-compose','-f','pushkin/docker-compose.dev.yml','exec','-T','test_db','psql -U postgres -c "create database test_db"']
const stopDbCommand = ['/usr/local/bin/docker-compose','-f','pushkin/docker-compose.dev.yml','stop','test_db']

async function go(){
  for (let dir of project_dirs) {
    const cwd = path.join(initDir, 'pushkin', dir)
    await runProcessToCompletion(['/usr/local/bin/npm','install'], { cwd })
    await runProcessToCompletion(['/usr/local/bin/npm','run','build'], { cwd })
  }
  await runProcessToCompletion(setupDbCommand)
  await runProcessToCompletion(startDbCommand)
  await runProcessToCompletion(createDbCommand)
  await runProcessToCompletion(stopDbCommand)
  return true
}

go().catch(err => {
  console.error(err)
  console.error(err.results)
})

If it's too hard without the shell stuff, you can turn that back on with the spawn options

{ shell: true }
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.