From d0f058568c1bd61279dc41ca971c2e2525b1fbcd Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Wed, 24 Jan 2018 10:48:34 -0600 Subject: [PATCH] Add Polkit support for installation --- lib/fbuild/context.py | 7 +- lib/fbuild/install.py | 163 +++++++++++++++++++++++ lib/fbuild/main.py | 19 +-- lib/fbuild/path.py | 4 +- lib/fbuild/subprocess/killableprocess.py | 1 + misc/com.github.fbuild.install.policy | 23 ++++ setup.py | 4 +- 7 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 lib/fbuild/install.py create mode 100644 misc/com.github.fbuild.install.policy diff --git a/lib/fbuild/context.py b/lib/fbuild/context.py index d057f62..576c129 100644 --- a/lib/fbuild/context.py +++ b/lib/fbuild/context.py @@ -252,7 +252,12 @@ def timeout_function(p): return stdout, stderr def install(self, path, target, *, rename=None, perms=None): - """Set the given file to be installed after the build completes.""" + """Set the given file to be installed after the build completes. + + *path* is the path of the file to install, and *target* is a subdirectory + of the installation prefix where the file should be installed to. If *rename* + is given, it should be a new basename for the file when installed into the + target directory.""" self.to_install.append((Path(path).abspath(), target, rename, perms)) # ------------------------------------------------------------------------------ diff --git a/lib/fbuild/install.py b/lib/fbuild/install.py new file mode 100644 index 0000000..b5b2ea4 --- /dev/null +++ b/lib/fbuild/install.py @@ -0,0 +1,163 @@ +import base64 +import os +import pickle +import subprocess +import sys + +import fbuild +import fbuild.builders +import fbuild.path +import fbuild.subprocess.killableprocess + +# ------------------------------------------------------------------------------ + + +class Commander: pass + + +class LocalCommander: + def install(self, file, target, perms=None): + file.copy(target) + if perms is not None: + file.chmod(perms) + + def close(self): pass + + +class PolkitCommander: + def __init__(self, proc): + self.proc = proc + + self._send('chdir', os.getcwd()) + + def _send(self, *message): + encoded = self._encode(message) + self.proc.stdin.write(encoded) + self.proc.stdin.write('\n') + self.proc.stdin.flush() + + @staticmethod + def _encode(data): + return base64.b64encode(pickle.dumps(data)).decode('ascii') + + @staticmethod + def _decode(data): + return pickle.loads(base64.b64decode(data.encode('ascii'))) + + def install(self, file, target, perms=None): + self._send('install', file, target, perms) + + def close(self): + self._send('close') + self.proc.wait() + + +def polkit_process(): + commander = LocalCommander() + + sys.stdout.write('\n') + sys.stdout.close() + sys.stdout = sys.stderr + + for line in sys.stdin: + message = PolkitCommander._decode(line.strip()) + command, args = message[0], message[1:] + + if command == 'chdir': + os.chdir(args[0]) + elif command == 'install': + commander.install(*args) + elif command == 'close': + sys.exit() + + +class Installer: + """An Installer manages installation of all the files marked for installation.""" + + def __init__(self, ctx): + self.ctx = ctx + self.privileged = False + + def _install(self, commander): + for file, subdir, rename, perms in self.ctx.to_install: + # Generate the full subdirectory. + target_root = fbuild.path.Path(subdir).addroot(self.ctx.install_prefix) + target_root.makedirs(exist_ok=True) + + # Generate the target path. + target = target_root / (rename or file.basename()) + file = file.relpath(file.getcwd()) + + # Install the file. + self.ctx.logger.check(' * install', '%s -> %s' % (file, target), + color='yellow') + commander.install(file, target, perms) + + def _find_pkexec(self): + try: + return fbuild.builders.find_program(self.ctx, ['pkexec'], quieter=1) + except fbuild.builders.MissingProgram: + return None + + def _load_polkit_process(self, pkexec): + root_module = fbuild.path.Path(fbuild.__file__) + root_directory = root_module.parent.parent + + # Make sure Fbuild can be found. + env = os.environ.copy() + if 'PYTHONPATH' in env: + env['PYTHONPATH'] = '%s:%s' % (root_directory, env['PYTHONPATH']) + else: + env['PYTHONPATH'] = root_directory + + # Run the process. + cmd = [pkexec, sys.executable, '-m', 'fbuild.install', 'polkit'] + proc = fbuild.subprocess.killableprocess.Popen(cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + universal_newlines=True) + + # Make sure it's ready first. + proc.stdout.readline() + return proc + + # try: + # p.wait() + # except KeyboardInterrupt: + # os.kill(os.getpid(), signal.SIGKILL) + # # p.wait() + # raise + + # if p.returncode != 0: + # self.ctx.logger.log("Failed to run 'fbuild install' via pkexec.", color='red') + # sys.exit(p.returncode) + + def install(self): + """Install all the files that are marked for installation. If the user does + not have permissions to install to the installation prefix, but Polkit's pkexec + is present, then pkexec will be used to spawn a privileged Fbuild process that + can install the files.""" + + commander = LocalCommander() + + if not self.ctx.install_prefix.access(os.W_OK): + pkexec = None + + if not self.privileged: + pkexec = self._find_pkexec() + + if pkexec is None: + self.ctx.logger.log('Warning: You do not have write access for the ' \ + 'installation directory.', color='red') + else: + proc = self._load_polkit_process(pkexec) + commander = PolkitCommander(proc) + + try: + self._install(commander) + finally: + commander.close() + + +if __name__ == '__main__': + assert sys.argv[1] == 'polkit' + polkit_process() diff --git a/lib/fbuild/main.py b/lib/fbuild/main.py index b2ed34a..78a9213 100644 --- a/lib/fbuild/main.py +++ b/lib/fbuild/main.py @@ -14,6 +14,7 @@ import fbuild.context import fbuild.options import fbuild.builders.file +import fbuild.install # If we can't import fbuildroot, save the exception and raise it later. try: @@ -72,22 +73,8 @@ def parse_args(argv): # ------------------------------------------------------------------------------ def install_files(ctx): - for file, subdir, rename, perms in ctx.to_install: - # Generate the full subdirectory. - target_root = fbuild.path.Path(subdir).addroot(ctx.install_prefix) - target_root.makedirs(exist_ok=True) - - # Generate the target path. - target = target_root / (rename or file.basename()) - file = file.relpath(file.getcwd()) - - # Copy the file. - ctx.logger.check(' * install', '%s -> %s' % (file, target), color='yellow') - file.copy(target) - - # Set permissions. - if perms is not None: - file.chmod(perms) + installer = fbuild.install.Installer(ctx) + installer.install() # ------------------------------------------------------------------------------ diff --git a/lib/fbuild/path.py b/lib/fbuild/path.py index ae13d82..93b0246 100644 --- a/lib/fbuild/path.py +++ b/lib/fbuild/path.py @@ -98,8 +98,8 @@ def parent(self): # ------------------------------------------------------------------------- # methods - def access(self): - return os.access(self) + def access(self, *args, **kw): + return os.access(self, *args, **kw) def addprefix(self, prefix): """Add the prefix before the basename of the path. diff --git a/lib/fbuild/subprocess/killableprocess.py b/lib/fbuild/subprocess/killableprocess.py index 9efda03..0e03167 100644 --- a/lib/fbuild/subprocess/killableprocess.py +++ b/lib/fbuild/subprocess/killableprocess.py @@ -250,6 +250,7 @@ def wait(self, timeout=-1, group=True): # time.sleep is interrupted by signals (good!) newtimeout = timeout - time.time() + starttime time.sleep(newtimeout) + time.sleep(0.1) self.kill(group) signal.signal(signal.SIGCHLD, oldsignal) diff --git a/misc/com.github.fbuild.install.policy b/misc/com.github.fbuild.install.policy new file mode 100644 index 0000000..5d01c95 --- /dev/null +++ b/misc/com.github.fbuild.install.policy @@ -0,0 +1,23 @@ + + + + + The Fbuild Build System + https://github.com/felix-lang/fbuild + + + Install the given project via Fbuild + Authentication is required to install the project via Fbuild + audio-x-generic + + no + no + auth_admin_keep + + /usr/bin/python + -m + fbuild.install + + + diff --git a/setup.py b/setup.py index eac5188..e5d9cfa 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,10 @@ cmdclass = {} data_files = [] -if sys.platform != 'win32': +if sys.platform.startswith('linux'): data_files.append(('/usr/local/share/uprocd/modules', ['misc/fbuild.module'])) + data_files.append(('/usr/share/polkit-1/actions', + ['misc/com.github.fbuild.install.policy'])) setup( name='fbuild',