'Called to indicate that the associated command has been run. Updates
the output datasets based on stderr and stdout from the command, and
the contents of the output files.'

def finish(self, stdout, stderr, tool_exit_code=None, remote_working_directory=None, remote_metadata_directory=None):

    finish_timer = util.ExecutionTimer()
    self.sa_session.expunge_all()
    job = self.get_job()
    try:
        self.reclaim_ownership()
    except:
        log.exception(('(%s)    Failed    to    change    ownership    of    %s,    failing' % (job.id, self.working_directory)))
        return self.fail(job.info, stdout=stdout, stderr=stderr, exit_code=tool_exit_code)
    if ((job.state == job.states.DELETED) or (job.state == job.states.ERROR)):
        return self.fail(job.info, stderr=stderr, stdout=stdout, exit_code=tool_exit_code)
    if self.check_tool_output(stdout, stderr, tool_exit_code, job):
        final_job_state = job.states.OK
    else:
        final_job_state = job.states.ERROR
    if self.tool.version_string_cmd:
        version_filename = self.get_version_string_path()
        if os.path.exists(version_filename):
            self.version_string = open(version_filename).read()
            os.unlink(version_filename)
    outputs_to_working_directory = util.asbool(self.get_destination_configuration('outputs_to_working_directory', False))
    if (outputs_to_working_directory and (not self.__link_file_check())):
        for dataset_path in self.get_output_fnames():
            try:
                shutil.move(dataset_path.false_path, dataset_path.real_path)
                log.debug(('finish():    Moved    %s    to    %s' % (dataset_path.false_path, dataset_path.real_path)))
            except (IOError, OSError):
                if (os.path.exists(dataset_path.real_path) and (os.stat(dataset_path.real_path).st_size > 0)):
                    log.warning(('finish():    %s    not    found,    but    %s    is    not    empty,    so    it    will    be    used    instead' % (dataset_path.false_path, dataset_path.real_path)))
                else:
                    job.set_state(final_job_state)
                    return self.fail(("Job    %s's    output    dataset(s)    could    not    be    read" % job.id))
    job_context = ExpressionContext(dict(stdout=job.stdout, stderr=job.stderr))
    for dataset_assoc in (job.output_datasets + job.output_library_datasets):
        context = self.get_dataset_finish_context(job_context, dataset_assoc.dataset.dataset)
        for dataset in (dataset_assoc.dataset.dataset.history_associations + dataset_assoc.dataset.dataset.library_associations):
            purged = dataset.dataset.purged
            if ((not purged) and (dataset.dataset.external_filename is None)):
                trynum = 0
                while (trynum < self.app.config.retry_job_output_collection):
                    try:
                        os.stat(dataset.dataset.file_name)
                        os.chown(dataset.dataset.file_name, os.getuid(), (-1))
                        trynum = self.app.config.retry_job_output_collection
                    except (OSError, ObjectNotFound) as e:
                        trynum += 1
                        log.warning('Error    accessing    %s,    will    retry:    %s', dataset.dataset.file_name, e)
                        time.sleep(2)
            if getattr(dataset, 'hidden_beneath_collection_instance', None):
                dataset.visible = False
            dataset.blurb = 'done'
            dataset.peek = 'no    peek'
            dataset.info = (dataset.info or '')
            if context['stdout'].strip():
                dataset.info = ((dataset.info.rstrip() + '\n') + context['stdout'].strip())
            if context['stderr'].strip():
                dataset.info = ((dataset.info.rstrip() + '\n') + context['stderr'].strip())
            dataset.tool_version = self.version_string
            dataset.set_size()
            if ('uuid' in context):
                dataset.dataset.uuid = context['uuid']
            if (dataset not in job.output_library_datasets):
                self.app.object_store.update_from_file(dataset.dataset, create=True)
            self.__update_output(job, dataset)
            if (not purged):
                self._collect_extra_files(dataset.dataset, self.working_directory)
            if ((dataset.datatype.composite_type == 'auto_primary_file') and (not dataset.has_data())):
                try:
                    with NamedTemporaryFile() as temp_fh:
                        temp_fh.write(dataset.datatype.generate_primary_file(dataset))
                        temp_fh.flush()
                        self.app.object_store.update_from_file(dataset.dataset, file_name=temp_fh.name, create=True)
                        dataset.set_size()
                except Exception as e:
                    log.warning('Unable    to    generate    primary    composite    file    automatically    for    %s:    %s', dataset.dataset.id, e)
            if (job.states.ERROR == final_job_state):
                dataset.blurb = 'error'
                dataset.mark_unhidden()
            elif ((not purged) and dataset.has_data()):
                if (dataset.ext == 'auto'):
                    dataset.extension = context.get('ext', 'data')
                    dataset.init_meta(copy_from=dataset)
                retry_internally = util.asbool(self.get_destination_configuration('retry_metadata_internally', True))
                if (retry_internally and (not self.external_output_metadata.external_metadata_set_successfully(dataset, self.sa_session))):
                    if (dataset.ext == '_sniff_'):
                        extension = sniff.handle_uploaded_dataset_file(dataset.dataset.file_name, self.app.datatypes_registry)
                        dataset.extension = extension
                    dataset.datatype.set_meta(dataset, overwrite=False)
                elif ((job.states.ERROR != final_job_state) and (not self.external_output_metadata.external_metadata_set_successfully(dataset, self.sa_session))):
                    dataset._state = model.Dataset.states.FAILED_METADATA
                else:
                    output_filename = self.external_output_metadata.get_output_filenames_by_dataset(dataset, self.sa_session).filename_out
                    def path_rewriter(path):
                        if (not path):
                            return path
                        normalized_remote_working_directory = (remote_working_directory and os.path.normpath(remote_working_directory))
                        normalized_remote_metadata_directory = (remote_metadata_directory and os.path.normpath(remote_metadata_directory))
                        normalized_path = os.path.normpath(path)
                        if (remote_working_directory and normalized_path.startswith(normalized_remote_working_directory)):
                            return normalized_path.replace(normalized_remote_working_directory, self.working_directory, 1)
                        if (remote_metadata_directory and normalized_path.startswith(normalized_remote_metadata_directory)):
                            return normalized_path.replace(normalized_remote_metadata_directory, self.working_directory, 1)
                        return path
                    dataset.metadata.from_JSON_dict(output_filename, path_rewriter=path_rewriter)
                try:
                    assert (context.get('line_count', None) is not None)
                    if (((not dataset.datatype.composite_type) and dataset.dataset.is_multi_byte()) or self.tool.is_multi_byte):
                        dataset.set_peek(line_count=context['line_count'], is_multi_byte=True)
                    else:
                        dataset.set_peek(line_count=context['line_count'])
                except:
                    if (((not dataset.datatype.composite_type) and dataset.dataset.is_multi_byte()) or self.tool.is_multi_byte):
                        dataset.set_peek(is_multi_byte=True)
                    else:
                        dataset.set_peek()
                for context_key in ['name', 'info', 'dbkey']:
                    if (context_key in context):
                        context_value = context[context_key]
                        setattr(dataset, context_key, context_value)
            else:
                dataset.blurb = 'empty'
                if (dataset.ext == 'auto'):
                    dataset.extension = 'txt'
            self.sa_session.add(dataset)
        if (job.states.ERROR == final_job_state):
            log.debug('(%s)    setting    dataset    %s    state    to    ERROR', job.id, dataset_assoc.dataset.dataset.id)
            dataset_assoc.dataset.dataset.state = model.Dataset.states.ERROR
            for dep_job_assoc in dataset_assoc.dataset.dependent_jobs:
                self.pause(dep_job_assoc.job, "Execution    of    this    dataset's    job    is    paused    because    its    input    datasets    are    in    an    error    state.")
        else:
            dataset_assoc.dataset.dataset.state = model.Dataset.states.OK
    for pja in job.post_job_actions:
        ActionBox.execute(self.app, self.sa_session, pja.post_job_action, job)
    self.sa_session.flush()
    job.set_streams(job.stdout, job.stderr)
    if (tool_exit_code is not None):
        job.exit_code = tool_exit_code
    inp_data = dict([(da.name, da.dataset) for da in job.input_datasets])
    out_data = dict([(da.name, da.dataset) for da in job.output_datasets])
    inp_data.update([(da.name, da.dataset) for da in job.input_library_datasets])
    out_data.update([(da.name, da.dataset) for da in job.output_library_datasets])
    out_collections = dict([(obj.name, obj.dataset_collection_instance) for obj in job.output_dataset_collection_instances])
    out_collections.update([(obj.name, obj.dataset_collection) for obj in job.output_dataset_collections])
    input_ext = 'data'
    input_dbkey = '?'
    for (_, data) in inp_data.items():
        if (not data):
            continue
        input_ext = data.ext
        input_dbkey = (data.dbkey or '?')
    param_dict = dict([(p.name, p.value) for p in job.parameters])
    param_dict = self.tool.params_from_strings(param_dict, self.app)
    tool_working_directory = self.tool_working_directory
    if (not os.path.exists(tool_working_directory)):
        tool_working_directory = self.working_directory
    collected_datasets = {'children': self.tool.collect_child_datasets(out_data, tool_working_directory), 'primary': self.tool.collect_primary_datasets(out_data, tool_working_directory, input_ext, input_dbkey)}
    self.tool.collect_dynamic_collections(out_collections, job_working_directory=tool_working_directory, inp_data=inp_data, job=job, input_dbkey=input_dbkey)
    param_dict.update({'__collected_datasets__': collected_datasets})
    self.tool.exec_after_process(self.queue.app, inp_data, out_data, param_dict, job=job)
    self.tool.call_hook('exec_after_process', self.queue.app, inp_data=inp_data, out_data=out_data, param_dict=param_dict, tool=self.tool, stdout=job.stdout, stderr=job.stderr)
    job.command_line = unicodify(self.command_line)
    collected_bytes = 0
    for dataset_assoc in job.output_datasets:
        if (not dataset_assoc.dataset.dataset.purged):
            dataset_assoc.dataset.dataset.set_total_size()
            collected_bytes += dataset_assoc.dataset.dataset.get_total_size()
    if job.user:
        job.user.adjust_total_disk_usage(collected_bytes)
    self.sa_session.flush()
    for path in [dp.real_path for dp in self.get_mutable_output_fnames()]:
        if os.path.exists(path):
            util.umask_fix_perms(path, self.app.config.umask, 438, self.app.config.gid)
    job.set_final_state(final_job_state)
    if (not job.tasks):
        self._collect_metrics(job)
    self.sa_session.flush()
    log.debug(('job    %d    ended    (finish()    executed    in    %s)' % (self.job_id, finish_timer)))
    if (job.state == job.states.ERROR):
        self._report_error()
    cleanup_job = self.cleanup_job
    delete_files = ((cleanup_job == 'always') or ((job.state == job.states.OK) and (cleanup_job == 'onsuccess')))
    self.cleanup(delete_files=delete_files)
