How to use nodejs spawn to output unity logs in real time

created at 05-31-2022 views: 4

problem

Originally, the CI build script of the Unity project was written in python. I always felt that it was very inconvenient for python type checking and inference. Later, I used nodejs to rewrite a build program. However, due to time constraints, it has not been able to handle Unity logs well. Real-time output problem. I'll fix it today if I have time.

As we all know, Unity does not output logs to the standard output stream (stdout) by default, but to log files.

The official solution has been given, you can specify - to send unity output to stdout. There are solutions about the python version on the Internet, here is how to implement it in nodejs.

output the unity log to stdout

In unity2019.1 and its subsequent versions, you need to add the parameter -logfile - to the command line that calls unity.

Note: In earlier versions, some people said to use the parameter -logfile directly (that is, without specifying a specific log file), but I have not tested it.

nodejs get sub thread output

Here, spawn is used to implement asynchronous child threads. There are two ways. One is to specify stdio as inherit when creating a child thread, which is linked to the standard output stream of the main thread.

let proc = spawn('D:/where_unity_installed/unity.exe', 
    ['-batchmode', '-projectPath', 'D:/project_path', '-quit', '-logfile', '-'], { stdio: 'inherit' });

The second is to specify stdio as pipe, which is different from inherit output as it is, which can be used for secondary processing of the output.

let proc = spawn('D:/where_unity_installed/unity.exe', 
    ['-batchmode', '-projectPath', 'D:/project_path', '-quit', '-logfile', '-'], { stdio: 'pipe' });

proc.stdout!.on('data', (data) => {
    let dataStr = String(data);
    process.stdout.write(moment().format('HH:mm:ss:SSS ') + dataStr);
});

proc.stderr!.on('data', (data) => {
    let dataStr = String(data);
    process.stderr.write(moment().format('HH:mm:ss:SSS ') + dataStr);
});

complete example

Below is a complete example of a generic subthread class

import os from 'os';
import { spawn, SpawnOptions } from "child_process";
import moment from 'moment';

interface CmdOption extends SpawnOptions {
    silent?: boolean;
    logPrefix?: string;
}

export class Cmd {
    private text: string = '';

    runNodeModule(moduleName: string, params?: string[], options?: CmdOption): Promise<string> {
        if(os.type() == 'Windows_NT' && !moduleName.match(/\.cmd$/)) {
            moduleName += '.cmd';
        }
        return this.run(moduleName, params, options);
    }

    run(command: string, params?: string[], options?: CmdOption): Promise<string> {
        this.text = '';
        // options = Object.assign(options || {}, { cwd: this.cfg.cwd });
        return new Promise((resolve: (data: string) => void, reject: (error: Error) => void) => {
            console.log(`run command: ${command}, params:`, params, options);

            if(!options) {
                options = {};
            }
            if(!params) params = [];
            options.stdio = 'pipe';
            let proc = spawn(command, params, options);

            proc.stdout!.on('data', (data) => {
                let dataStr = String(data);
                if(options?.logPrefix) {
                    dataStr = options.logPrefix + dataStr;
                }
                this.text += dataStr;
                if(!options?.silent) process.stdout.write(moment().format('HH:mm:ss:SSS ') + dataStr);
            });

            proc.stderr!.on('data', (data) => {
                // It does not necessarily mean that the process exitcode != 0, it may just be that the process called console.error
                let dataStr = String(data);
                if(options?.logPrefix) {
                    dataStr = options.logPrefix + dataStr;
                }
                if(!options?.silent) process.stderr.write(moment().format('HH:mm:ss:SSS ') + dataStr);
            });

            // process closes
            proc.on('error', (error: Error) => {
                if(!options?.silent) console.error(error);
                reject(error);
            });

            // process closes 
            proc.on('close', (code: number) => {
                console.log(`process closed with exit code: ${code}`);
                if(code == 0) {
                    resolve(this.text || '');
                } else {
                    let errMsg = `process closed with exit code: ${code}`;
                    if(options?.logPrefix) {
                        errMsg = options.logPrefix + errMsg;
                    }
                    reject(new Error(errMsg));
                }
            });

            proc.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
                console.log(`process exits`);
            });
        });
    }
}
created at:05-31-2022
edited at: 05-31-2022: