#!/usr/bin/env python3
#
# Minimal reproduction for concurrent entry into libvyosconfig.
#
# vyos-http-api runs configure operations in a threadpool
# (_configure_op -> run_in_threadpool(_execute_configure_op)), and that
# workflow builds Config(), which parses the running and session configs via
# ConfigTree(...) -> libvyosconfig from_string. retrieve_op is served on the
# event loop and also builds ConfigTree(res). ctypes releases the GIL for each
# foreign call, so both threads are inside the OCaml library at once, which
# corrupts the parser state and kills the process with SIGSEGV.
#
# This script drives the same library path directly: several threads build
# ConfigTree objects from the default config, compare them, and drop them (so
# ConfigTree.__del__ -> destroy runs too), while other threads parse an invalid
# config and check the error text. The work runs in a child process so a
# segfault is reported instead of killing the script.
#
# Run as root on a VyOS instance:
#   python3 reproduce.py
#
# Stock image: "child process killed by SIGSEGV". Exit code 1.
# With the patched python/vyos/configtree.py in place: "0 errors". Exit code 0.

import signal
import subprocess
import sys
import threading
import time

CONFIG = '/opt/vyatta/etc/config.boot.default'
INVALID = 'interfaces {\n    ethernet eth0 {\n        address \n'
THREADS = 4
SECONDS = 20


def child():
    from vyos.configtree import ConfigTree

    config = open(CONFIG).read()
    reference = ConfigTree(config)
    deadline = time.time() + SECONDS
    errors = []
    counts = [0] * THREADS

    def valid(idx):
        while time.time() < deadline:
            tree = ConfigTree(config)
            if tree != reference:
                errors.append('parsed tree differs from reference')
            del tree
            counts[idx] += 1

    def invalid(idx):
        while time.time() < deadline:
            try:
                ConfigTree(INVALID)
                errors.append('invalid config parsed without error')
            except ValueError as e:
                if 'Syntax error' not in str(e):
                    errors.append(f'unexpected error text: {str(e)[:80]!r}')
            counts[idx] += 1

    threads = [
        threading.Thread(target=valid if i % 2 == 0 else invalid, args=(i,))
        for i in range(THREADS)
    ]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    print(f'{sum(counts)} ConfigTree operations from {THREADS} threads in {SECONDS}s')
    for e in sorted(set(errors))[:5]:
        print(f'  {e}')
    print(f'{len(errors)} errors')
    return 1 if errors else 0


def main():
    if sys.argv[1:] == ['--child']:
        return child()
    proc = subprocess.run([sys.executable, __file__, '--child'])
    if proc.returncode < 0:
        name = signal.Signals(-proc.returncode).name
        print(f'child process killed by {name}')
        return 1
    return proc.returncode


if __name__ == '__main__':
    sys.exit(main())
