104 lines
4.3 KiB
Python
104 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
from codexpi.remote_ipc import remote_channel_request
|
||
from codexpi.remote_service import RemoteError, enqueue_remote_job
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Accès fichier limité à une machine associée à CodexPi")
|
||
parser.add_argument("--machine", help="identifiant de machine ; facultatif si une seule est associée")
|
||
subparsers = parser.add_subparsers(dest="operation", required=True)
|
||
subparsers.add_parser("machines")
|
||
listing = subparsers.add_parser("list")
|
||
listing.add_argument("path", nargs="?", default="")
|
||
reading = subparsers.add_parser("read")
|
||
reading.add_argument("path")
|
||
writing = subparsers.add_parser("write")
|
||
writing.add_argument("path")
|
||
writing.add_argument("--expected", help="SHA-256 obtenu lors de la lecture, pour empêcher un écrasement concurrent")
|
||
executing = subparsers.add_parser("exec")
|
||
executing.add_argument("argv", nargs=argparse.REMAINDER)
|
||
args = parser.parse_args()
|
||
database = os.environ.get("CODEXPI_DATABASE", "/app/data/codexpi.db")
|
||
remote_channel = os.environ.get("CODEXPI_REMOTE_CHANNEL")
|
||
project_id = os.environ.get("CODEXPI_PROJECT_ID", "")
|
||
if not project_id:
|
||
print("Cette commande doit être lancée depuis une session de projet CodexPi.", file=sys.stderr)
|
||
return 2
|
||
try:
|
||
if remote_channel:
|
||
payload = None
|
||
expected = None
|
||
if args.operation == "write":
|
||
if sys.stdin.isatty():
|
||
raise RemoteError("Le contenu du fichier doit être fourni sur l’entrée standard")
|
||
payload = {"content": sys.stdin.read()}
|
||
expected = args.expected
|
||
elif args.operation == "exec":
|
||
if not args.argv:
|
||
raise RemoteError("Indiquez la commande distante après exec")
|
||
payload = {"argv": args.argv, "timeout": 120}
|
||
result = remote_channel_request(remote_channel, {
|
||
"operation": args.operation,
|
||
"path": getattr(args, "path", ""),
|
||
"payload": payload,
|
||
"expected_hash": expected,
|
||
"machine_id": args.machine,
|
||
})
|
||
if args.operation == "machines":
|
||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||
return 0
|
||
elif args.operation == "machines":
|
||
import sqlite3
|
||
connection = sqlite3.connect(database)
|
||
connection.row_factory = sqlite3.Row
|
||
rows = connection.execute(
|
||
"SELECT id,name,folder_name,platform,last_seen FROM remote_machines WHERE project_id=? AND revoked_at IS NULL ORDER BY last_seen DESC",
|
||
(project_id,),
|
||
).fetchall()
|
||
print(json.dumps([dict(row) for row in rows], ensure_ascii=False, indent=2))
|
||
return 0
|
||
else:
|
||
payload = None
|
||
expected = None
|
||
if args.operation == "write":
|
||
if sys.stdin.isatty():
|
||
raise RemoteError("Le contenu du fichier doit être fourni sur l’entrée standard")
|
||
content = sys.stdin.read()
|
||
payload = {"content": content}
|
||
expected = args.expected
|
||
elif args.operation == "exec":
|
||
if not args.argv:
|
||
raise RemoteError("Indiquez la commande distante après exec")
|
||
payload = {"argv": args.argv, "timeout": 120}
|
||
result = enqueue_remote_job(
|
||
database,
|
||
project_id,
|
||
args.operation,
|
||
args.path,
|
||
payload,
|
||
expected,
|
||
args.machine,
|
||
)
|
||
if args.operation == "read":
|
||
sys.stdout.write(str(result.get("content", "")))
|
||
if result.get("content") and not str(result["content"]).endswith("\n"):
|
||
sys.stdout.write("\n")
|
||
print(f"[sha256:{result.get('sha256','')}]", file=sys.stderr)
|
||
else:
|
||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||
return 0
|
||
except (RemoteError, OSError, ValueError) as exc:
|
||
print(f"Erreur distante : {exc}", file=sys.stderr)
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|