Pārlūkot izejas kodu

backup: convert from multiprocessing to asyncio

QubesOS/qubes-issues#2931
Marek Marczykowski-Górecki 6 gadi atpakaļ
vecāks
revīzija
d4e9120903
1 mainītis faili ar 250 papildinājumiem un 259 dzēšanām
  1. 250 259
      qubes/backup.py

+ 250 - 259
qubes/backup.py

@@ -1,7 +1,7 @@
 #
 # The Qubes OS Project, http://www.qubes-os.org
 #
-# Copyright (C) 2013-2015  Marek Marczykowski-Górecki
+# Copyright (C) 2013-2017  Marek Marczykowski-Górecki
 #                                   <marmarek@invisiblethingslab.com>
 # Copyright (C) 2013  Olivier Médoc <o_medoc@yahoo.fr>
 #
@@ -25,6 +25,8 @@ import logging
 import functools
 import termios
 
+import asyncio
+
 from qubes.utils import size_to_human
 import stat
 import os
@@ -37,7 +39,6 @@ import time
 import grp
 import pwd
 import datetime
-from multiprocessing import Queue, Process
 import qubes
 import qubes.core2migration
 import qubes.storage
@@ -182,7 +183,8 @@ class BackupHeader(object):
                 f_header.write("{!s}={!s}\n".format(key, getattr(self, attr)))
 
 
-class SendWorker(Process):
+class SendWorker(object):
+    # pylint: disable=too-few-public-methods
     def __init__(self, queue, base_dir, backup_stdout):
         super(SendWorker, self).__init__()
         self.queue = queue
@@ -190,13 +192,12 @@ class SendWorker(Process):
         self.backup_stdout = backup_stdout
         self.log = logging.getLogger('qubes.backup')
 
+    @asyncio.coroutine
     def run(self):
         self.log.debug("Started sending thread")
 
-        self.log.debug("Moving to temporary dir %s", self.base_dir)
-        os.chdir(self.base_dir)
-
-        for filename in iter(self.queue.get, None):
+        while True:
+            filename = yield from self.queue.get()
             if filename in (QUEUE_FINISHED, QUEUE_ERROR):
                 break
 
@@ -206,14 +207,11 @@ class SendWorker(Process):
             # verified before untaring.
             tar_final_cmd = ["tar", "-cO", "--posix",
                              "-C", self.base_dir, filename]
-            final_proc = subprocess.Popen(tar_final_cmd,
-                                          stdin=subprocess.PIPE,
-                                          stdout=self.backup_stdout)
-            if final_proc.wait() >= 2:
-                if self.queue.full():
-                    # if queue is already full, remove some entry to wake up
-                    # main thread, so it will be able to notice error
-                    self.queue.get()
+            final_proc = yield from asyncio.create_subprocess_exec(
+                *tar_final_cmd,
+                stdout=self.backup_stdout)
+            retcode = yield from final_proc.wait()
+            if retcode >= 2:
                 # handle only exit code 2 (tar fatal error) or
                 # greater (call failed?)
                 raise qubes.exc.QubesException(
@@ -222,11 +220,11 @@ class SendWorker(Process):
 
             # Delete the file as we don't need it anymore
             self.log.debug("Removing file {}".format(filename))
-            os.remove(filename)
+            os.remove(os.path.join(self.base_dir, filename))
 
         self.log.debug("Finished sending thread")
 
-
+@asyncio.coroutine
 def launch_proc_with_pty(args, stdin=None, stdout=None, stderr=None, echo=True):
     """Similar to pty.fork, but handle stdin/stdout according to parameters
     instead of connecting to the pty
@@ -244,12 +242,16 @@ def launch_proc_with_pty(args, stdin=None, stdout=None, stderr=None, echo=True):
             termios_p[3] &= ~termios.ECHO
             termios.tcsetattr(ctty_fd, termios.TCSANOW, termios_p)
     (pty_master, pty_slave) = os.openpty()
-    p = subprocess.Popen(args, stdin=stdin, stdout=stdout, stderr=stderr,
+    p = yield from asyncio.create_subprocess_exec(*args,
+        stdin=stdin,
+        stdout=stdout,
+        stderr=stderr,
         preexec_fn=lambda: set_ctty(pty_slave, pty_master))
     os.close(pty_slave)
-    return p, os.fdopen(pty_master, 'wb+', buffering=0)
+    return p, open(pty_master, 'wb+', buffering=0)
 
 
+@asyncio.coroutine
 def launch_scrypt(action, input_name, output_name, passphrase):
     '''
     Launch 'scrypt' process, pass passphrase to it and return
@@ -262,7 +264,7 @@ def launch_scrypt(action, input_name, output_name, passphrase):
     :return: subprocess.Popen object
     '''
     command_line = ['scrypt', action, input_name, output_name]
-    (p, pty) = launch_proc_with_pty(command_line,
+    (p, pty) = yield from launch_proc_with_pty(command_line,
         stdin=subprocess.PIPE if input_name == '-' else None,
         stdout=subprocess.PIPE if output_name == '-' else None,
         stderr=subprocess.PIPE,
@@ -272,7 +274,7 @@ def launch_scrypt(action, input_name, output_name, passphrase):
     else:
         prompts = (b'Please enter passphrase: ',)
     for prompt in prompts:
-        actual_prompt = p.stderr.read(len(prompt))
+        actual_prompt = yield from p.stderr.read(len(prompt))
         if actual_prompt != prompt:
             raise qubes.exc.QubesException(
                 'Unexpected prompt from scrypt: {}'.format(actual_prompt))
@@ -301,7 +303,7 @@ class Backup(object):
     >>> }
     >>> backup_op = Backup(app, vms, exclude_vms, **options)
     >>> print(backup_op.get_backup_summary())
-    >>> backup_op.backup_do()
+    >>> asyncio.get_event_loop().run_until_complete(backup_op.backup_do())
 
     See attributes of this object for all available options.
 
@@ -393,11 +395,6 @@ class Backup(object):
             else:
                 raise AttributeError(key)
 
-        #: whether backup was canceled
-        self.canceled = False
-        #: list of PIDs to kill on backup cancel
-        self.processes_to_kill_on_cancel = []
-
         self.log = logging.getLogger('qubes.backup')
 
         if exclude_list is None:
@@ -416,17 +413,6 @@ class Backup(object):
         if self.tmpdir and os.path.exists(self.tmpdir):
             shutil.rmtree(self.tmpdir)
 
-    def cancel(self):
-        """Cancel running backup operation. Can be called from another thread.
-        """
-        self.canceled = True
-        for proc in self.processes_to_kill_on_cancel:
-            try:
-                proc.terminate()
-            except OSError:
-                pass
-
-
     def get_files_to_backup(self):
         files_to_backup = {}
         for vm in self.vms_for_backup:
@@ -554,7 +540,8 @@ class Backup(object):
 
         return summary
 
-    def prepare_backup_header(self):
+    @asyncio.coroutine
+    def _prepare_backup_header(self):
         header_file_path = os.path.join(self.tmpdir, HEADER_FILENAME)
         backup_header = BackupHeader(
             version=CURRENT_BACKUP_FORMAT_VERSION,
@@ -569,30 +556,21 @@ class Backup(object):
         # protection
         scrypt_passphrase = u'{filename}!{passphrase}'.format(
             filename=HEADER_FILENAME, passphrase=self.passphrase)
-        scrypt = launch_scrypt(
+        scrypt = yield from launch_scrypt(
             'enc', header_file_path, header_file_path + '.hmac',
             scrypt_passphrase)
 
-        if scrypt.wait() != 0:
+        retcode = yield from scrypt.wait()
+        if retcode:
             raise qubes.exc.QubesException(
                 "Failed to compute hmac of header file: "
                 + scrypt.stderr.read())
         return HEADER_FILENAME, HEADER_FILENAME + ".hmac"
 
 
-    @staticmethod
-    def _queue_put_with_check(proc, vmproc, queue, element):
-        if queue.full():
-            if not proc.is_alive():
-                if vmproc:
-                    message = ("Failed to write the backup, VM output:\n" +
-                               vmproc.stderr.read())
-                else:
-                    message = "Failed to write the backup. Out of disk space?"
-                raise qubes.exc.QubesException(message)
-        queue.put(element)
-
     def _send_progress_update(self):
+        if not self.total_backup_bytes:
+            return
         if callable(self.progress_callback):
             progress = (
                 100 * (self._done_vms_bytes + self._current_vm_bytes) /
@@ -604,6 +582,168 @@ class Backup(object):
         self._current_vm_bytes += bytes_done
         self._send_progress_update()
 
+    @asyncio.coroutine
+    def _split_and_send(self, input_stream, file_basename,
+            output_queue):
+        '''Split *input_stream* into parts of max *chunk_size* bytes and send
+        to *output_queue*.
+
+        :param input_stream: stream (asyncio reader stream) of data to split
+        :param file_basename: basename (i.e. without part number and '.enc')
+        of output files
+        :param output_queue: asyncio.Queue instance to put produced files to
+        - queue will get only filenames of written chunks
+        '''
+        # Wait for compressor (tar) process to finish or for any
+        # error of other subprocesses
+        i = 0
+        run_error = "size_limit"
+        scrypt = None
+        while run_error == "size_limit":
+            # Prepare a first chunk
+            chunkfile = file_basename + ".%03d.enc" % i
+            i += 1
+
+            # Start encrypt, scrypt will also handle integrity
+            # protection
+            scrypt_passphrase = \
+                u'{backup_id}!{filename}!{passphrase}'.format(
+                    backup_id=self.backup_id,
+                    filename=os.path.relpath(chunkfile[:-4],
+                        self.tmpdir),
+                    passphrase=self.passphrase)
+            try:
+                scrypt = yield from launch_scrypt(
+                    "enc", "-", chunkfile, scrypt_passphrase)
+
+                run_error = yield from handle_streams(
+                    input_stream,
+                    scrypt.stdin,
+                    self.chunk_size,
+                    self._add_vm_progress
+                )
+
+                self.log.debug(
+                    "handle_streams returned: {}".format(run_error))
+            except:
+                scrypt.terminate()
+                raise
+
+            scrypt.stdin.close()
+            yield from scrypt.wait()
+            self.log.debug("scrypt return code: {}".format(
+                scrypt.returncode))
+
+            # Send the chunk to the backup target
+            yield from output_queue.put(
+                os.path.relpath(chunkfile, self.tmpdir))
+
+    @asyncio.coroutine
+    def _wrap_and_send_files(self, files_to_backup, output_queue):
+        for vm_info in files_to_backup:
+            for file_info in vm_info.files:
+
+                self.log.debug("Backing up {}".format(file_info))
+
+                backup_tempfile = os.path.join(
+                    self.tmpdir, file_info.subdir,
+                    file_info.name)
+                self.log.debug("Using temporary location: {}".format(
+                    backup_tempfile))
+
+                # Ensure the temporary directory exists
+                if not os.path.isdir(os.path.dirname(backup_tempfile)):
+                    os.makedirs(os.path.dirname(backup_tempfile))
+
+                # The first tar cmd can use any complex feature as we want.
+                # Files will be verified before untaring this.
+                # Prefix the path in archive with filename["subdir"] to have it
+                # verified during untar
+                tar_cmdline = (["tar", "-Pc", '--sparse',
+                                '-C', os.path.dirname(file_info.path)] +
+                               (['--dereference'] if
+                               file_info.subdir != "dom0-home/" else []) +
+                               ['--xform=s:^%s:%s\\0:' % (
+                                   os.path.basename(file_info.path),
+                                   file_info.subdir),
+                                   os.path.basename(file_info.path)
+                               ])
+                file_stat = os.stat(file_info.path)
+                if stat.S_ISBLK(file_stat.st_mode) or \
+                        file_info.name != os.path.basename(file_info.path):
+                    # tar doesn't handle content of block device, use our
+                    # writer
+                    # also use our tar writer when renaming file
+                    assert not stat.S_ISDIR(file_stat.st_mode), \
+                        "Renaming directories not supported"
+                    tar_cmdline = ['python3', '-m', 'qubes.tarwriter',
+                        '--override-name=%s' % (
+                            os.path.join(file_info.subdir, os.path.basename(
+                                file_info.name))),
+                        file_info.path]
+                if self.compressed:
+                    tar_cmdline.insert(-2,
+                        "--use-compress-program=%s" % self.compression_filter)
+
+                self.log.debug(" ".join(tar_cmdline))
+
+                # Pipe: tar-sparse | scrypt | tar | backup_target
+                # TODO: log handle stderr
+                tar_sparse = yield from asyncio.create_subprocess_exec(
+                    *tar_cmdline, stdout=subprocess.PIPE)
+
+                try:
+                    yield from self._split_and_send(
+                        tar_sparse.stdout,
+                        backup_tempfile,
+                        output_queue)
+                except:
+                    try:
+                        tar_sparse.terminate()
+                    except ProcessLookupError:
+                        pass
+                    raise
+
+
+            # This VM done, update progress
+            self._done_vms_bytes += vm_info.size
+            self._current_vm_bytes = 0
+            self._send_progress_update()
+            # Save date of last backup
+            if vm_info.vm:
+                vm_info.vm.backup_timestamp = datetime.datetime.now()
+
+        yield from output_queue.put(QUEUE_FINISHED)
+
+    @staticmethod
+    @asyncio.coroutine
+    def _monitor_process(proc, error_message):
+        try:
+            yield from proc.wait()
+        except:
+            proc.terminate()
+            raise
+
+        if proc.returncode:
+            raise qubes.exc.QubesException(error_message)
+
+    @staticmethod
+    @asyncio.coroutine
+    def _cancel_on_error(future, previous_task):
+        '''If further element of chain fail, cancel previous one to
+        avoid deadlock.
+        When earlier element of chain fail, it will be handled by
+        :py:meth:`backup_do`.
+
+        The chain is:
+        :py:meth:`_wrap_and_send_files` -> :py:class:`SendWorker` -> vmproc
+        '''
+        try:
+            yield from future
+        except:  # pylint: disable=bare-except
+            previous_task.cancel()
+
+    @asyncio.coroutine
     def backup_do(self):
         # pylint: disable=too-many-statements
         if self.passphrase is None:
@@ -613,10 +753,12 @@ class Backup(object):
         shutil.copy(qubes_xml, os.path.join(self.tmpdir, 'qubes.xml'))
         qubes_xml = os.path.join(self.tmpdir, 'qubes.xml')
         backup_app = qubes.Qubes(qubes_xml)
+        backup_app.events_enabled = False
 
         files_to_backup = self._files_to_backup
         # make sure backup_content isn't set initially
         for vm in backup_app.domains:
+            vm.events_enabled = False
             vm.features['backup-content'] = False
 
         for qid, vm_info in files_to_backup.items():
@@ -629,17 +771,16 @@ class Backup(object):
         backup_app.save()
 
         vmproc = None
-        tar_sparse = None
         if self.target_vm is not None:
             # Prepare the backup target (Qubes service call)
             # If APPVM, STDOUT is a PIPE
-            vmproc = self.target_vm.run_service('qubes.Backup',
-                passio_popen=True, passio_stderr=True)
-            vmproc.stdin.write((self.target_dir.
+            read_fd, write_fd = os.pipe()
+            vmproc = yield from self.target_vm.run_service('qubes.Backup',
+                stdin=read_fd, stderr=subprocess.PIPE)
+            os.close(read_fd)
+            os.write(write_fd, (self.target_dir.
                 replace("\r", "").replace("\n", "") + "\n").encode())
-            vmproc.stdin.flush()
-            backup_stdout = vmproc.stdin
-            self.processes_to_kill_on_cancel.append(vmproc)
+            backup_stdout = write_fd
         else:
             # Prepare the backup target (local file)
             if os.path.isdir(self.target_dir):
@@ -662,202 +803,80 @@ class Backup(object):
         # For this reason, we will use named pipes instead
         self.log.debug("Working in {}".format(self.tmpdir))
 
-        backup_pipe = os.path.join(self.tmpdir, "backup_pipe")
-        self.log.debug("Creating pipe in: {}".format(backup_pipe))
-        os.mkfifo(backup_pipe)
-
         self.log.debug("Will backup: {}".format(files_to_backup))
 
-        header_files = self.prepare_backup_header()
+        header_files = yield from self._prepare_backup_header()
 
         # Setup worker to send encrypted data chunks to the backup_target
-        to_send = Queue(10)
+        to_send = asyncio.Queue(10)
         send_proc = SendWorker(to_send, self.tmpdir, backup_stdout)
-        send_proc.start()
+        send_task = asyncio.ensure_future(send_proc.run())
+
+        vmproc_task = None
+        if vmproc is not None:
+            vmproc_task = asyncio.ensure_future(self._cancel_on_error(
+                self._monitor_process(vmproc,
+                    'Writing backup to VM {} failed'.format(
+                        self.target_vm.name)),
+                    send_task))
 
         for file_name in header_files:
-            to_send.put(file_name)
+            yield from to_send.put(file_name)
 
         qubes_xml_info = self.VMToBackup(
             None,
             [self.FileToBackup(qubes_xml, '')],
             ''
         )
-        for vm_info in itertools.chain([qubes_xml_info],
-                files_to_backup.values()):
-            for file_info in vm_info.files:
-
-                self.log.debug("Backing up {}".format(file_info))
-
-                backup_tempfile = os.path.join(
-                    self.tmpdir, file_info.subdir,
-                    file_info.name)
-                self.log.debug("Using temporary location: {}".format(
-                    backup_tempfile))
-
-                # Ensure the temporary directory exists
-                if not os.path.isdir(os.path.dirname(backup_tempfile)):
-                    os.makedirs(os.path.dirname(backup_tempfile))
+        inner_archive_task = asyncio.ensure_future(
+            self._wrap_and_send_files(
+                itertools.chain([qubes_xml_info], files_to_backup.values()),
+                to_send
+            ))
+        asyncio.ensure_future(
+            self._cancel_on_error(send_task, inner_archive_task))
 
-                # The first tar cmd can use any complex feature as we want.
-                # Files will be verified before untaring this.
-                # Prefix the path in archive with filename["subdir"] to have it
-                # verified during untar
-                tar_cmdline = (["tar", "-Pc", '--sparse',
-                               "-f", backup_pipe,
-                               '-C', os.path.dirname(file_info.path)] +
-                               (['--dereference'] if
-                                file_info.subdir != "dom0-home/" else []) +
-                               ['--xform=s:^%s:%s\\0:' % (
-                                   os.path.basename(file_info.path),
-                                   file_info.subdir),
-                                os.path.basename(file_info.path)
-                                ])
-                file_stat = os.stat(file_info.path)
-                if stat.S_ISBLK(file_stat.st_mode) or \
-                        file_info.name != os.path.basename(file_info.path):
-                    # tar doesn't handle content of block device, use our
-                    # writer
-                    # also use our tar writer when renaming file
-                    assert not stat.S_ISDIR(file_stat.st_mode),\
-                        "Renaming directories not supported"
-                    tar_cmdline = ['python3', '-m', 'qubes.tarwriter',
-                        '--override-name=%s' % (
-                            os.path.join(file_info.subdir, os.path.basename(
-                                file_info.name))),
-                        file_info.path,
-                        backup_pipe]
-                if self.compressed:
-                    tar_cmdline.insert(-2,
-                        "--use-compress-program=%s" % self.compression_filter)
-
-                self.log.debug(" ".join(tar_cmdline))
-
-                # Pipe: tar-sparse | scrypt | tar | backup_target
-                # TODO: log handle stderr
-                tar_sparse = subprocess.Popen(
-                    tar_cmdline)
-                self.processes_to_kill_on_cancel.append(tar_sparse)
-
-                # Wait for compressor (tar) process to finish or for any
-                # error of other subprocesses
-                i = 0
-                pipe = open(backup_pipe, 'rb')
-                run_error = "paused"
-                while run_error == "paused":
-                    # Prepare a first chunk
-                    chunkfile = backup_tempfile + ".%03d.enc" % i
-                    i += 1
-
-                    # Start encrypt, scrypt will also handle integrity
-                    # protection
-                    scrypt_passphrase = \
-                        u'{backup_id}!{filename}!{passphrase}'.format(
-                            backup_id=self.backup_id,
-                            filename=os.path.relpath(chunkfile[:-4],
-                                self.tmpdir),
-                            passphrase=self.passphrase)
-                    scrypt = launch_scrypt(
-                        "enc", "-", chunkfile, scrypt_passphrase)
-
-                    run_error = handle_streams(
-                        pipe,
-                        {'backup_target': scrypt.stdin},
-                        {'vmproc': vmproc,
-                         'addproc': tar_sparse,
-                         'scrypt': scrypt,
-                        },
-                        self.chunk_size,
-                        self._add_vm_progress
-                    )
-
-                    self.log.debug(
-                        "Wait_backup_feedback returned: {}".format(run_error))
-
-                    if self.canceled:
-                        try:
-                            tar_sparse.terminate()
-                        except OSError:
-                            pass
-                        tar_sparse.wait()
-                        to_send.put(QUEUE_ERROR)
-                        send_proc.join()
-                        shutil.rmtree(self.tmpdir)
-                        raise BackupCanceledError("Backup canceled")
-                    if run_error and run_error != "size_limit":
-                        send_proc.terminate()
-                        if run_error == "VM" and vmproc:
-                            raise qubes.exc.QubesException(
-                                "Failed to write the backup, VM output:\n" +
-                                vmproc.stderr.read(MAX_STDERR_BYTES))
-                        else:
-                            raise qubes.exc.QubesException(
-                                "Failed to perform backup: error in " +
-                                run_error)
-
-                    scrypt.stdin.close()
-                    scrypt.wait()
-                    self.log.debug("scrypt return code: {}".format(
-                        scrypt.poll()))
-
-                    # Send the chunk to the backup target
-                    self._queue_put_with_check(
-                        send_proc, vmproc, to_send,
-                        os.path.relpath(chunkfile, self.tmpdir))
-
-                    if tar_sparse.poll() is None or run_error == "size_limit":
-                        run_error = "paused"
-                    else:
-                        self.processes_to_kill_on_cancel.remove(tar_sparse)
-                        self.log.debug(
-                            "Finished tar sparse with exit code {}".format(
-                                tar_sparse.poll()))
-                pipe.close()
-
-            # This VM done, update progress
-            self._done_vms_bytes += vm_info.size
-            self._current_vm_bytes = 0
-            self._send_progress_update()
-            # Save date of last backup
-            if vm_info.vm:
-                vm_info.vm.backup_timestamp = datetime.datetime.now()
-
-        self._queue_put_with_check(send_proc, vmproc, to_send, QUEUE_FINISHED)
-        send_proc.join()
-        shutil.rmtree(self.tmpdir)
-
-        if self.canceled:
-            raise BackupCanceledError("Backup canceled")
-
-        if send_proc.exitcode != 0:
-            raise qubes.exc.QubesException(
-                "Failed to send backup: error in the sending process")
-
-        if vmproc:
-            self.log.debug("VMProc1 proc return code: {}".format(vmproc.poll()))
-            if tar_sparse is not None:
-                self.log.debug("Sparse1 proc return code: {}".format(
-                    tar_sparse.poll()))
-            vmproc.stdin.close()
+        try:
+            try:
+                yield from inner_archive_task
+            except:
+                yield from to_send.put(QUEUE_ERROR)
+                # in fact we may be handling CancelledError, induced by
+                # exception in send_task (and propagated by
+                # self._cancel_on_error call above); in such a case this
+                # yield from will raise exception, covering CancelledError -
+                # this is intended behaviour
+                yield from send_task
+                raise
+
+            yield from send_task
+
+        finally:
+            if isinstance(backup_stdout, int):
+                os.close(backup_stdout)
+            else:
+                backup_stdout.close()
+            if vmproc_task:
+                yield from vmproc_task
+            shutil.rmtree(self.tmpdir)
 
         self.app.save()
 
 
-def handle_streams(stream_in, streams_out, processes, size_limit=None,
+@asyncio.coroutine
+def handle_streams(stream_in, stream_out, size_limit=None,
         progress_callback=None):
     '''
     Copy stream_in to all streams_out and monitor all mentioned processes.
     If any of them terminate with non-zero code, interrupt the process. Copy
     at most `size_limit` data (if given).
 
-    :param stream_in: file-like object to read data from
-    :param streams_out: dict of file-like objects to write data to
-    :param processes: dict of subprocess.Popen objects to monitor
+    :param stream_in: StreamReader object to read data from
+    :param stream_out: StreamWriter object to write data to
     :param size_limit: int maximum data amount to process
     :param progress_callback: callable function to report progress, will be
         given copied data size (it should accumulate internally)
-    :return: failed process name, failed stream name, "size_limit" or None (
-        no error)
+    :return: "size_limit" or None (no error)
     '''
     buffer_size = 409600
     bytes_copied = 0
@@ -868,42 +887,14 @@ def handle_streams(stream_in, streams_out, processes, size_limit=None,
                 return "size_limit"
         else:
             to_copy = buffer_size
-        buf = stream_in.read(to_copy)
+        buf = yield from stream_in.read(to_copy)
         if not buf:
             # done
             return None
 
         if callable(progress_callback):
             progress_callback(len(buf))
-        for name, stream in streams_out.items():
-            if stream is None:
-                continue
-            try:
-                stream.write(buf)
-            except IOError:
-                return name
+        stream_out.write(buf)
         bytes_copied += len(buf)
 
-        for name, proc in processes.items():
-            if proc is None:
-                continue
-            if proc.poll():
-                return name
-
-
-def get_supported_hmac_algo(hmac_algorithm=None):
-    # Start with provided default
-    if hmac_algorithm:
-        yield hmac_algorithm
-    if hmac_algorithm != 'scrypt':
-        yield 'scrypt'
-    proc = subprocess.Popen(['openssl', 'list-message-digest-algorithms'],
-                            stdout=subprocess.PIPE)
-    for algo in proc.stdout.readlines():
-        algo = algo.decode('ascii')
-        if '=>' in algo:
-            continue
-        yield algo.strip()
-    proc.wait()
-
 # vim:sw=4:et: