77 lines
4.2 KiB
Python
Executable File
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()
|