FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set +e
|
||||
umask 077
|
||||
ROOT="$FRAMEBRIDGE_FBTERM_ROOT"
|
||||
[ -n "$ROOT" ] || ROOT="$HOME/.local/share/framebridge/fbterm"
|
||||
READY="$ROOT/prompt.ready"
|
||||
TMP="$ROOT/.prompt.ready.$$"
|
||||
mkdir -p "$ROOT" 2>/dev/null || true
|
||||
printf '%s\n' "$(date +%s%N)" > "$TMP"
|
||||
mv -f "$TMP" "$READY"
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
umask 077
|
||||
|
||||
ROOT="${FBTERM_ROOT:?FBTERM_ROOT no definido}"
|
||||
|
||||
# R2: mientras fbterm-run esta activo, el pane no esta disponible.
|
||||
rm -f "$ROOT/prompt.ready"
|
||||
JOB_ID="${1:-}"
|
||||
|
||||
case "$JOB_ID" in
|
||||
''|*[!A-Za-z0-9._-]*)
|
||||
printf 'FBTERM_ERROR=INVALID_JOB_ID\n' >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
JOB="$ROOT/jobs/$JOB_ID"
|
||||
CMD="$JOB/command.bash"
|
||||
TIMEOUT_FILE="$JOB/timeout.seconds"
|
||||
|
||||
if [ ! -f "$CMD" ]; then
|
||||
printf 'FBTERM_ERROR=COMMAND_NOT_FOUND JOB_ID=%s\n' "$JOB_ID" >&2
|
||||
exit 66
|
||||
fi
|
||||
|
||||
TIMEOUT_S='120'
|
||||
|
||||
if [ -f "$TIMEOUT_FILE" ]; then
|
||||
VALUE="$(cat "$TIMEOUT_FILE" 2>/dev/null || true)"
|
||||
case "$VALUE" in
|
||||
''|*[!0-9]*)
|
||||
;;
|
||||
*)
|
||||
if [ "$VALUE" -ge 1 ] 2>/dev/null &&
|
||||
[ "$VALUE" -le 3600 ] 2>/dev/null
|
||||
then
|
||||
TIMEOUT_S="$VALUE"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
OUT_FIFO="$JOB/stdout.fifo"
|
||||
ERR_FIFO="$JOB/stderr.fifo"
|
||||
|
||||
STDOUT="$JOB/stdout.bin"
|
||||
STDERR="$JOB/stderr.bin"
|
||||
|
||||
rm -f "$OUT_FIFO" "$ERR_FIFO"
|
||||
|
||||
mkfifo "$OUT_FIFO" "$ERR_FIFO"
|
||||
|
||||
: > "$STDOUT"
|
||||
: > "$STDERR"
|
||||
|
||||
date -Ins > "$JOB/started.at"
|
||||
|
||||
sha256sum "$CMD" |
|
||||
awk '{print $1}' > "$JOB/command.sha256"
|
||||
|
||||
tee "$STDOUT" < "$OUT_FIFO" &
|
||||
OUT_TEE_PID=$!
|
||||
|
||||
tee "$STDERR" < "$ERR_FIFO" >&2 &
|
||||
ERR_TEE_PID=$!
|
||||
|
||||
/usr/bin/timeout \
|
||||
--signal=TERM \
|
||||
--kill-after=2s \
|
||||
"${TIMEOUT_S}s" \
|
||||
bash --noprofile --norc "$CMD" \
|
||||
> "$OUT_FIFO" \
|
||||
2> "$ERR_FIFO"
|
||||
|
||||
RC=$?
|
||||
|
||||
wait "$OUT_TEE_PID"
|
||||
OUT_TEE_RC=$?
|
||||
|
||||
wait "$ERR_TEE_PID"
|
||||
ERR_TEE_RC=$?
|
||||
|
||||
printf '%s\n' "$RC" > "$JOB/exit.code"
|
||||
printf '%s\n' "$OUT_TEE_RC" > "$JOB/stdout-tee.exit"
|
||||
printf '%s\n' "$ERR_TEE_RC" > "$JOB/stderr-tee.exit"
|
||||
|
||||
sha256sum "$STDOUT" |
|
||||
awk '{print $1}' > "$JOB/stdout.sha256"
|
||||
|
||||
sha256sum "$STDERR" |
|
||||
awk '{print $1}' > "$JOB/stderr.sha256"
|
||||
|
||||
stat -c '%s' "$STDOUT" > "$JOB/stdout.bytes"
|
||||
stat -c '%s' "$STDERR" > "$JOB/stderr.bytes"
|
||||
|
||||
date -Ins > "$JOB/finished.at"
|
||||
|
||||
{
|
||||
printf 'JOB_ID=%s\n' "$JOB_ID"
|
||||
printf 'EXIT=%s\n' "$RC"
|
||||
printf 'COMMAND_SHA256=%s\n' \
|
||||
"$(cat "$JOB/command.sha256")"
|
||||
printf 'STDOUT_SHA256=%s\n' \
|
||||
"$(cat "$JOB/stdout.sha256")"
|
||||
printf 'STDERR_SHA256=%s\n' \
|
||||
"$(cat "$JOB/stderr.sha256")"
|
||||
printf 'STDOUT_BYTES=%s\n' \
|
||||
"$(cat "$JOB/stdout.bytes")"
|
||||
printf 'STDERR_BYTES=%s\n' \
|
||||
"$(cat "$JOB/stderr.bytes")"
|
||||
printf 'STDOUT_TEE_EXIT=%s\n' "$OUT_TEE_RC"
|
||||
printf 'STDERR_TEE_EXIT=%s\n' "$ERR_TEE_RC"
|
||||
} > "$JOB/result.tmp"
|
||||
|
||||
mv "$JOB/result.tmp" "$JOB/result.env"
|
||||
touch "$JOB/DONE"
|
||||
|
||||
rm -f "$OUT_FIFO" "$ERR_FIFO"
|
||||
|
||||
printf 'FBTERM_JOB_COMPLETE JOB_ID=%s EXIT=%s\n' \
|
||||
"$JOB_ID" "$RC"
|
||||
|
||||
exit "$RC"
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
umask 077
|
||||
|
||||
export HISTFILE=/dev/null
|
||||
set +o history 2>/dev/null || true
|
||||
|
||||
printf '\n'
|
||||
printf '========================================\n'
|
||||
printf ' FrameBridge Terminal R1\n'
|
||||
printf ' PTY persistente / comandos controlados\n'
|
||||
printf '========================================\n'
|
||||
printf '\n'
|
||||
|
||||
export FRAMEBRIDGE_FBTERM_ROOT="$HOME/.local/share/framebridge/fbterm"
|
||||
if [ -n "$FRAMEBRIDGE_FBTERM_ROOT_OVERRIDE" ]; then
|
||||
export FRAMEBRIDGE_FBTERM_ROOT="$FRAMEBRIDGE_FBTERM_ROOT_OVERRIDE"
|
||||
fi
|
||||
export PROMPT_COMMAND='"$FRAMEBRIDGE_FBTERM_ROOT/bin/fbterm-prompt-ready"'
|
||||
|
||||
exec bash --noprofile --norc
|
||||
+497
@@ -0,0 +1,497 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import fcntl
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
class FBTermError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class FBTermBusy(FBTermError):
|
||||
pass
|
||||
|
||||
|
||||
class FBTerm:
|
||||
JOB_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
|
||||
|
||||
def __init__(self, root, session, title):
|
||||
self.root = Path(root)
|
||||
self.session = session
|
||||
self.title = title
|
||||
|
||||
self.bin = self.root / "bin"
|
||||
self.jobs = self.root / "jobs"
|
||||
|
||||
self.runner = self.bin / "fbterm-run"
|
||||
self.shell = self.bin / "fbterm-shell"
|
||||
self.lock_path = self.root / "controller.lock"
|
||||
|
||||
@staticmethod
|
||||
def _run(args, **kwargs):
|
||||
return subprocess.run(
|
||||
args,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def has_session(self):
|
||||
p = self._run(
|
||||
["tmux", "has-session", "-t", self.session]
|
||||
)
|
||||
return p.returncode == 0
|
||||
|
||||
def ensure(self, visible=True):
|
||||
self.root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
self.jobs.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
|
||||
if not self.has_session():
|
||||
env = os.environ.copy()
|
||||
env["FBTERM_ROOT"] = str(self.root)
|
||||
env["FBTERM_SESSION"] = self.session
|
||||
env["PATH"] = str(self.bin) + os.pathsep + env.get("PATH", "")
|
||||
|
||||
p = subprocess.run(
|
||||
[
|
||||
"tmux",
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
self.session,
|
||||
str(self.shell),
|
||||
],
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise FBTermError(
|
||||
"tmux new-session fallo: " + p.stderr.strip()
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"tmux",
|
||||
"set-environment",
|
||||
"-t",
|
||||
self.session,
|
||||
"FBTERM_ROOT",
|
||||
str(self.root),
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"tmux",
|
||||
"set-environment",
|
||||
"-t",
|
||||
self.session,
|
||||
"FBTERM_SESSION",
|
||||
self.session,
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"tmux",
|
||||
"set-option",
|
||||
"-t",
|
||||
self.session,
|
||||
"allow-rename",
|
||||
"off",
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"tmux",
|
||||
"set-option",
|
||||
"-t",
|
||||
self.session,
|
||||
"set-titles",
|
||||
"off",
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"tmux",
|
||||
"send-keys",
|
||||
"-t",
|
||||
self.session + ":0.0",
|
||||
(
|
||||
"export FBTERM_ROOT="
|
||||
+ subprocess.list2cmdline([str(self.root)])
|
||||
+ "; export FBTERM_SESSION="
|
||||
+ subprocess.list2cmdline([self.session])
|
||||
+ "; export PATH="
|
||||
+ subprocess.list2cmdline(
|
||||
[str(self.bin) + ":$PATH"]
|
||||
)
|
||||
),
|
||||
"C-m",
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
if visible and not self.visible_window():
|
||||
subprocess.Popen(
|
||||
[
|
||||
"gnome-terminal",
|
||||
"--title=" + self.title,
|
||||
"--",
|
||||
"bash",
|
||||
"-lc",
|
||||
(
|
||||
"exec tmux attach-session -r -t "
|
||||
+ subprocess.list2cmdline([self.session])
|
||||
),
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
for _ in range(40):
|
||||
if self.visible_window():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
return self.status()
|
||||
|
||||
def visible_window(self):
|
||||
p = self._run(["wmctrl", "-l"])
|
||||
return (
|
||||
p.returncode == 0
|
||||
and self.title in p.stdout
|
||||
)
|
||||
|
||||
def status(self):
|
||||
if not self.has_session():
|
||||
return {
|
||||
"session": False,
|
||||
"visible": False,
|
||||
"pane_pid": None,
|
||||
"pane_tty": None,
|
||||
"pane_command": None,
|
||||
"clients": 0,
|
||||
"readonly_clients": 0,
|
||||
}
|
||||
|
||||
fmt = (
|
||||
"#{pane_pid}|#{pane_tty}|"
|
||||
"#{pane_current_command}"
|
||||
)
|
||||
|
||||
p = self._run(
|
||||
[
|
||||
"tmux",
|
||||
"display-message",
|
||||
"-p",
|
||||
"-t",
|
||||
self.session + ":0.0",
|
||||
fmt,
|
||||
]
|
||||
)
|
||||
|
||||
parts = p.stdout.strip().split("|", 2)
|
||||
|
||||
pane_pid = parts[0] if len(parts) > 0 else ""
|
||||
pane_tty = parts[1] if len(parts) > 1 else ""
|
||||
pane_command = parts[2] if len(parts) > 2 else ""
|
||||
|
||||
clients = self._run(
|
||||
[
|
||||
"tmux",
|
||||
"list-clients",
|
||||
"-t",
|
||||
self.session,
|
||||
"-F",
|
||||
"#{client_readonly}",
|
||||
]
|
||||
)
|
||||
|
||||
values = [
|
||||
x.strip()
|
||||
for x in clients.stdout.splitlines()
|
||||
if x.strip()
|
||||
]
|
||||
|
||||
readonly = sum(
|
||||
1
|
||||
for x in values
|
||||
if x in ("1", "on", "true")
|
||||
)
|
||||
|
||||
return {
|
||||
"session": True,
|
||||
"visible": self.visible_window(),
|
||||
"pane_pid": pane_pid,
|
||||
"pane_tty": pane_tty,
|
||||
"pane_command": pane_command,
|
||||
"clients": len(values),
|
||||
"readonly_clients": readonly,
|
||||
}
|
||||
|
||||
def execute(self, job_id, command, timeout_s=120):
|
||||
if not self.JOB_RE.fullmatch(job_id):
|
||||
raise FBTermError("JOB_ID invalido")
|
||||
|
||||
if not isinstance(command, str) or not command:
|
||||
raise FBTermError("command vacio")
|
||||
|
||||
timeout_s = int(timeout_s)
|
||||
|
||||
if timeout_s < 1 or timeout_s > 3600:
|
||||
raise FBTermError("timeout fuera de rango")
|
||||
|
||||
self.ensure(visible=True)
|
||||
|
||||
lock_fd = os.open(
|
||||
self.lock_path,
|
||||
os.O_CREAT | os.O_RDWR,
|
||||
0o600,
|
||||
)
|
||||
|
||||
try:
|
||||
try:
|
||||
fcntl.flock(
|
||||
lock_fd,
|
||||
fcntl.LOCK_EX | fcntl.LOCK_NB,
|
||||
)
|
||||
except BlockingIOError:
|
||||
raise FBTermBusy("FBTERM ocupado")
|
||||
|
||||
# R2: fail-closed si existe evidencia durable incompleta.
|
||||
# La reconciliacion conserva esos directorios fuera de jobs.
|
||||
incomplete_jobs = []
|
||||
|
||||
try:
|
||||
for prior in self.jobs.iterdir():
|
||||
if (
|
||||
prior.is_dir()
|
||||
and (
|
||||
not (prior / "DONE").exists()
|
||||
or not (prior / "result.env").exists()
|
||||
)
|
||||
):
|
||||
incomplete_jobs.append(prior.name)
|
||||
except OSError as exc:
|
||||
raise FBTermError(
|
||||
"no se pudo auditar jobs previos: " + str(exc)
|
||||
)
|
||||
|
||||
if incomplete_jobs:
|
||||
sample = ", ".join(sorted(incomplete_jobs)[:3])
|
||||
raise FBTermBusy(
|
||||
"FBTERM tiene job incompleto: " + sample
|
||||
)
|
||||
|
||||
job = self.jobs / job_id
|
||||
|
||||
if job.exists():
|
||||
raise FBTermError("JOB_ID ya existe")
|
||||
|
||||
job.mkdir(mode=0o700)
|
||||
|
||||
command_file = job / "command.bash"
|
||||
command_file.write_text(
|
||||
command,
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.chmod(command_file, 0o600)
|
||||
|
||||
(job / "timeout.seconds").write_text(
|
||||
str(timeout_s) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
|
||||
expected_command_sha = hashlib.sha256(
|
||||
command.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
# R2 idle gate: prompt.ready existe solo cuando el shell
|
||||
# persistente esta realmente esperando el siguiente comando.
|
||||
ready_path = self.jobs.parent / "prompt.ready"
|
||||
ready_deadline = time.monotonic() + 3.0
|
||||
|
||||
while time.monotonic() < ready_deadline:
|
||||
if ready_path.exists():
|
||||
break
|
||||
time.sleep(0.02)
|
||||
else:
|
||||
raise FBTermBusy(
|
||||
"FBTERM pane no esta en prompt"
|
||||
)
|
||||
|
||||
try:
|
||||
ready_path.unlink()
|
||||
except FileNotFoundError:
|
||||
raise FBTermBusy(
|
||||
"FBTERM prompt cambio antes del envio"
|
||||
)
|
||||
except OSError as exc:
|
||||
raise FBTermError(
|
||||
"no se pudo reservar prompt: " + str(exc)
|
||||
)
|
||||
|
||||
send = self._run(
|
||||
[
|
||||
"tmux",
|
||||
"send-keys",
|
||||
"-t",
|
||||
self.session + ":0.0",
|
||||
"fbterm-run " + job_id,
|
||||
"C-m",
|
||||
]
|
||||
)
|
||||
|
||||
if send.returncode != 0:
|
||||
raise FBTermError(
|
||||
"tmux send-keys fallo: "
|
||||
+ send.stderr.strip()
|
||||
)
|
||||
|
||||
deadline = time.monotonic() + timeout_s + 8
|
||||
|
||||
while time.monotonic() < deadline:
|
||||
if (job / "DONE").exists():
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
if not (job / "DONE").exists():
|
||||
raise FBTermError(
|
||||
"resultado durable no aparecio"
|
||||
)
|
||||
|
||||
# R2: no liberar el lock hasta que bash haya vuelto realmente
|
||||
# al prompt. fbterm-shell actualiza prompt.ready mediante
|
||||
# PROMPT_COMMAND despues de que fbterm-run haya terminado.
|
||||
done_path = job / "DONE"
|
||||
prompt_deadline = time.monotonic() + 5.0
|
||||
prompt_ready = False
|
||||
|
||||
while time.monotonic() < prompt_deadline:
|
||||
try:
|
||||
if (
|
||||
ready_path.exists()
|
||||
and ready_path.stat().st_mtime_ns
|
||||
> done_path.stat().st_mtime_ns
|
||||
):
|
||||
prompt_ready = True
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
time.sleep(0.02)
|
||||
|
||||
if not prompt_ready:
|
||||
raise FBTermError(
|
||||
"prompt durable no aparecio"
|
||||
)
|
||||
|
||||
result = {}
|
||||
|
||||
for line in (job / "result.env").read_text(
|
||||
encoding="utf-8"
|
||||
).splitlines():
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
result[k] = v
|
||||
|
||||
result["EXPECTED_COMMAND_SHA256"] = (
|
||||
expected_command_sha
|
||||
)
|
||||
|
||||
result["COMMAND_HASH_GATE"] = (
|
||||
"PASS"
|
||||
if result.get("COMMAND_SHA256")
|
||||
== expected_command_sha
|
||||
else "FAIL"
|
||||
)
|
||||
|
||||
result["STDOUT_PATH"] = str(
|
||||
job / "stdout.bin"
|
||||
)
|
||||
|
||||
result["STDERR_PATH"] = str(
|
||||
job / "stderr.bin"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
finally:
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
os.close(lock_fd)
|
||||
|
||||
|
||||
def file_text(path):
|
||||
return Path(path).read_text(
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import sys
|
||||
|
||||
root = os.environ["FBTERM_ROOT"]
|
||||
session = os.environ["FBTERM_SESSION"]
|
||||
title = os.environ["FBTERM_TITLE"]
|
||||
|
||||
term = FBTerm(root, session, title)
|
||||
|
||||
action = sys.argv[1]
|
||||
|
||||
if action == "ensure":
|
||||
print(json.dumps(term.ensure(), sort_keys=True))
|
||||
|
||||
elif action == "status":
|
||||
print(json.dumps(term.status(), sort_keys=True))
|
||||
|
||||
elif action == "exec":
|
||||
job_id = sys.argv[2]
|
||||
timeout_s = int(sys.argv[3])
|
||||
command_path = sys.argv[4]
|
||||
|
||||
try:
|
||||
r = term.execute(
|
||||
job_id,
|
||||
file_text(command_path),
|
||||
timeout_s,
|
||||
)
|
||||
print(json.dumps(r, sort_keys=True))
|
||||
except FBTermBusy as e:
|
||||
print(json.dumps({
|
||||
"ERROR": "BUSY",
|
||||
"MESSAGE": str(e),
|
||||
}))
|
||||
sys.exit(75)
|
||||
|
||||
else:
|
||||
raise SystemExit(64)
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
umask 077
|
||||
|
||||
ROOT="${FBTERM_ROOT:-$HOME/.local/share/framebridge/fbterm}"
|
||||
LIB="$ROOT/library"
|
||||
JOBS="$ROOT/jobs"
|
||||
|
||||
valid() {
|
||||
[[ "$1" =~ ^[A-Za-z0-9._-]+$ ]]
|
||||
}
|
||||
|
||||
secret_guard() {
|
||||
grep -Eqi \
|
||||
'BEGIN .*PRIVATE KEY|rclone\.conf|client_secret|access_token|refresh_token|password[[:space:]]*=|token[[:space:]]*=|cookie[[:space:]]*=' \
|
||||
"$1"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
list)
|
||||
mkdir -p "$LIB"
|
||||
find "$LIB" -maxdepth 1 -type f -name '*.sh' -printf '%f\n' |
|
||||
sed 's/\.sh$//' | sort
|
||||
;;
|
||||
|
||||
adopt)
|
||||
job="${2:-}"
|
||||
name="${3:-}"
|
||||
|
||||
valid "$job" && valid "$name" || {
|
||||
echo 'FBX_ERROR=INVALID_NAME'
|
||||
exit 64
|
||||
}
|
||||
|
||||
src="$JOBS/$job/command.bash"
|
||||
dst="$LIB/$name.sh"
|
||||
|
||||
[ -f "$src" ] || {
|
||||
echo 'FBX_ERROR=JOB_COMMAND_NOT_FOUND'
|
||||
exit 66
|
||||
}
|
||||
|
||||
[ ! -e "$dst" ] || {
|
||||
echo 'FBX_ERROR=LIBRARY_NAME_EXISTS'
|
||||
exit 73
|
||||
}
|
||||
|
||||
cp -p "$src" "$dst"
|
||||
chmod 600 "$dst"
|
||||
|
||||
sha256sum "$dst" | awk '{print $1}' > "$LIB/$name.sha256"
|
||||
printf '%s\n' "$job" > "$LIB/$name.source-job"
|
||||
|
||||
echo "FBX_ADOPT=PASS NAME=$name JOB=$job SHA256=$(cat "$LIB/$name.sha256")"
|
||||
;;
|
||||
|
||||
hash)
|
||||
name="${2:-}"
|
||||
valid "$name" || exit 64
|
||||
file="$LIB/$name.sh"
|
||||
[ -f "$file" ] || exit 66
|
||||
sha256sum "$file"
|
||||
;;
|
||||
|
||||
show|run)
|
||||
action="$1"
|
||||
name="${2:-}"
|
||||
|
||||
valid "$name" || {
|
||||
echo 'FBX_ERROR=INVALID_NAME'
|
||||
exit 64
|
||||
}
|
||||
|
||||
file="$LIB/$name.sh"
|
||||
hashfile="$LIB/$name.sha256"
|
||||
|
||||
[ -f "$file" ] && [ -f "$hashfile" ] || {
|
||||
echo 'FBX_ERROR=LIBRARY_ITEM_NOT_FOUND'
|
||||
exit 66
|
||||
}
|
||||
|
||||
expected="$(cat "$hashfile")"
|
||||
actual="$(sha256sum "$file" | awk '{print $1}')"
|
||||
|
||||
[ "$actual" = "$expected" ] || {
|
||||
echo 'FBX_ERROR=HASH_MISMATCH'
|
||||
exit 74
|
||||
}
|
||||
|
||||
if secret_guard "$file"; then
|
||||
echo 'FBX_ERROR=POSSIBLE_SECRET_REFUSE_DISPLAY'
|
||||
exit 77
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "========== FBX SCRIPT: $name =========="
|
||||
echo "SHA256=$actual"
|
||||
echo
|
||||
cat "$file"
|
||||
echo
|
||||
echo "========== END FBX SCRIPT =========="
|
||||
echo
|
||||
|
||||
[ "$action" = show ] && exit 0
|
||||
|
||||
bash --noprofile --norc "$file"
|
||||
rc=$?
|
||||
|
||||
echo
|
||||
echo "========== FBX RESULT: $name EXIT=$rc =========="
|
||||
exit "$rc"
|
||||
;;
|
||||
|
||||
status)
|
||||
echo "FBX_ROOT=$ROOT"
|
||||
echo "FBX_LIBRARY=$LIB"
|
||||
echo "FBX_ITEMS=$(find "$LIB" -maxdepth 1 -type f -name '*.sh' | wc -l)"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo 'USAGE: fbx {status|list|adopt JOB NAME|show NAME|run NAME|hash NAME}'
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
Reference in new issue
Block a user