From 8e15a021c50c4ec5c5601cd0d846e8efc20987a9 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Sun, 25 Oct 2026 14:52:41 +0000 Subject: [PATCH] . --- .../Man_In_Grey_file_gate.CLI.c | 0 .../authored/Man_In_Grey.copy/FileLanguage.py | 2 +- .../authored/Man_In_Grey.copy/Journal.py | 2 +- .../authored/Man_In_Grey.copy/Man_In_Grey.py | 115 +++++++++++------- .../Man_In_Grey.copy/journal_interpreter.py | 2 +- document/design_Man_In_Grey.html | 2 +- .../{stage => composer}/DNS/unbound.conf.py | 0 .../{stage => composer}/obsolete.py | 0 .../{stage => composer}/unbound_conf.py | 0 .../{stage => composer}/web/site_conf.py | 0 .../wrong_mode.py | 0 .../{stage_create => composer_create}/new.py | 0 .../allowed_and_not.py | 0 tester/authored/write_file/run | 26 ++-- 14 files changed, 86 insertions(+), 63 deletions(-) rename developer/{authored => }/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c (100%) rename tester/authored/write_file/{stage => composer}/DNS/unbound.conf.py (100%) rename tester/authored/write_file/{stage => composer}/obsolete.py (100%) rename tester/authored/write_file/{stage => composer}/unbound_conf.py (100%) rename tester/authored/write_file/{stage => composer}/web/site_conf.py (100%) rename tester/authored/write_file/{stage_argument => composer_argument}/wrong_mode.py (100%) rename tester/authored/write_file/{stage_create => composer_create}/new.py (100%) rename tester/authored/write_file/{stage_refused => composer_refused}/allowed_and_not.py (100%) diff --git a/developer/authored/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c b/developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c similarity index 100% rename from developer/authored/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c rename to developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c diff --git a/developer/authored/Man_In_Grey.copy/FileLanguage.py b/developer/authored/Man_In_Grey.copy/FileLanguage.py index 42a71ae..e937760 100644 --- a/developer/authored/Man_In_Grey.copy/FileLanguage.py +++ b/developer/authored/Man_In_Grey.copy/FileLanguage.py @@ -1,6 +1,6 @@ # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -FileLanguage - a small command language for writing files, Man-In-Grey's +FileLanguage - a small command language for writing files, Man_In_Grey's test language. It is a constrained form of the POSIX file interface. open(directory ,name ,mode ,owner ,permission) produces a symbol for the file diff --git a/developer/authored/Man_In_Grey.copy/Journal.py b/developer/authored/Man_In_Grey.copy/Journal.py index 5d310d7..c5c45d9 100644 --- a/developer/authored/Man_In_Grey.copy/Journal.py +++ b/developer/authored/Man_In_Grey.copy/Journal.py @@ -1,6 +1,6 @@ # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -Journal - the journal of Man-In-Grey, independent of any command language. +Journal - the journal of Man_In_Grey, independent of any command language. A composer places commands into a journal. A command is data: the name of the command, its arguments, and the composer that placed it. A command whose diff --git a/developer/authored/Man_In_Grey.copy/Man_In_Grey.py b/developer/authored/Man_In_Grey.copy/Man_In_Grey.py index b5063b9..2764b9d 100755 --- a/developer/authored/Man_In_Grey.copy/Man_In_Grey.py +++ b/developer/authored/Man_In_Grey.copy/Man_In_Grey.py @@ -3,26 +3,30 @@ """ Man_In_Grey -Runs the composers on a stage, which place commands into one journal, and -hands the journal to a gate, which starts a bespoke journal interpreter. The -journal interpreter checks every command against its constraints, and only -then executes the journal. - -A composer is a Python file on the stage defining compose(prov ,journal_iface). -prov describes where the composer is; journal_iface places commands of the -file command language: open, write and unlink. - -Usage: Man_In_Grey --stage [option ...] - help | -h | --help print this message (also printed when given no arguments) - --stage the stage of composers - --input_acceptance a Python file defining accept(prov), true for each stage - file that is a composer; by default every file ending in - .py whose name does not begin with a dot +Runs composers, which place commands into one journal, and hands the journal +to a gate, which starts a bespoke journal interpreter. The journal interpreter +checks every command against its constraints, and only then executes the +journal. + +A composer is a Python file defining compose(prov ,journal_iface). prov +describes where the composer is; journal_iface places commands of the file +command language: open, write and unlink. + +Usage: Man_In_Grey [option ...] + help | -h | --help print this message (also printed when given no arguments) + compose run the one composer in + orchestrate run every composer in , breadth first, then + by name; the layout of the directory carries no meaning + +Options: + --input_acceptance orchestrate only: a Python file defining accept(prov), + true for each file that is a composer; by default every + file ending in .py whose name does not begin with a dot --gate the gate to hand the journal to; by default ../Man_In_Grey_file_gate/Man_In_Grey_file_gate --constraints constraints for an unprivileged run, given to the journal interpreter - --phase-0-then-stop stop after checking the stage and the filter + --phase-0-then-stop stop after checking the path and the filter --phase-1-print print the journal after composing --phase-1-then-stop stop after composing; nothing reaches the gate --phase-2-print print the journal as the journal interpreter decoded it @@ -59,7 +63,8 @@ STOP_OPTION = { ,"--phase-2-filesystem-then-stop": "filesystem" } FLAG = {"--phase-0-then-stop" ,"--phase-1-print" ,"--phase-1-then-stop" ,"--phase-2-print"} | set(STOP_OPTION) -VALUE = {"--stage" ,"--input_acceptance" ,"--gate" ,"--constraints"} +VALUE = {"--input_acceptance" ,"--gate" ,"--constraints"} +COMMAND = ("compose" ,"orchestrate") # -------------------------------------------------------------------------------- @@ -70,7 +75,7 @@ class Provenance: Where a composer is. Given to the acceptance filter and to compose(). """ __slots__ = ( - "stage_root_dpath" + "composer_root_dpath" ,"composer_abs_fpath" ,"composer_rel_fpath" ,"composer_dir_dpath" @@ -79,15 +84,14 @@ class Provenance: ,"cwd_dpath" ) - def __init__(self ,stage_root ,composer_path): - self.stage_root_dpath = stage_root.resolve() + def __init__(self ,composer_root ,composer_path): + self.composer_root_dpath = composer_root.resolve() self.composer_abs_fpath = composer_path.resolve() - self.composer_rel_fpath = self.composer_abs_fpath.relative_to(self.stage_root_dpath) + self.composer_rel_fpath = self.composer_abs_fpath.relative_to(self.composer_root_dpath) self.composer_dir_dpath = self.composer_abs_fpath.parent name = self.composer_abs_fpath.name - for suffix in (".stage.py" ,".py"): - if name.endswith(suffix): - name = name[:-len(suffix)]; break + if name.endswith(".py"): + name = name[:-3] self.implied_fname = name self.process_user = getpass.getuser() self.cwd_dpath = Path.cwd().resolve() @@ -109,22 +113,22 @@ def accept_function(filter_path): raise RuntimeError(f"{filter_path} defines no accept(prov)") return function -def composer_list(stage_root ,accept): - """The accepted stage files, breadth first, then by name. Symbolic links to directories are not followed.""" +def composer_list(composer_root ,accept): + """The accepted files of the composer directory, breadth first, then by name. Symbolic links to directories are not followed.""" found = [] - for dirpath ,dirnames ,filenames in os.walk(stage_root): + for dirpath ,dirnames ,filenames in os.walk(composer_root): dirnames[:] = sorted(d for d in dirnames if not os.path.islink(os.path.join(dirpath ,d))) for name in filenames: path = Path(dirpath ,name) if not stat.S_ISREG(path.stat().st_mode): continue - prov = Provenance(stage_root ,path) + prov = Provenance(composer_root ,path) if accept(prov): found.append((len(prov.composer_rel_fpath.parts) ,prov.composer_rel_fpath.as_posix() ,prov)) found.sort(key=lambda t: (t[0] ,t[1])) return [t[2] for t in found] -def compose_all(stage_root ,prov_list): +def compose_all(composer_root ,prov_list): """Run each composer into one journal. Raises RuntimeError naming the composer that failed.""" journal = Journal.Journal(FileLanguage.NAME) journal_iface = FileLanguage.JournalInterface(journal ,Path.cwd()) @@ -148,28 +152,41 @@ def compose_all(stage_root ,prov_list): "composed_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ") ,"process_user": getpass.getuser() ,"host": os.uname().nodename - ,"stage_root": str(stage_root.resolve()) + ,"composer_root": str(composer_root.resolve()) ,"composer_list": [p.composer_rel_fpath.as_posix() for p in prov_list] } return journal -def run(stage ,input_acceptance ,gate ,constraints ,flag ,stop_after): - stage_root = Path(stage) - if not stage_root.is_dir(): - print(f"error: the stage {stage_root} is not a directory" ,file=sys.stderr) - return 2 +def run(command ,path ,input_acceptance ,gate ,constraints ,flag ,stop_after): + path = Path(path) + if command == "compose": + if input_acceptance: + print("error: --input_acceptance is for orchestrate; compose runs the one file it is given" ,file=sys.stderr) + return 2 + if not path.is_file(): + print(f"error: the composer {path} is not a file" ,file=sys.stderr) + return 2 + composer_root = path.resolve().parent + else: + if not path.is_dir(): + print(f"error: {path} is not a directory" ,file=sys.stderr) + return 2 + composer_root = path try: accept = accept_function(Path(input_acceptance) if input_acceptance else None) except Exception as error: print(f"error: acceptance filter: {error}" ,file=sys.stderr) return 2 if "--phase-0-then-stop" in flag: - print(f"phase 0: OK, stage {stage_root.resolve()}") + print(f"phase 0: OK, {command} {path.resolve()}") return 0 try: - prov_list = composer_list(stage_root ,accept) - journal = compose_all(stage_root ,prov_list) + if command == "compose": + prov_list = [Provenance(composer_root ,path)] + else: + prov_list = composer_list(composer_root ,accept) + journal = compose_all(composer_root ,prov_list) except Exception as error: print(f"error: {error}" ,file=sys.stderr) return 2 @@ -202,26 +219,32 @@ def print_help(): def CLI(argv=None): argv = sys.argv[1:] if argv is None else argv if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help() + command ,rest = argv[0] ,argv[1:] + if command not in COMMAND: + print(f"Error: unknown command '{command}'." ,file=sys.stderr) + print_help() + return 2 + if not rest or rest[0].startswith("--"): + print(f"Error: {command} is given a {'file' if command == 'compose' else 'directory'} first." ,file=sys.stderr) + return 2 + path ,rest = rest[0] ,rest[1:] value ,flag = {} ,set() i = 0 - while i < len(argv): - word = argv[i] - if word in VALUE and i + 1 < len(argv): - value[word] = argv[i + 1]; i += 2 + while i < len(rest): + word = rest[i] + if word in VALUE and i + 1 < len(rest): + value[word] = rest[i + 1]; i += 2 elif word in FLAG: flag.add(word); i += 1 else: print(f"Error: unknown or incomplete option '{word}'." ,file=sys.stderr) return 2 - if "--stage" not in value: - print("Error: --stage is required." ,file=sys.stderr) - return 2 stop = [STOP_OPTION[f] for f in flag if f in STOP_OPTION] if len(stop) > 1: print("Error: give at most one --phase-2-...-then-stop option." ,file=sys.stderr) return 2 return run( - value["--stage"] ,value.get("--input_acceptance") ,value.get("--gate") ,value.get("--constraints") + command ,path ,value.get("--input_acceptance") ,value.get("--gate") ,value.get("--constraints") ,flag ,stop[0] if stop else None ) diff --git a/developer/authored/Man_In_Grey.copy/journal_interpreter.py b/developer/authored/Man_In_Grey.copy/journal_interpreter.py index f01cadb..cdca012 100644 --- a/developer/authored/Man_In_Grey.copy/journal_interpreter.py +++ b/developer/authored/Man_In_Grey.copy/journal_interpreter.py @@ -1,6 +1,6 @@ # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -journal_interpreter - the general journal interpreter of Man-In-Grey. +journal_interpreter - the general journal interpreter of Man_In_Grey. A bespoke journal interpreter is a gate, a guard and this general interpreter. The bespoke interpreter chooses the command language and the diff --git a/document/design_Man_In_Grey.html b/document/design_Man_In_Grey.html index 4cdf48c..e787fb4 100644 --- a/document/design_Man_In_Grey.html +++ b/document/design_Man_In_Grey.html @@ -95,7 +95,7 @@ Out-of-band signalling

- A template mixes two kinds of thing in one stream: the content to be written, and marks that say where to substitute. That is in-band signalling. Some content will always look like a mark, so the template language needs an escape, and the escape is itself a mark. There is no tight way to design the aliasing out. Each level of nesting multiplies the escapes: an f-string that explains how to write f-strings escapes its braces, and a document about that f-string escapes them again. + The general method of filling in templates mixes data and control together, i.e. the content to be written, and marks that say where to substitute. Thus this method is a form of in-band signalling. With in-band signaling there will potentially that a content string will be identical to a control string, so data and control can alias. There is no tight way to design the aliasing out. The typical workaround is to disallow control strings in the data and to replace missing characters with 'escape sequences', where the escape then is control. Each level of nesting multiplies the escapes: an f-string that explains how to write f-strings escapes its braces, and a document about that f-string escapes them again.

Man_In_Grey signals out of band. A composer is a program, so the logic that forms the output is code, with loops, conditions and lookups, rather than marks embedded in the output. A command in the journal is a structure, and file content is carried in it as a byte string with its length. The journal interpreter never scans content for meaning, so content can hold anything, including text that looks like a journal, with no escaping. This is the same principle as an extent string, where the extent of the content travels beside it rather than being marked within it. diff --git a/tester/authored/write_file/stage/DNS/unbound.conf.py b/tester/authored/write_file/composer/DNS/unbound.conf.py similarity index 100% rename from tester/authored/write_file/stage/DNS/unbound.conf.py rename to tester/authored/write_file/composer/DNS/unbound.conf.py diff --git a/tester/authored/write_file/stage/obsolete.py b/tester/authored/write_file/composer/obsolete.py similarity index 100% rename from tester/authored/write_file/stage/obsolete.py rename to tester/authored/write_file/composer/obsolete.py diff --git a/tester/authored/write_file/stage/unbound_conf.py b/tester/authored/write_file/composer/unbound_conf.py similarity index 100% rename from tester/authored/write_file/stage/unbound_conf.py rename to tester/authored/write_file/composer/unbound_conf.py diff --git a/tester/authored/write_file/stage/web/site_conf.py b/tester/authored/write_file/composer/web/site_conf.py similarity index 100% rename from tester/authored/write_file/stage/web/site_conf.py rename to tester/authored/write_file/composer/web/site_conf.py diff --git a/tester/authored/write_file/stage_argument/wrong_mode.py b/tester/authored/write_file/composer_argument/wrong_mode.py similarity index 100% rename from tester/authored/write_file/stage_argument/wrong_mode.py rename to tester/authored/write_file/composer_argument/wrong_mode.py diff --git a/tester/authored/write_file/stage_create/new.py b/tester/authored/write_file/composer_create/new.py similarity index 100% rename from tester/authored/write_file/stage_create/new.py rename to tester/authored/write_file/composer_create/new.py diff --git a/tester/authored/write_file/stage_refused/allowed_and_not.py b/tester/authored/write_file/composer_refused/allowed_and_not.py similarity index 100% rename from tester/authored/write_file/stage_refused/allowed_and_not.py rename to tester/authored/write_file/composer_refused/allowed_and_not.py diff --git a/tester/authored/write_file/run b/tester/authored/write_file/run index 5613e6f..3aee789 100755 --- a/tester/authored/write_file/run +++ b/tester/authored/write_file/run @@ -3,21 +3,21 @@ """ run -The write_file test of Man-In-Grey's file command language. It runs the +The write_file test of Man_In_Grey's file command language. It runs the promoted Man_In_Grey, from $REPO_HOME/consumer, through an unblessed gate, with constraints it writes for the files it expects, and checks: - run_1 the stage beside this file: each composer's file written, and + run_1 orchestrate composer/: each composer's file written, and out/obsolete.conf, made by the test beforehand, unlinked; the files are compared with expected/run_1 - run_2 the same stage again: replace_keep files kept as backups named + run_2 orchestrate composer/ again: replace_keep files kept as backups named .YYYYMMDDTHHMMSSZ, the replace file overwritten with no backup; compared with expected/run_2 - create stage_create: a file opened in create mode is written once, and + create compose composer_create/new.py: a file opened in create mode is written once, and a second run is refused by the filesystem check - refused stage_refused: a journal opening one allowed file and one the + refused orchestrate composer_refused/: a journal opening one allowed file and one the constraints do not allow is refused whole by the constraint check - argument stage_argument: a composer giving a wrong mode fails when it + argument orchestrate composer_argument/: a composer giving a wrong mode fails when it places the command, and no journal reaches the gate A refused run is to leave every file as it was. Backup names carry the time @@ -143,8 +143,8 @@ def compare(log ,run_name ,work_dpath): problem.append(f"owner uid {path.lstat().st_uid}, expected {os.getuid()}") log.check(not problem ,what ,"; ".join(problem) or None) -def run_Man_In_Grey(program ,stage ,work_dpath ,constraint_fpath): - command = [str(program) ,"--stage" ,str(TEST_DPATH / stage) ,"--constraints" ,str(constraint_fpath)] +def run_Man_In_Grey(program ,command ,path ,work_dpath ,constraint_fpath): + command = [str(program) ,command ,str(TEST_DPATH / path) ,"--constraints" ,str(constraint_fpath)] result = subprocess.run(command ,cwd=work_dpath ,capture_output=True ,text=True) return result.returncode ,(result.stdout + result.stderr).strip() @@ -196,7 +196,7 @@ def test(repo_home): # run_1 and run_2 for run_name in ("run_1" ,"run_2"): - status ,output = run_Man_In_Grey(program ,"stage" ,work_dpath ,constraint_fpath) + status ,output = run_Man_In_Grey(program ,"orchestrate" ,"composer" ,work_dpath ,constraint_fpath) if check_status(log ,f"{run_name} Man_In_Grey exits 0" ,status ,0 ,output): compare(log ,run_name ,work_dpath) else: @@ -204,25 +204,25 @@ def test(repo_home): return log.summary() # create: once, then refused - status ,output = run_Man_In_Grey(program ,"stage_create" ,work_dpath ,constraint_fpath) + status ,output = run_Man_In_Grey(program ,"compose" ,"composer_create/new.py" ,work_dpath ,constraint_fpath) check_status(log ,"create Man_In_Grey exits 0" ,status ,0 ,output) new = out / "new.conf" log.check(new.is_file() and new.read_text() == "made once\n" and stat.S_IMODE(new.stat().st_mode) == 0o600 ,"create out/new.conf" ,None if new.is_file() else "not written") before = snapshot(out) - status ,output = run_Man_In_Grey(program ,"stage_create" ,work_dpath ,constraint_fpath) + status ,output = run_Man_In_Grey(program ,"compose" ,"composer_create/new.py" ,work_dpath ,constraint_fpath) check_status(log ,"create again is refused by the filesystem check" ,status ,1 ,output ,"filesystem: refused") log.check(snapshot(out) == before ,"create again changes nothing") # a journal the constraints refuse before = snapshot(out) - status ,output = run_Man_In_Grey(program ,"stage_refused" ,work_dpath ,constraint_fpath) + status ,output = run_Man_In_Grey(program ,"orchestrate" ,"composer_refused" ,work_dpath ,constraint_fpath) check_status(log ,"refused is refused by the constraint check" ,status ,1 ,output ,"constraint: refused") log.check(snapshot(out) == before ,"refused changes nothing, the allowed file included") # a composer with a wrong argument before = snapshot(out) - status ,output = run_Man_In_Grey(program ,"stage_argument" ,work_dpath ,constraint_fpath) + status ,output = run_Man_In_Grey(program ,"orchestrate" ,"composer_argument" ,work_dpath ,constraint_fpath) check_status(log ,"argument fails in the composer" ,status ,2 ,output ,"wrong_mode.py: open: mode 'overwrite'") log.check(snapshot(out) == before ,"argument changes nothing") -- 2.20.1