You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							326 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							326 lines
						
					
					
						
							12 KiB
						
					
					
				
								#!/usr/bin/env python
							 | 
						|
								"""
							 | 
						|
								Run any program and show the output. The purpose is to allow VSCode's
							 | 
						|
								python runner to run a minetest binary (such as if you are using VSCode
							 | 
						|
								to edit Lua but want to run Minetest to test the Lua).
							 | 
						|
								
							 | 
						|
								Usage:
							 | 
						|
								run-any <minetest_executable_path>
							 | 
						|
								"""
							 | 
						|
								from __future__ import print_function
							 | 
						|
								import os
							 | 
						|
								import sys
							 | 
						|
								import subprocess
							 | 
						|
								import copy
							 | 
						|
								import shlex
							 | 
						|
								import platform
							 | 
						|
								from pprint import pformat
							 | 
						|
								from collections import OrderedDict
							 | 
						|
								
							 | 
						|
								SCRIPTS_DIR = os.path.dirname(os.path.realpath(__file__))
							 | 
						|
								REPO_DIR = os.path.dirname(SCRIPTS_DIR)
							 | 
						|
								REPOS_DIR = os.path.dirname(REPO_DIR)
							 | 
						|
								for try_dirname in ["outputinspector-python", "outputinspector"]:
							 | 
						|
								    try_path = os.path.join(REPOS_DIR, try_dirname)
							 | 
						|
								    if os.path.isfile(os.path.join(try_path, "outputinspector",
							 | 
						|
								                                    "__init__.py")):
							 | 
						|
								        # ^ Yes, it is in outputinspector/outputinspector/
							 | 
						|
								        #   If in same git dir as REPO_DIR rather than installed as a module
							 | 
						|
								        sys.path.insert(0, try_path)
							 | 
						|
								        break
							 | 
						|
								
							 | 
						|
								if sys.version_info.major < 3:
							 | 
						|
								    ModuleNotFoundError = ImportError
							 | 
						|
								    FileNotFoundError = IOError
							 | 
						|
								    input = raw_input
							 | 
						|
								
							 | 
						|
								inspector = None
							 | 
						|
								try:
							 | 
						|
								    import outputinspector
							 | 
						|
								    print("OutputInspector...FOUND.", file=sys.stderr)
							 | 
						|
								    from outputinspector import (
							 | 
						|
								        OutputInspector,
							 | 
						|
								        ROLE_COLLECTED_FILE,
							 | 
						|
								        ROLE_ROW,
							 | 
						|
								        ROLE_COL,
							 | 
						|
								        ROLE_LOWER,
							 | 
						|
								        ROLE_COLLECTED_LINE,
							 | 
						|
								        ROLE_DETAILS,
							 | 
						|
								    )
							 | 
						|
								    inspector = OutputInspector()
							 | 
						|
								except ModuleNotFoundError as ex:
							 | 
						|
								    print(ex, file=sys.stderr)
							 | 
						|
								    print("INFO: Install outputinspector or clone to %s"
							 | 
						|
								          " for clickable Lua traceback lines"
							 | 
						|
								          % REPOS_DIR, file=sys.stderr)
							 | 
						|
								
							 | 
						|
								def echo0(*args):
							 | 
						|
								    print(*args, file=sys.stderr)
							 | 
						|
								    return True
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def usage():
							 | 
						|
								    echo0(__doc__)
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def show_and_return(cmd, enable_collect=False, cwd=None, shell=False):
							 | 
						|
								    """Show the output of a process while it is running & get return code.
							 | 
						|
								
							 | 
						|
								    Warning: Minetest doesn't start flushing data until the server
							 | 
						|
								    starts. Output from mainmenu is flushed around when server starts
							 | 
						|
								    (or when user exits main menu, whichever comes first). This behavior
							 | 
						|
								    is the same whether or not shell is True.
							 | 
						|
								
							 | 
						|
								    If running minetest, set cmd to [minetest_path, "--logfile", ""]
							 | 
						|
								    and set cwd to minetest directory (not "bin"!).
							 | 
						|
								
							 | 
						|
								    Args:
							 | 
						|
								        cmd (Union[list[string], string]): The command to run, either as
							 | 
						|
								            list of args where first is command and no quotes are
							 | 
						|
								            necessary, or space-separated string formatted the same way
							 | 
						|
								            except with quotes where necessary (if an arg
							 | 
						|
								            contains spaces).
							 | 
						|
								            - If is *not* str and shell is True, cmd is converted to
							 | 
						|
								            str.
							 | 
						|
								        enable_collect (Optional[boolean]): Enable collecting lines
							 | 
						|
								            in the 'lines' key of the return.
							 | 
						|
								        cwd (Optional[string]): Set a new current working dir.
							 | 
						|
								        shell (boolean): If True, run a shell (processes globs and
							 | 
						|
								            shell options that occur within cmd).
							 | 
						|
								
							 | 
						|
								    Returns:
							 | 
						|
								        dict: 'code' is return code of the command.
							 | 
						|
								    """
							 | 
						|
								    prefix = "[show_and_return] "
							 | 
						|
								    # (See
							 | 
						|
								    # <https://cyberciti.biz/faq/python-run-external-command-and-get-output/>)
							 | 
						|
								    if shell not in [None, True, False]:
							 | 
						|
								        raise ValueError("Expected None, True, or False for shell but got %s"
							 | 
						|
								                         % pformat(shell))
							 | 
						|
								    if cwd is None:
							 | 
						|
								        cwd = os.getcwd()
							 | 
						|
								    if shell and not hasattr(cmd, "split"):
							 | 
						|
								        # shell cannot correctly utilize a list/tuple (only first
							 | 
						|
								        #   element is used!) so join as string to use all arguments:
							 | 
						|
								        cmd = shlex.join(cmd)
							 | 
						|
								    remove_bin = False  # FIXME: True for debug only
							 | 
						|
								    # ^ Either True/False succeeds in bash, either fails in vscode
							 | 
						|
								    #   with https://github.com/Poikilos/EnlivenMinetest/issues/616
							 | 
						|
								    if remove_bin:
							 | 
						|
								        cwd = os.path.join(cwd, "bin")
							 | 
						|
								        bin_rel = "./bin/"
							 | 
						|
								        if isinstance(cmd, list):
							 | 
						|
								            if cmd[0].startswith(bin_rel):
							 | 
						|
								                cmd[0] = "./" + cmd[0][len(bin_rel):]
							 | 
						|
								        else:
							 | 
						|
								            if cmd.startswith(bin_rel):
							 | 
						|
								                cmd = "./" + cmd[len(bin_rel):]
							 | 
						|
								    force_shell = False
							 | 
						|
								    # ^ Either True/False succeeds in bash, either fails in vscode
							 | 
						|
								    #   with https://github.com/Poikilos/EnlivenMinetest/issues/616
							 | 
						|
								    if not shell and force_shell:
							 | 
						|
								        # force_shell fails with shell=True (just hangs
							 | 
						|
								        #   near `meta['bytes'] = err['source'].read(1)`,
							 | 
						|
								        #   even if run-any is run from bash prompt manually)
							 | 
						|
								        if platform.system() == "Linux":
							 | 
						|
								            if isinstance(cmd, list):
							 | 
						|
								                cmd = ["bash", "-c", "cd '" + cwd + "'; " + shlex.join(cmd)]
							 | 
						|
								            else:
							 | 
						|
								                cmd = ["bash", "-c", "cd '" + cwd + "'; " + cmd]
							 | 
						|
								    run_msg = "Running %s" % pformat(cmd)
							 | 
						|
								    run_msg += '  # shell=%s in "%s"' % (shell, cwd)
							 | 
						|
								    echo0(run_msg)
							 | 
						|
								    if cwd is not None:
							 | 
						|
								        os.chdir(cwd)
							 | 
						|
								    enable_call = False
							 | 
						|
								    out = {
							 | 
						|
								        'bytes': None,  # current line bytes
							 | 
						|
								        'string': "",  # current line string
							 | 
						|
								        # ^ (same as bytes if Python 2 running)
							 | 
						|
								        'buffer': "",  # cumulative buffer
							 | 
						|
								        'lines': [],
							 | 
						|
								    }
							 | 
						|
								    if enable_call:  # FIXME: True for debug only--issue #616 (doesn't fix)
							 | 
						|
								        err = copy.deepcopy(out)
							 | 
						|
								        out['source'] = []
							 | 
						|
								        stream_metas = OrderedDict({
							 | 
						|
								            'out': out,
							 | 
						|
								            'err': err,
							 | 
						|
								        })
							 | 
						|
								        code = subprocess.call(cmd)
							 | 
						|
								        return {  # FIXME: for debug issue #616 only
							 | 
						|
								            'code': code,
							 | 
						|
								            'streams': stream_metas,
							 | 
						|
								        }
							 | 
						|
								    proc = subprocess.Popen(cmd, shell=shell, stderr=subprocess.PIPE,
							 | 
						|
								                            stdout=subprocess.PIPE, cwd=cwd)
							 | 
						|
								    code = None
							 | 
						|
								    # Do not wait for finish--start displaying output immediately
							 | 
						|
								    if enable_collect:
							 | 
						|
								        out['lines'] = []  # already-shown lines
							 | 
						|
								    err = copy.deepcopy(out)  # sets err['lines'] if enable_collect
							 | 
						|
								    if enable_collect:
							 | 
						|
								        if id(err['lines']) == id(out['lines']):
							 | 
						|
								            raise RuntimeError(
							 | 
						|
								                "deepcopy filed. lines are same in both streams"
							 | 
						|
								            )
							 | 
						|
								    out['source'] = proc.stdout
							 | 
						|
								    err['source'] = proc.stderr
							 | 
						|
								    out['stream'] = sys.stdout
							 | 
						|
								    err['stream'] = sys.stderr
							 | 
						|
								    stream_metas = OrderedDict({
							 | 
						|
								        'out': out,
							 | 
						|
								        'err': err,
							 | 
						|
								    })
							 | 
						|
								
							 | 
						|
								    while True:
							 | 
						|
								        for name, meta in stream_metas.items():
							 | 
						|
								            # meta['bytes'] = err['source'].read(1)
							 | 
						|
								            # ^ scrambles letters (mixes stderr and stdout) somehow, so:
							 | 
						|
								            if name == "out":
							 | 
						|
								                meta['bytes'] = proc.stdout.read(1)
							 | 
						|
								            elif name == "err":
							 | 
						|
								                meta['bytes'] = proc.stderr.read(1)
							 | 
						|
								            meta['string'] = meta['bytes']
							 | 
						|
								            if (meta['string'] is not None) and (sys.version_info.major >= 3):
							 | 
						|
								                meta['string'] = meta['bytes'].decode("utf-8")
							 | 
						|
								            meta['buffer'] += meta['string']
							 | 
						|
								
							 | 
						|
								        code = proc.poll()  # None if command isn't finished
							 | 
						|
								        if out['string'] == '' and err['string'] == '' and code is not None:
							 | 
						|
								            # If no more to write *and* there is a return code (finished)
							 | 
						|
								            for name, meta in stream_metas.items():
							 | 
						|
								                if len(meta['buffer']) > 0:
							 | 
						|
								                    if name == "out":
							 | 
						|
								                        print(meta['buffer'].rstrip("\n\r"))
							 | 
						|
								                    else:
							 | 
						|
								                        echo0(meta['buffer'].rstrip("\n\r"))
							 | 
						|
								                    meta['lines'] += meta['buffer'].split("\n")
							 | 
						|
								            break
							 | 
						|
								
							 | 
						|
								        for _, meta in stream_metas.items():
							 | 
						|
								            # Write out['string'] to stdout then err['string'] to stderr
							 | 
						|
								            if meta['buffer'] == '':
							 | 
						|
								                continue
							 | 
						|
								            lastI = meta['buffer'].rfind("\n")
							 | 
						|
								            if lastI >= 0:
							 | 
						|
								                # Only show finished lines (avoid mixing stdout and
							 | 
						|
								                #   stderr on the same line).
							 | 
						|
								                if code is not None:
							 | 
						|
								                    # The program ended, so show everything.
							 | 
						|
								                    lastI = len(meta['buffer']) - 1
							 | 
						|
								                    # ^ -1 since inclusive (+1 below)
							 | 
						|
								
							 | 
						|
								                this_chunk = meta['buffer'][:lastI+1]
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								                if enable_collect:
							 | 
						|
								                    # Don't use append (since split always returns a list
							 | 
						|
								                    #   even if 1-long).
							 | 
						|
								                    meta['lines'] += this_chunk.split("\n")
							 | 
						|
								                meta['stream'].write(this_chunk)
							 | 
						|
								                meta['stream'].flush()
							 | 
						|
								                meta['buffer'] = meta['buffer'][lastI+1:]
							 | 
						|
								            # else:
							 | 
						|
								            #     echo0("Incomplete line:%s" % meta['buffer'])
							 | 
						|
								    for _, meta in stream_metas.items():
							 | 
						|
								        del meta['source']
							 | 
						|
								        del meta['stream']
							 | 
						|
								        del meta['bytes']
							 | 
						|
								        # del meta['string']
							 | 
						|
								        if len(meta['string']) > 0:
							 | 
						|
								            raise NotImplementedError(
							 | 
						|
								                "The remaining %s wasn't processed: %s"
							 | 
						|
								                % (_, pformat(meta['string']))
							 | 
						|
								            )
							 | 
						|
								    return {
							 | 
						|
								        'code': code,
							 | 
						|
								        'streams': stream_metas,
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def main():
							 | 
						|
								    enablePush = True  # collect the line in the inspector ui right away
							 | 
						|
								    if len(sys.argv) < 2:
							 | 
						|
								        usage()
							 | 
						|
								        echo0("Error: You didn't specify what program to run.")
							 | 
						|
								        return 1
							 | 
						|
								    # subprocess.call(sys.argv[1])
							 | 
						|
								    path = sys.argv[1]
							 | 
						|
								    if path.lower().endswith(".txt"):
							 | 
						|
								        raise ValueError("expected an executable!")
							 | 
						|
								    cwd = None
							 | 
						|
								    if os.path.isfile(path):
							 | 
						|
								        cwd = os.path.dirname(os.path.realpath(path))
							 | 
						|
								        dir_name = os.path.basename(cwd)
							 | 
						|
								        if dir_name == "bin":
							 | 
						|
								            # Minetest must run from minetest not from minetest/bin
							 | 
						|
								            # (especially when minetest is a script in minetest.org
							 | 
						|
								            # builds)
							 | 
						|
								            cwd = os.path.dirname(cwd)
							 | 
						|
								        else:
							 | 
						|
								            echo0("Warning: not Minetest-like dir_name=%s" % dir_name)
							 | 
						|
								    else:
							 | 
						|
								        raise ValueError('missing "%s"--use absolute path if not in cwd'
							 | 
						|
								                         % path)
							 | 
						|
								    # else:
							 | 
						|
								    #     cwd = os.dirname(hierosoft.which(path))  # TODO:uncomment this case?
							 | 
						|
								    basename = os.path.basename(path)
							 | 
						|
								    project_name = basename.replace("server", "")
							 | 
						|
								    cmd = path
							 | 
						|
								    exeDir = os.path.dirname(path)
							 | 
						|
								    if project_name in ["minetest", "finetest", "trolltest", "multicraft"]:
							 | 
						|
								        cmd = [path, "--logfile", ""]
							 | 
						|
								        targetBaseDir = os.path.dirname(exeDir)
							 | 
						|
								        # minetest/ not minetest/bin/ is the root of paths in tracebacks
							 | 
						|
								        # Do not write debug.txt to cwd, since Python code will read
							 | 
						|
								        #   stdout and stderr.
							 | 
						|
								    else:
							 | 
						|
								        targetBaseDir = exeDir
							 | 
						|
								    if inspector:
							 | 
						|
								        OutputInspector.addRoot(targetBaseDir)
							 | 
						|
								    enable_collect = True
							 | 
						|
								    results = show_and_return(
							 | 
						|
								        cmd,
							 | 
						|
								        enable_collect=enable_collect,
							 | 
						|
								        cwd=cwd,
							 | 
						|
								        # TODO: silent=not inspector,  # If there is no outputinspector, show in realtime
							 | 
						|
								    )
							 | 
						|
								    # ^ TODO: push to outputinspector in realtime
							 | 
						|
								    if enable_collect:
							 | 
						|
								        # echo0("\nLINES:")
							 | 
						|
								        # for line in results['streams']['out']['lines']:
							 | 
						|
								        #     print(line)
							 | 
						|
								        # for line in results['streams']['err']['lines']:
							 | 
						|
								        #     echo0(line)
							 | 
						|
								        out = results['streams']['out']
							 | 
						|
								        err = results['streams']['err']
							 | 
						|
								    # else 'lines' lists will not be in the streams!
							 | 
						|
								
							 | 
						|
								    if results['code'] == 0:
							 | 
						|
								        echo0("No errors (return code 0)")
							 | 
						|
								        # return results['code']
							 | 
						|
								
							 | 
						|
								    err_index = len(out['lines'])
							 | 
						|
								    all_lines = out['lines'] + err['lines']
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								    if inspector:
							 | 
						|
								        print("\nOutputInspector:")
							 | 
						|
								        for line in all_lines:
							 | 
						|
								            inspector.addLine(line, enablePush)
							 | 
						|
								        # NOTE: addLine adds all of the metadata!
							 | 
						|
								        for info in inspector.getLineInfos():
							 | 
						|
								            print(info['all'], file=sys.stderr)
							 | 
						|
								
							 | 
						|
								        # raise SyntaxError(
							 | 
						|
								        #     "{} line(s)".format(len(all_lines))+"\n".join(all_lines)
							 | 
						|
								        # )
							 | 
						|
								    # else output should already have been shown in realtime.
							 | 
						|
								    return results['code']
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								if __name__ == "__main__":
							 | 
						|
								    code = main()
							 | 
						|
								    #sys.exit(code)
							 | 
						|
								    sys.exit(0)  # Don't confuse VSCode. If nonzero, a popup will point here.
							 | 
						|
								
							 |