How & where to best retrieve sudo password via a native GUI on a macOS Python-based app - (while maintaining an interactive output stream (stdout))

狂风中的少年 提交于 2019-12-07 20:18:35

问题


Ok, so the situation is this: I am building a macOS GUI App using Python and wx (wxphoenix). The user can use the GUI (say: script1) to launch a file-deletion process (contained in script2). In order to run successfully script2 needs to run with sudo rights.

script2 will itterate over a long list of files and delete them. But I need it to communicate with the GUI contained in script1 after each round so that script1 can update the progressbar.

In it's absolute most basic form my current working setup looks like this:

Script1:

import io
from threading import Thread
import subprocess

import wx

# a whole lot of wx GUI stuff 

def get_password():
    """Retrieve user password via a GUI"""

    # A wx solution using wx.PasswordEntryDialog()
    # Store password in a variable

    return variable

class run_script_with_sudo(Thread):
    """Launch a script with administrator privileges"""

    def __init__(self, path_to_script, wx_pubsub_sendmessage):
        """Set variables to self"""
        self.path = path_to_script
        self.sender = wx_pubsub_sendmessage
        self.password = get_password()
        Thread.__init__(self)
        self.start()

    def run(self):
        """Run thread"""

        prepare_script = subprocess.Popen(["echo", password], stdout=subprocess.PIPE)
        prepare_script.wait()
        launch_script = subprocess.Popen(['sudo', '-S', '/usr/local/bin/python3.6', '-u', self.path], stdin=prepare_script.stdout, stdout=subprocess.PIPE)
        for line in io.TextIOWrapper(launch_script.stdout, encoding="utf-8"):
            print("Received line: ", line.rstrip())
            # Tell progressbar to add another step:
            wx.CallAfter(self.sender, "update", msg="")

Script2:

import time

# This is a test setup, just a very simple loop that produces an output.

for i in range(25):
    time.sleep(1)
    print(i)

The above setup works in that script1 receives the output of script2 in real-time and acts on it. (So in the given example: after each second script1 adds another step to the progress bar until it reaches 25 steps).

What I want to achieve = not storing the password in a variable and using macOS it's native GUI to retrieve the password.

However when I change:

prepare_script = subprocess.Popen(["echo", password], stdout=subprocess.PIPE)
prepare_script.wait()
launch_script = subprocess.Popen(['sudo', '-S', '/usr/local/bin/python3.6', '-u', self.path], stdin=prepare_script.stdout, stdout=subprocess.PIPE)
for line in io.TextIOWrapper(launch_script.stdout, encoding="utf-8"):
                print("Received line: ", line.rstrip())
                # Tell progressbar to add another step:
                wx.CallAfter(self.sender, "update", msg="")

Into:

command = r"""/usr/bin/osascript -e 'do shell script "/usr/local/bin/python3.6 -u """ + self.path + """ with prompt "Sart Deletion Process " with administrator privileges'"""
command_list = shlex.split(command)

launch_script = subprocess.Popen(command_list, stdout=subprocess.PIPE)
for line in io.TextIOWrapper(launch_script.stdout, encoding="utf-8"):
    print("Received line: ", line.rstrip())
    # Tell progressbar to add another step:
    wx.CallAfter(self.sender, "update", msg="")

It stops working because osascript apparently runs in a non-interactive shell. This means script2 doesn't sent any output until it is fully finished, causing the progress bar in script1 to stall.

My question thus becomes: How can I make sure to use macOS native GUI to ask for the sudo password, thus preventing having to store it in a variable, while still maintaining the possibility to catch the stdout from the privileged script in an interactive / real-time stream.

Hope that makes sense.

Would appreciate any insights!


回答1:


My question thus becomes: How can I make sure to use macOS native GUI to ask for the sudo password, thus preventing having to store it in a variable, while still maintaining the possibility to catch the stdout from the privileged script in an interactive / real-time stream.

I have found a solution myself, using a named pipe (os.mkfifo()).

That way, you can have 2 python scripts communicate with each other while 1 of them is launched with privileged rights via osascript (meaning: you get a native GUI window that asks for the users sudo password).

Working solution:

mainscript.py

import os
from pathlib import Path
import shlex
import subprocess
import sys
from threading import Thread
import time

class LaunchDeletionProcess(Thread):

    def __init__(self):

        Thread.__init__(self)

    def run(self):

        launch_command = r"""/usr/bin/osascript -e 'do shell script "/usr/local/bin/python3.6 -u /path/to/priviliged_script.py" with prompt "Sart Deletion Process " with administrator privileges'"""
        split_command = shlex.split(launch_command)

        print("Thread 1 started")
        testprogram = subprocess.Popen(split_command)
        testprogram.wait()
        print("Thread1 Finished")

class ReadStatus(Thread):

    def __init__(self):

        Thread.__init__(self)

    def run(self):

        while not os.path.exists(os.path.expanduser("~/p1")):
            time.sleep(0.1)

        print("Thread 2 started")

        self.wfPath = os.path.expanduser("~/p1")

        rp = open(self.wfPath, 'r')
        response = rp.read()

        self.try_pipe(response)

    def try_pipe(self, response):
        rp = open(self.wfPath, 'r')
        response = rp.read()
        print("Receiving response: ", response)
        rp.close()
        if response == str(self.nr_of_steps-1):
            print("Got to end")
            os.remove(os.path.expanduser("~/p1"))
        else:
            time.sleep(1)
            self.try_pipe(response)

if __name__ == "__main__":

    thread1 = LaunchDeletionProcess()
    thread2 = ReadStatus()
    thread1.start()
    thread2.start()

priviliged_script.py

import os
import time
import random

wfPath = os.path.expanduser("~/p1")

try:

    os.mkfifo(wfPath)

except OSError:

    print("error")
    pass

result = 10

nr = 0 

while nr < result:

    random_nr = random.random()

    wp = open(wfPath, 'w')
    print("writing new number: ", random_nr)
    wp.write("Number: " + str(random_nr))       
    wp.close()

    time.sleep(1)
    nr += 1

wp = open(wfPath, 'w')
wp.write("end")     
wp.close()


来源:https://stackoverflow.com/questions/49171769/how-where-to-best-retrieve-sudo-password-via-a-native-gui-on-a-macos-python-ba

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!