diff --git a/projects/minetest-rsync.code-workspace b/projects/minetest-rsync.code-workspace new file mode 100644 index 0000000..c0f5815 --- /dev/null +++ b/projects/minetest-rsync.code-workspace @@ -0,0 +1,20 @@ +{ + "folders": [ + { + "path": "../../../minetest-rsync" + } + ], + "settings": {}, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "type":"python", + "request": "launch", + "name": "Launch minetest-rsync", + "program": "../git/EnlivenMinetest/utilities/run-any", + "args": ["./bin/minetest"], + } + ] + } +} diff --git a/utilities/run-any b/utilities/run-any new file mode 100755 index 0000000..de664c0 --- /dev/null +++ b/utilities/run-any @@ -0,0 +1,195 @@ +#!/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 +""" +from __future__ import print_function +import os +import sys +import subprocess +import copy +import shlex +from collections import OrderedDict + + +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. + """ + # (See + # ) + 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 (somehow program + # runs but doesn't get the blank argument in + # [path, "--logfile, ""] because debug.txt still gets created) + # so convert to string: + cmd = shlex.join(cmd) + 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 + out = { + 'bytes': None, # current line bytes + 'string': "", # current line string (same as bytes if Python 2 running) + 'buffer': "", # cumulative buffer + } + if enable_collect: + out['lines'] = [] # already-shown lines + err = copy.deepcopy(out) + 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']) + else: + echo0(meta['buffer']) + 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: + 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'] + return { + 'code': code, + 'streams': stream_metas, + } + + +def main(): + 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] + cwd = None + if os.path.isfile(path): + cwd = os.path.dirname(path) + if os.path.basename(cwd) == "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: + # cwd = os.dirname(hierosoft.which(path)) # TODO: uncomment this case? + basename = os.path.basename(path) + project_name = basename.replace("server", "") + cmd = path + if project_name in ["minetest", "finetest", "trolltest", "multicraft"]: + cmd = [path, "--logfile", ""] + # Do not write debug.txt to cwd, since Python code will read + # stdout and stderr. + echo0("Running %s" % shlex.join(cmd)) + else: + echo0("Running %s" % path) + enable_collect = False + results = show_and_return( + cmd, + enable_collect=enable_collect, + cwd=cwd + ) + if enable_collect: + echo0("\nLINES:") + for line in results['streams']['out']['lines']: + print(line) + for line in results['streams']['err']['lines']: + echo0(line) + # else 'lines' lists will not be in the streams! + return results['code'] + + +if __name__ == "__main__": + sys.exit(main())