execProcess.mjs

import cp from 'child_process'
import get from 'lodash-es/get.js'
import isearr from './isearr.mjs'
import isbol from './isbol.mjs'
import isfun from './isfun.mjs'
import isestr from './isestr.mjs'
import genPm from './genPm.mjs'
import strleft from './strleft.mjs'
import strright from './strright.mjs'


/**
 * 呼叫執行檔執行
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/execProcess.test.mjs Github}
 * @memberOf wsemi
 * @param {String} prog 輸入執行檔或程式語言位置字串,若為註冊系統的全域指令,例如可直接給'Python',腳本需自行接收呼叫引數,並將回傳資料轉json字串後print/log到dos視窗,即可由nodejs接收
 * @param {String|Array} args 輸入腳本檔案位置字串或參數
 * @param {Object} [opt={}] 輸入設定物件
 * @param {Function} [opt.cbStdout=null] 輸入回調stdout函數,預設null
 * @param {Function} [opt.cbStderr=null] 輸入回調stderr函數,預設null
 * @param {String} [opt.codeCmd='big5'] 輸入讀回程序的stdout與stderr回應的解碼字串,為當前作業系統語系,預設'big5'
 * @returns {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息
 * @example
 * //need test in nodejs
 *
 * if (true) {
 *     let prog = 'prog.exe'
 *     let args = 'input'
 *     execProcess(prog, args)
 *         .then(function(data) {
 *             console.log('then', data)
 *         })
 *         .catch(function(data) {
 *             console.log('catch', data)
 *         })
 * }
 *
 * if (true) {
 *     let prog = `"C:\\Program Files\\7-Zip\\7z.exe"`
 *     let pw = 'AbcD1234'
 *     let args = `a aaa.7z aaa.txt -p${pw}`
 *     execProcess(prog, args)
 *         .then(function(data) {
 *             console.log('then', data)
 *         })
 *         .catch(function(data) {
 *             console.log('catch', data)
 *         })
 * }
 *
 */
function execProcess(prog, args, opt = {}) {

    //check
    if (!isearr(args) && !isestr(args)) {
        throw new Error(`args is not an effective array or string`)
    }
    if (isestr(args)) {
        args = [args]
    }

    //mode
    // spawn: 非同步執行命令,適合處理大量資料或長時間執行的程式,輸出以串流方式處理。spawnSync 為 spawn 的同步版本。
    // exec: 在 shell 中非同步執行命令,輸出被緩衝,適合輸出量較小的情況。execSync 為 exec 的同步版本。
    // execFile: 直接執行可執行檔案,不經過 shell,非同步執行,適合執行已知的可執行檔案。execFileSync 為 execFile 的同步版本。
    let mode = get(opt, 'mode')
    if (mode !== 'spawn' && mode !== 'exec' && mode !== 'execFile') {
        mode = 'spawn'
    }
    // console.log('mode',mode)

    //cbStdout
    let cbStdout = get(opt, 'cbStdout')

    //cbStderr
    let cbStderr = get(opt, 'cbStderr')

    //codeCmd
    let codeCmd = get(opt, 'codeCmd')
    if (!isestr(codeCmd)) {
        codeCmd = 'utf8'
    }

    //useChcp
    let useChcp = get(opt, 'useChcp')
    if (!isbol(useChcp)) {
        useChcp = false
    }

    //pm
    let pm = genPm()

    //cmsg, cerr
    let cmsg = ''
    let cerr = ''

    //r
    let r = null
    if (mode === 'spawn') {
        // console.log('mode',mode)
        let cr = strleft(prog, 1)
        let cl = strright(prog, 1)
        if (cr === `"` || cl === `"` || cr === `'` || cl === `'`) {
            throw new Error('prog of spawn doens not need to add quotes')
        }
        r = cp.spawn(prog, args, { encoding: codeCmd, shell: false }) //spwan的prog與args內檔案, 都不需要用單/雙引號括住, 已內建處理機制, 額外添加單/雙引號會導致錯誤
    }
    else if (mode === 'exec') {
        // console.log('mode',mode)
        let cpre = ''
        if (useChcp) {
            cpre = `cmd /c chcp 65001>nul &&`
        }
        // console.log(`${cpre} ${prog} ${args.join(' ')} & exit`)
        r = cp.exec(`${cpre} ${prog} ${args.join(' ')} & exit`, { encoding: codeCmd })
    }
    else if (mode === 'execFile') {
        // console.log('mode',mode)
        r = cp.execFile(prog, args, { encoding: codeCmd })
    }
    else {
        throw new Error(`invalid mode[${mode}]`)
    }

    //stdout data
    r.stdout.on('data', data => {
        // console.log('stdout chunk:', data.toString().trim())

        //cdata
        let cdata = data.toString().trim()

        //megre
        cmsg += cdata + '\n'

        //cbStdout
        if (isfun(cbStdout)) {
            cbStdout(cdata)
        }

    })

    //stderr data
    r.stderr.on('data', data => {
        // console.error('stderr chunk:', data.toString().trim())

        //cdata
        let cdata = data.toString().trim()

        //megre
        cerr += cdata + '\n'

        //cbStderr
        if (isfun(cbStderr)) {
            cbStderr(cdata)
        }

    })

    // //exit, 會比close先觸發故不使用
    // r.on('exit', (code) => {
    //     // console.log('exit code', code)
    // })

    //close
    r.on('close', code => {
        // console.log('close code', code)
        // if (code !== 0) {
        //     pm.reject(`code=${code} and stderr='${cerr}'`)
        // }
        // else if(isestr(cerr)){
        //     pm.reject(cerr)
        // }
        if (isestr(cerr)) {
            pm.reject(cerr)
        }
        else {
            pm.resolve(cmsg)
        }
    })

    //error
    r.on('error', (err) => {
        pm.reject(err)
    })

    return pm
}


// function execProcess2(prog, args, opt = {}) {

//     //cbStdout
//     let cbStdout = get(opt, 'cbStdout')

//     //cbStderr
//     let cbStderr = get(opt, 'cbStderr')

//     //codeCmd
//     let codeCmd = get(opt, 'codeCmd')
//     if (!isestr(codeCmd)) {
//         codeCmd = 'big5'
//     }

//     return new Promise(function(resolve, reject) {
//         let msg = ''

//         //spawn: 非同步執行命令,適合處理大量資料或長時間執行的程式,輸出以串流方式處理。spawnSync 為 spawn 的同步版本。
//         //exec: 在 shell 中非同步執行命令,輸出被緩衝,適合輸出量較小的情況。execSync 為 exec 的同步版本。
//         //execFile: 直接執行可執行檔案,不經過 shell,非同步執行,適合執行已知的可執行檔案。execFileSync 為 execFile 的同步版本。

//         //exec
//         //不能用同步版execSync, 須提供cpu控制權給調用端, 才能驅動例如偵測檔案等進行額外顯示
//         //執行程序時會使用當前作業系統語系, 故回傳時得要依照當前語系進行指定解碼, 才不會有亂碼
//         let r = cp.exec(`${prog} ${args}`, { encoding: 'buffer' }, (err, stdout, stderr) => {

//             //check
//             if (err) {
//                 // console.log('err',err)
//                 // err = iconv.decode(err, codeCmd) //err是當前作業系統語系且編碼過, 若出現err則編碼為big5而非buffer, 故不能decode
//                 // console.log('err(decode)',err)
//                 return reject(err)
//             }

//             try {
//                 // console.log('stdout', stdout)
//                 stdout = iconv.decode(stdout, codeCmd)
//                 // console.log('stdout(decode)', stdout)
//             }
//             catch (err) {}
//             try {
//                 // console.log('stderr', stderr)
//                 stderr = iconv.decode(stderr, codeCmd)
//                 // console.log('stderr(decode)', stderr)
//             }
//             catch (err) {}

//             //stderr, 若stderr與stdout同時有, 則先添加stderr再添加stdout
//             if (isestr(stderr)) {
//                 // console.log('stderr', stderr)
//                 msg += stderr
//             }

//             //stdout
//             if (isestr(stdout)) {
//                 // console.log('stdout', stdout)
//                 msg += stdout
//             }

//         })

//         // //exit, 會比close先觸發
//         // r.on('exit', (code) => {
//         //     // console.log('exit code', code)
//         // })

//         //close
//         r.on('close', (code) => {
//             // console.log('close code', code)
//             resolve(msg)
//         })

//         //cbStdout
//         if (isfun(cbStdout)) {
//             r.stdout.on('data', function (data) {
//                 data = iconv.decode(data, codeCmd)
//                 // console.log('stdout', data)
//                 cbStdout(data)
//             })
//         }

//         //cbStderr
//         if (isfun(cbStderr)) {
//             r.stderr.on('data', function (data) {
//                 data = iconv.decode(data, codeCmd)
//                 // console.log('stderr', data)
//                 cbStderr(data)
//             })
//         }

//     })
// }


export default execProcess