Skip to content

Refactor @needs_root to re-run only the decorated function in a subprocess#317

Open
ezio-melotti wants to merge 2 commits into
masterfrom
refactor-needs-root
Open

Refactor @needs_root to re-run only the decorated function in a subprocess#317
ezio-melotti wants to merge 2 commits into
masterfrom
refactor-needs-root

Conversation

@ezio-melotti

Copy link
Copy Markdown
Collaborator

The @needs_root decorator was initially designed with individual commands in mind: if the user ran python simoc-sam.py foo arg1 arg2 and foo required root privileges, the decorator would use os.execvp to replace the current process with sudo python simoc-sam.py foo arg1 arg2.

This works well for individual commands, but not if the foo() function was called by other functions as in the following example:

@cmd
def test_cmd():
    print(os.geteuid())
    foo(arg1, arg2)
    print(os.geteuid())

Running python simoc-sam.py test-cmd would:

  1. start executing test_cmd
  2. print 1000
  3. try to call foo(arg1, arg2)
  4. call sudo python simoc-sam.py test-cmd because of @needs_root
  5. start executing test_cmd again
  6. print 0 (root user)
  7. execute foo(arg1, arg2) (as root)
  8. print 0 again

IOW, even though only foo required root privileges, the decorator would cause everything before foo() to be executed twice (first normally, then as root), and everything after foo() to be executed as root (even if it's not needed).

This PR fixes this by replacing the os.execvp call with a subprocess that only runs the decorated function as root, with the caveat that the function must be a command too.

Running python simoc-sam.py test-cmd now would:

  1. start executing test_cmd
  2. print 1000
  3. try to call foo(arg1, arg2)
  4. call sudo python simoc-sam.py foo arg1 arg2 because of @needs_root
  5. print 1000 again

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the @needs_root decorator in simoc-sam.py so that, when root privileges are required, it re-invokes only the decorated command via sudo in a subprocess rather than execvp-replacing the whole process (avoiding re-running the caller command and accidentally running subsequent code as root).

Changes:

  • Added inspect usage to reconstruct the decorated function’s arguments for re-invocation.
  • Replaced os.execvp(...) with subprocess.run(['sudo', ..., sys.executable, __file__, cmd_name, *cmd_args]).
  • Returned success based on the subprocess exit code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread simoc-sam.py
Comment on lines +82 to +85
bound.apply_defaults()
# re-run with sudo in a subprocess
cmd_name = func.__name__
cmd_args = [str(arg) for arg in bound.arguments.values()]

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bound.apply_defaults() causes default values to be forwarded into the sudo subprocess. For parameters where None means “not provided” (e.g. setup_wifi(ssid=None, password=None, ...)), this turns into the literal string "None" on the command line, changing behavior (the is None checks won’t trigger) and can lead to incorrect configuration. Consider not applying defaults and only forwarding explicitly provided arguments, or omit any defaulted arguments (especially those defaulting to None) when building cmd_args.

Suggested change
bound.apply_defaults()
# re-run with sudo in a subprocess
cmd_name = func.__name__
cmd_args = [str(arg) for arg in bound.arguments.values()]
# re-run with sudo in a subprocess; only forward explicitly provided,
# non-None arguments to avoid turning default None into "None"
cmd_name = func.__name__
cmd_args = [str(arg) for arg in bound.arguments.values() if arg is not None]

Copilot uses AI. Check for mistakes.
Comment thread simoc-sam.py
Comment on lines +80 to +86
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
# re-run with sudo in a subprocess
cmd_name = func.__name__
cmd_args = [str(arg) for arg in bound.arguments.values()]
cmd = ['sudo', '--preserve-env=HOME',

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sudo re-run builds CLI args using only bound.arguments.values(), which loses keyword information and can mis-map arguments if the decorated function is called with keyword arguments (e.g. setup_hotspot(password='x') would pass 'x' as the first positional arg). Either reject kwargs in the non-root path with a clear error, or extend the CLI to accept --param value style flags and preserve names when re-invoking.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #318.

Comment thread simoc-sam.py
Comment on lines +84 to +88
cmd_name = func.__name__
cmd_args = [str(arg) for arg in bound.arguments.values()]
cmd = ['sudo', '--preserve-env=HOME',
sys.executable, __file__, cmd_name, *cmd_args]
result = subprocess.run(cmd, cwd=SIMOC_SAM_DIR)

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation assumes the decorated function is invokable via the script’s command dispatcher (COMMANDS[func.__name__]). If @needs_root is applied to a helper (or a command name differs from __name__), the sudo subprocess will fail at runtime. Consider asserting func.__name__ in COMMANDS (and possibly that COMMANDS[name] is func) and raising a helpful error message if not.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants