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.
413 lines
16 KiB
413 lines
16 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 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 platform.system() == "Windows":
|
|
HOME = os.environ['USERPROFILE']
|
|
else:
|
|
HOME = os.environ['HOME']
|
|
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"
|
|
% repr(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):]
|
|
# pre_bin = "export LD_LIBRARY_PATH=/usr/lib64 " # doesn't fix #616
|
|
# Somehow #616 gone, even shell, force_shell, force_env False & no pre_bin
|
|
# Maybe restart computer or run `sudo ldconfig` but I did neither
|
|
# (unless installing a package triggered it--I installed lsb before the
|
|
# problem went away I think).
|
|
pre_bin = ""
|
|
force_shell = False
|
|
# ^ Either True/False succeeds in bash, either way LD_LIBRARY_PATH
|
|
# fails in vscode:
|
|
# 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):
|
|
# pre_bin = "LD_LIBRARY_PATH=/usr/lib64; export LD_LIBRARY_PATH; " # doesn't fix #616
|
|
# ^ adding "export " causes bad identifier error later in command
|
|
cmd = ["bash", "-c", "cd '" + cwd + "'; " + pre_bin + shlex.join(cmd)]
|
|
else:
|
|
cmd = ["bash", "-c", "cd '" + cwd + "'; " + pre_bin + cmd]
|
|
run_msg = prefix+"Running %s" % repr(cmd)
|
|
run_msg += ' # shell=%s in "%s"' % (shell, cwd)
|
|
echo0(run_msg)
|
|
if cwd is not None:
|
|
os.chdir(cwd)
|
|
out = {
|
|
'bytes': None, # current line bytes
|
|
'string': "", # current line string
|
|
# ^ (same as bytes if Python 2 running)
|
|
'buffer': "", # cumulative buffer
|
|
}
|
|
good_path = (
|
|
"{HOME}/.rvm/gems/ruby-3.0.0/bin:"
|
|
"{HOME}/.rvm/gems/ruby-3.0.0@global/bin:"
|
|
"{HOME}/.rvm/rubies/ruby-3.0.0/bin:"
|
|
"{HOME}/.nvm/versions/node/v14.21.2/bin:"
|
|
"/usr/lib64/ccache:{HOME}/.cargo/bin:"
|
|
"/usr/local/bin:/usr/bin:/bin:"
|
|
"{HOME}/.local/bin:/usr/local/sbin:/usr/sbin:"
|
|
"{HOME}/.local/bin:"
|
|
"{HOME}/git/linux-preinstall/utilities:"
|
|
"{HOME}/git/linux-preinstall/utilities-developer:"
|
|
"{HOME}/git/linux-preinstall/utilities-server:"
|
|
"{HOME}/.rvm/bin:{HOME}/.local/bin:{HOME}/bin:"
|
|
"{HOME}/git/linux-preinstall/utilities:"
|
|
"{HOME}/git/linux-preinstall/utilities-server:"
|
|
"{HOME}/git/linux-preinstall/utilities-developer:"
|
|
"{HOME}/.local/bin:{HOME}/git/linux-preinstall/utilities:"
|
|
"{HOME}/git/linux-preinstall/utilities-developer:"
|
|
"{HOME}/git/linux-preinstall/utilities-server:"
|
|
"{HOME}/.rvm/bin"
|
|
"".format(HOME=HOME)
|
|
)
|
|
# ^ known good path in working bash that can run run-any
|
|
# os.environ['PATH'] = good_path # still doesn't fix issue #616
|
|
this_env = os.environ.copy()
|
|
force_env = False
|
|
if force_env and platform.system() == "Linux":
|
|
# For diagnosing issue #616
|
|
LD_LIBRARY_PATH = "/usr/lib64"
|
|
if "LD_LIBRARY_PATH" not in this_env:
|
|
print(prefix+"adding LD_LIBRARY_PATH=%s" % LD_LIBRARY_PATH)
|
|
this_env["LD_LIBRARY_PATH"] = LD_LIBRARY_PATH
|
|
elif LD_LIBRARY_PATH not in this_env['LD_LIBRARY_PATH']:
|
|
print(prefix+"LD_LIBRARY_PATH=%s"
|
|
% (os.environ['LD_LIBRARY_PATH']))
|
|
print(prefix+"appending %s to LD_LIBRARY_PATH" % LD_LIBRARY_PATH)
|
|
if not this_env["LD_LIBRARY_PATH"].endswith(os.pathsep):
|
|
this_env["LD_LIBRARY_PATH"] += os.pathsep
|
|
os.environ["LD_LIBRARY_PATH"] += os.pathsep
|
|
this_env["LD_LIBRARY_PATH"] += LD_LIBRARY_PATH
|
|
os.environ["LD_LIBRARY_PATH"] += LD_LIBRARY_PATH
|
|
print(prefix+"LD_LIBRARY_PATH=%s"
|
|
% (os.environ['LD_LIBRARY_PATH']))
|
|
else:
|
|
print(prefix+"detected %s in LD_LIBRARY_PATH=%s"
|
|
% (LD_LIBRARY_PATH, os.environ['LD_LIBRARY_PATH']))
|
|
enable_call = False
|
|
if enable_call: # FIXME: True for debug only--issue #616 (doesn't fix)
|
|
out['lines'] = []
|
|
# source, stream, and bytes members are discarded before return
|
|
# in non-dummy case (not enable_call) & not necessary for caller.
|
|
err = copy.deepcopy(out)
|
|
stream_metas = OrderedDict({
|
|
'out': out,
|
|
'err': err,
|
|
})
|
|
print(prefix+"calling (shell=%s): %s" % (shell, cmd),
|
|
file=sys.stderr)
|
|
code = subprocess.run(cmd, shell=shell, env=this_env)
|
|
return { # FIXME: for debug issue #616 only
|
|
'code': code,
|
|
'streams': stream_metas,
|
|
}
|
|
if shell:
|
|
cmd = pre_bin + cmd
|
|
print(prefix+"opening (shell=%s): %s" % (shell, cmd),
|
|
file=sys.stderr)
|
|
proc = subprocess.Popen(cmd, shell=shell, stderr=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, cwd=cwd,
|
|
env=this_env)
|
|
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"
|
|
% (_, repr(meta['string']))
|
|
)
|
|
return {
|
|
'code': code,
|
|
'streams': stream_metas,
|
|
}
|
|
|
|
|
|
issue_616_flag = "cannot open shared object file: No such file or directory"
|
|
|
|
|
|
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" (cwd="%s")--use absolute path if not in cwd'
|
|
% (path, os.getcwd()))
|
|
# 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 targetBaseDir == "":
|
|
targetBaseDir = os.getcwd()
|
|
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:
|
|
sys.stdout.write("\nOutputInspector:")
|
|
sys.stdout.flush()
|
|
count = 0
|
|
for line in all_lines:
|
|
inspector.addLine(line, enablePush)
|
|
count += 1
|
|
# NOTE: addLine adds all of the metadata!
|
|
if count < 1:
|
|
cmd_msg = cmd if isinstance(cmd, str) else shlex.join(cmd)
|
|
print(" (Output of `%s` had %s line(s) of output.)"
|
|
% (repr(cmd_msg), count),
|
|
file=sys.stderr)
|
|
else:
|
|
print("", file=sys.stderr)
|
|
|
|
for info in inspector.getLineInfos():
|
|
print(info['all'], file=sys.stderr)
|
|
if issue_616_flag in info['all']:
|
|
print("- detected issue #616.")
|
|
print(" See <https://"
|
|
"github.com/Poikilos/EnlivenMinetest/issues/616>",
|
|
file=sys.stderr)
|
|
pass
|
|
|
|
# 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.
|
|
|