Files
lmxopcua/scripts/validation/opcua-client-lifecycle/run.py
T
2026-11-10 12:44:04 -05:00

77 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Build and run isolated actual production Shared/driver/browser lifecycle loops."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import secrets
import shutil
import subprocess
import time
import uuid
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[2]
def write(path, value):
path.write_text(json.dumps(value, indent=2) + '\n')
def certs(root):
return {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in sorted(root.rglob('*.der')) if '/own/certs/' in str(p)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', required=True, type=Path, help='Fresh nonexistent task evidence root, includes private PKI; never commit entire output')
parser.add_argument('--duration', type=int, default=90)
parser.add_argument('--full', action='store_true', help='Requires >=7200 seconds of real completed client runtime')
parser.add_argument('--image', default='mcr.microsoft.com/dotnet/sdk:10.0.103-noble')
args = parser.parse_args()
if args.duration <= 0 or (args.full and args.duration < 7200):
parser.error('--full requires duration >=7200; diagnostic duration must be positive')
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=False)
os.chmod(output, 0o700)
build = output / 'app'
task = output / 'task'
task.mkdir()
image = json.loads(subprocess.check_output(['docker', 'image', 'inspect', args.image, '--format', '{{json .}}'], text=True))
if image['Architecture'] != 'arm64' or image['Os'] != 'linux':
raise RuntimeError('Expected cached official Linux arm64 SDK image')
write(output / 'source.json', {'commit': subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip(),
'root': str(ROOT), 'durationSeconds': args.duration, 'mode': 'full' if args.full else 'diagnostic',
'image': {k: image.get(k) for k in ('Id', 'RepoDigests', 'Architecture', 'Os')}, 'network': 'none',
'startedUtc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'scope': 'actual unchanged production Shared/OpcUaClientDriver/AdminUI browser in a repeated same-process loop; SDK 2.0.1 only'})
with (output / 'build.log').open('w') as log:
subprocess.run(['dotnet', 'publish', str(HERE / 'Lifecycle.csproj'), '-c', 'Release', '-m:1', '--nologo', '-o', str(build)], stdout=log, stderr=subprocess.STDOUT, check=True, timeout=300)
shutil.copy2(HERE / 'container.sh', task / 'container.sh')
name = 'otopcua-client-lifecycle-' + uuid.uuid4().hex[:12]
env = dict(os.environ, OPCUA_MATRIX_PASSWORD='otua_lifecycle_' + secrets.token_urlsafe(32),
LIFECYCLE_SECONDS=str(args.duration), LIFECYCLE_MODE='full' if args.full else 'diagnostic')
command = ['docker', 'run', '--rm', '--name', name, '--network', 'none',
'-e', 'OPCUA_MATRIX_PASSWORD', '-e', 'LIFECYCLE_SECONDS', '-e', 'LIFECYCLE_MODE',
'-v', str(build) + ':/app:ro', '-v', str(task) + ':/task', args.image, 'bash', '/task/container.sh']
write(output / 'command.json', {'argv': command, 'timeoutSeconds': args.duration + 300})
started = time.monotonic()
try:
with (output / 'container.log').open('w') as log:
result = subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, env=env, timeout=args.duration + 300)
except BaseException:
subprocess.run(['docker', 'rm', '-f', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=20)
raise
summary = json.loads((task / 'summary.json').read_text()) if (task / 'summary.json').exists() else None
write(output / 'result.json', {'exitCode': result.returncode, 'containerElapsedSeconds': time.monotonic() - started,
'summary': summary, 'applicationCertificates': certs(task)})
if result.returncode or not summary or (args.full and not summary['fullTwoHourLifecycle']):
raise RuntimeError('Lifecycle run failed; inspect task/failure.txt, client.log and server.log: ' + str(output))
print(json.dumps({'output': str(output), 'summary': summary}, indent=2))
if __name__ == '__main__':
main()