Skip to content

Conversation

@mbarlow12
Copy link

Fixes #12

Instead of writing the pngs directly from within the field audit. The PR adds a helper function in graph.py to create the final Dot object and uses that method to generate the process graph for the field which is then added to the FieldResult. Then, pngs are actually written in save_results in manager.py.

This also tidies the architecture a bit in that the auditing features are no longer responsible for saving/storing data.

Tests are updated to reflect the changes.

@mbarlow12 mbarlow12 added the bug Something isn't working label May 12, 2025
@lloyd-rmi lloyd-rmi self-requested a review May 14, 2025 17:56
@lloyd-rmi
Copy link

Trying to run tests locally and running into audit test failures still. This is the output from the test_audit_save_procs:
`__________________________________________________________________________________________ test_audit_save_procs ___________________________________________________________________________________________

self = <pydot.core.Dot object at 0x313e83fe0>, prog = 'dot', format = 'png', encoding = None

def create(self, prog=None, format="ps", encoding=None):
    """Creates and returns a binary image for the graph.

    create will write the graph to a temporary dot file in the
    encoding specified by `encoding` and process it with the
    program given by 'prog' (which defaults to 'twopi'), reading
    the binary image output and return it as `bytes`.

    There's also the preferred possibility of using:

        create_'format'(prog='program')

    which are automatically defined for all the supported formats,
    for example:

      - `create_ps()`
      - `create_gif()`
      - `create_dia()`

    If 'prog' is a list, instead of a string,
    then the fist item is expected to be the program name,
    followed by any optional command-line arguments for it:

        [ 'twopi', '-Tdot', '-s10' ]


    @param prog: either:

      - name of GraphViz executable that
        can be found in the `$PATH`, or

      - absolute path to GraphViz executable.

      If you have added GraphViz to the `$PATH` and
      use its executables as installed
      (without renaming any of them)
      then their names are:

        - `'dot'`
        - `'twopi'`
        - `'neato'`
        - `'circo'`
        - `'fdp'`
        - `'sfdp'`

      On Windows, these have the notorious ".exe" extension that,
      only for the above strings, will be added automatically.

      The `$PATH` is inherited from `os.env['PATH']` and
      passed to `subprocess.Popen` using the `env` argument.

      If you haven't added GraphViz to your `$PATH` on Windows,
      then you may want to give the absolute path to the
      executable (for example, to `dot.exe`) in `prog`.
    """
    if prog is None:
        prog = self.prog

    assert prog is not None

    if isinstance(prog, (list, tuple)):
        prog, args = prog[0], prog[1:]
    else:
        args = []

    # temp file
    with tempfile.TemporaryDirectory(
        ignore_cleanup_errors=True
    ) as tmp_dir:
        fp = tempfile.NamedTemporaryFile(dir=tmp_dir, delete=False)
        fp.close()
        self.write(fp.name, encoding=encoding)

        # For each of the image files, copy it to the temporary directory
        # with the same filename as the original
        for img in self.shape_files:
            outfile = os.path.join(tmp_dir, os.path.basename(img))
            with open(img, "rb") as img_in, open(outfile, "wb") as img_out:
                img_data = img_in.read()
                img_out.write(img_data)

        arguments = [f"-T{format}"] + args + [fp.name]

        try:
          stdout_data, stderr_data, process = call_graphviz(
                program=prog,
                arguments=arguments,
                working_dir=tmp_dir,
            )

opgee4-env/lib/python3.12/site-packages/pydot/core.py:1851:


opgee4-env/lib/python3.12/site-packages/pydot/core.py:211: in call_graphviz
process = subprocess.Popen(
../../../../../../../.local/share/uv/python/cpython-3.12.0-macos-aarch64-none/lib/python3.12/subprocess.py:1026: in init
self._execute_child(args, executable, preexec_fn, close_fds,


self = <Popen: returncode: 255 args: ['dot', '-Tpng', '/var/folders/sn/ys4drmt52ngf...>, args = ['dot', '-Tpng', '/var/folders/sn/ys4drmt52ngftlv8vrg07dvr0000gp/T/tmprad1zixz/tmpe225nh0x']
executable = b'dot', preexec_fn = None, close_fds = True, pass_fds = (), cwd = '/var/folders/sn/ys4drmt52ngftlv8vrg07dvr0000gp/T/tmprad1zixz'
env = {'LD_LIBRARY_PATH': '', 'PATH': '/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/...run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin', 'SYSTEMROOT': ''}
startupinfo = None, creationflags = 0, shell = False, p2cread = -1, p2cwrite = -1, c2pread = 88, c2pwrite = 91, errread = 92, errwrite = 93, restore_signals = True, gid = None, gids = None, uid = None
umask = -1, start_new_session = False, process_group = -1

def _execute_child(self, args, executable, preexec_fn, close_fds,
                   pass_fds, cwd, env,
                   startupinfo, creationflags, shell,
                   p2cread, p2cwrite,
                   c2pread, c2pwrite,
                   errread, errwrite,
                   restore_signals,
                   gid, gids, uid, umask,
                   start_new_session, process_group):
    """Execute program (POSIX version)"""

    if isinstance(args, (str, bytes)):
        args = [args]
    elif isinstance(args, os.PathLike):
        if shell:
            raise TypeError('path-like args is not allowed when '
                            'shell is true')
        args = [args]
    else:
        args = list(args)

    if shell:
        # On Android the default shell is at '/system/bin/sh'.
        unix_shell = ('/system/bin/sh' if
                  hasattr(sys, 'getandroidapilevel') else '/bin/sh')
        args = [unix_shell, "-c"] + args
        if executable:
            args[0] = executable

    if executable is None:
        executable = args[0]

    sys.audit("subprocess.Popen", executable, args, cwd, env)

    if (_USE_POSIX_SPAWN
            and os.path.dirname(executable)
            and preexec_fn is None
            and not close_fds
            and not pass_fds
            and cwd is None
            and (p2cread == -1 or p2cread > 2)
            and (c2pwrite == -1 or c2pwrite > 2)
            and (errwrite == -1 or errwrite > 2)
            and not start_new_session
            and process_group == -1
            and gid is None
            and gids is None
            and uid is None
            and umask < 0):
        self._posix_spawn(args, executable, env, restore_signals,
                          p2cread, p2cwrite,
                          c2pread, c2pwrite,
                          errread, errwrite)
        return

    orig_executable = executable

    # For transferring possible exec failure from child to parent.
    # Data format: "exception name:hex errno:description"
    # Pickle is not used; it is complex and involves memory allocation.
    errpipe_read, errpipe_write = os.pipe()
    # errpipe_write must not be in the standard io 0, 1, or 2 fd range.
    low_fds_to_close = []
    while errpipe_write < 3:
        low_fds_to_close.append(errpipe_write)
        errpipe_write = os.dup(errpipe_write)
    for low_fd in low_fds_to_close:
        os.close(low_fd)
    try:
        try:
            # We must avoid complex work that could involve
            # malloc or free in the child process to avoid
            # potential deadlocks, thus we do all this here.
            # and pass it to fork_exec()

            if env is not None:
                env_list = []
                for k, v in env.items():
                    k = os.fsencode(k)
                    if b'=' in k:
                        raise ValueError("illegal environment variable name")
                    env_list.append(k + b'=' + os.fsencode(v))
            else:
                env_list = None  # Use execv instead of execve.
            executable = os.fsencode(executable)
            if os.path.dirname(executable):
                executable_list = (executable,)
            else:
                # This matches the behavior of os._execvpe().
                executable_list = tuple(
                    os.path.join(os.fsencode(dir), executable)
                    for dir in os.get_exec_path(env))
            fds_to_keep = set(pass_fds)
            fds_to_keep.add(errpipe_write)
            self.pid = _fork_exec(
                    args, executable_list,
                    close_fds, tuple(sorted(map(int, fds_to_keep))),
                    cwd, env_list,
                    p2cread, p2cwrite, c2pread, c2pwrite,
                    errread, errwrite,
                    errpipe_read, errpipe_write,
                    restore_signals, start_new_session,
                    process_group, gid, gids, uid, umask,
                    preexec_fn, _USE_VFORK)
            self._child_created = True
        finally:
            # be sure the FD is closed no matter what
            os.close(errpipe_write)

        self._close_pipe_fds(p2cread, p2cwrite,
                             c2pread, c2pwrite,
                             errread, errwrite)

        # Wait for exec to fail or succeed; possibly raising an
        # exception (limited in size)
        errpipe_data = bytearray()
        while True:
            part = os.read(errpipe_read, 50000)
            errpipe_data += part
            if not part or len(errpipe_data) > 50000:
                break
    finally:
        # be sure the FD is closed no matter what
        os.close(errpipe_read)

    if errpipe_data:
        try:
            pid, sts = os.waitpid(self.pid, 0)
            if pid == self.pid:
                self._handle_exitstatus(sts)
            else:
                self.returncode = sys.maxsize
        except ChildProcessError:
            pass

        try:
            exception_name, hex_errno, err_msg = (
                    errpipe_data.split(b':', 2))
            # The encoding here should match the encoding
            # written in by the subprocess implementations
            # like _posixsubprocess
            err_msg = err_msg.decode()
        except ValueError:
            exception_name = b'SubprocessError'
            hex_errno = b'0'
            err_msg = 'Bad exception data from child: {!r}'.format(
                          bytes(errpipe_data))
        child_exception_type = getattr(
                builtins, exception_name.decode('ascii'),
                SubprocessError)
        if issubclass(child_exception_type, OSError) and hex_errno:
            errno_num = int(hex_errno, 16)
            child_exec_never_called = (err_msg == "noexec")
            if child_exec_never_called:
                err_msg = ""
                # The error must be from chdir(cwd).
                err_filename = cwd
            else:
                err_filename = orig_executable
            if errno_num != 0:
                err_msg = os.strerror(errno_num)
          raise child_exception_type(errno_num, err_msg, err_filename)

E FileNotFoundError: [Errno 2] No such file or directory: 'dot'

../../../../../../../.local/share/uv/python/cpython-3.12.0-macos-aarch64-none/lib/python3.12/subprocess.py:1950: FileNotFoundError

During handling of the above exception, another exception occurred:

tmp_path = PosixPath('/private/var/folders/sn/ys4drmt52ngftlv8vrg07dvr0000gp/T/pytest-of-lloyd.mckenzie/pytest-7/test_audit_save_procs0'), opgee_main = <opgee.tool.Opgee object at 0x311a73770>

def test_audit_save_procs(tmp_path: Path, opgee_main):
  audit_path, proc_graph_path = audit_setup_and_run(tmp_path, opgee_main, "Processes")

tests/test_audit.py:133:


tests/test_audit.py:118: in audit_setup_and_run
opgee.run(argList=cmd)
opgee/tool.py:239: in run
obj.run(args, self)
opgee/built_ins/run_plugin.py:364: in run
save_results(
opgee/manager.py:488: in save_results
proc_graph.write_png(pathjoin(procs_path, f"{field_name}_process_graph.png"))
opgee4-env/lib/python3.12/site-packages/pydot/core.py:166: in __write_method
self.write(path, format=f, prog=prog, encoding=encoding)
opgee4-env/lib/python3.12/site-packages/pydot/core.py:1762: in write
s = self.create(prog, format, encoding=encoding)


self = <pydot.core.Dot object at 0x313e83fe0>, prog = 'dot', format = 'png', encoding = None

def create(self, prog=None, format="ps", encoding=None):
    """Creates and returns a binary image for the graph.

    create will write the graph to a temporary dot file in the
    encoding specified by `encoding` and process it with the
    program given by 'prog' (which defaults to 'twopi'), reading
    the binary image output and return it as `bytes`.

    There's also the preferred possibility of using:

        create_'format'(prog='program')

    which are automatically defined for all the supported formats,
    for example:

      - `create_ps()`
      - `create_gif()`
      - `create_dia()`

    If 'prog' is a list, instead of a string,
    then the fist item is expected to be the program name,
    followed by any optional command-line arguments for it:

        [ 'twopi', '-Tdot', '-s10' ]


    @param prog: either:

      - name of GraphViz executable that
        can be found in the `$PATH`, or

      - absolute path to GraphViz executable.

      If you have added GraphViz to the `$PATH` and
      use its executables as installed
      (without renaming any of them)
      then their names are:

        - `'dot'`
        - `'twopi'`
        - `'neato'`
        - `'circo'`
        - `'fdp'`
        - `'sfdp'`

      On Windows, these have the notorious ".exe" extension that,
      only for the above strings, will be added automatically.

      The `$PATH` is inherited from `os.env['PATH']` and
      passed to `subprocess.Popen` using the `env` argument.

      If you haven't added GraphViz to your `$PATH` on Windows,
      then you may want to give the absolute path to the
      executable (for example, to `dot.exe`) in `prog`.
    """
    if prog is None:
        prog = self.prog

    assert prog is not None

    if isinstance(prog, (list, tuple)):
        prog, args = prog[0], prog[1:]
    else:
        args = []

    # temp file
    with tempfile.TemporaryDirectory(
        ignore_cleanup_errors=True
    ) as tmp_dir:
        fp = tempfile.NamedTemporaryFile(dir=tmp_dir, delete=False)
        fp.close()
        self.write(fp.name, encoding=encoding)

        # For each of the image files, copy it to the temporary directory
        # with the same filename as the original
        for img in self.shape_files:
            outfile = os.path.join(tmp_dir, os.path.basename(img))
            with open(img, "rb") as img_in, open(outfile, "wb") as img_out:
                img_data = img_in.read()
                img_out.write(img_data)

        arguments = [f"-T{format}"] + args + [fp.name]

        try:
            stdout_data, stderr_data, process = call_graphviz(
                program=prog,
                arguments=arguments,
                working_dir=tmp_dir,
            )
        except OSError as e:
            if e.errno == errno.ENOENT:
                args = list(e.args)
                args[1] = f'"{prog}" not found in path.'
              raise OSError(*args)

E FileNotFoundError: [Errno 2] "dot" not found in path.

opgee4-env/lib/python3.12/site-packages/pydot/core.py:1860: FileNotFoundError`

@lloyd-rmi
Copy link

Seeing similar issue with test_audit_save_none test

@lloyd-rmi
Copy link

When running locally, still running into errors when trying to use the "Processes" audit level.

Logs:
`INFO opgee.manager: <Timer 'run_serial' completed in 0:00:23>
INFO opgee.manager: Stopping cluster
INFO opgee.manager: <Timer 'Manager.run_packets' completed in 0:01:37>
{PROGRAM} failed: {e}
Traceback (most recent call last):
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee4-env/lib/python3.12/site-packages/pydot/core.py", line 1851, in create
stdout_data, stderr_data, process = call_graphviz(
^^^^^^^^^^^^^^
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee4-env/lib/python3.12/site-packages/pydot/core.py", line 211, in call_graphviz
process = subprocess.Popen(
^^^^^^^^^^^^^^^^^
File "/Users/lloyd.mckenzie/.local/share/uv/python/cpython-3.12.0-macos-aarch64-none/lib/python3.12/subprocess.py", line 1026, in init
self._execute_child(args, executable, preexec_fn, close_fds,
File "/Users/lloyd.mckenzie/.local/share/uv/python/cpython-3.12.0-macos-aarch64-none/lib/python3.12/subprocess.py", line 1950, in _execute_child
raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'dot'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee/tool.py", line 329, in main
_main(argv)
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee/tool.py", line 309, in _main
tool.run(args=args)
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee/tool.py", line 239, in run
obj.run(args, self)
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee/built_ins/run_plugin.py", line 364, in run
save_results(
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee/manager.py", line 488, in save_results
proc_graph.write_png(pathjoin(procs_path, f"{field_name}_process_graph.png"))
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee4-env/lib/python3.12/site-packages/pydot/core.py", line 166, in __write_method
self.write(path, format=f, prog=prog, encoding=encoding)
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee4-env/lib/python3.12/site-packages/pydot/core.py", line 1762, in write
s = self.create(prog, format, encoding=encoding)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lloyd.mckenzie/Library/CloudStorage/OneDrive-RMI/Documents/GitHub/RMI/OPGEEv4/opgee4-env/lib/python3.12/site-packages/pydot/core.py", line 1860, in create
raise OSError(*args)
FileNotFoundError: [Errno 2] "dot" not found in path.`

@lloyd-rmi
Copy link

Issue might be that need to install graphviz package locally. If so, we should add this as a necessary step in our README

@mbarlow12
Copy link
Author

@lloyd-rmi I'm curious to know if you've tried installing Graphviz and whether that fixed your issue.

chore(deps): update lock file and add requirements.in
@mbarlow12
Copy link
Author

@lloyd-rmi I added a pip installation section that includes Graphviz as an optional step.

@lloyd-rmi
Copy link

Unblocking this for now. I'm still running into some local testing issues but that seems to be a localized issue and not an issue with this actual PR.

@mbarlow12 mbarlow12 merged commit fd79fe7 into main Jun 21, 2025
2 of 3 checks passed
@mbarlow12 mbarlow12 deleted the fix/audit-procs branch June 21, 2025 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Processes auditing level not working locally

3 participants