| // Copyright 2016 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| #include "object/job_dispatcher.h" |
| |
| #include <err.h> |
| #include <inttypes.h> |
| #include <lib/counters.h> |
| #include <platform.h> |
| #include <zircon/rights.h> |
| #include <zircon/syscalls/policy.h> |
| |
| #include <fbl/alloc_checker.h> |
| #include <fbl/array.h> |
| #include <fbl/auto_lock.h> |
| #include <fbl/mutex.h> |
| #include <object/process_dispatcher.h> |
| |
| KCOUNTER(dispatcher_job_create_count, "dispatcher.job.create") |
| KCOUNTER(dispatcher_job_destroy_count, "dispatcher.job.destroy") |
| |
| // The starting max_height value of the root job. |
| static constexpr uint32_t kRootJobMaxHeight = 32; |
| |
| static constexpr char kRootJobName[] = "<superroot>"; |
| |
| template <> |
| uint32_t JobDispatcher::ChildCountLocked<JobDispatcher>() const { |
| return job_count_; |
| } |
| |
| template <> |
| uint32_t JobDispatcher::ChildCountLocked<ProcessDispatcher>() const { |
| return process_count_; |
| } |
| |
| // Calls the provided |zx_status_t func(fbl::RefPtr<DISPATCHER_TYPE>)| |
| // function on all live elements of |children|, which must be one of |jobs_| |
| // or |procs_|. Stops iterating early if |func| returns a value other than |
| // ZX_OK, returning that value from this method. |lock_| must be held when |
| // calling this method, and it will still be held while the callback is |
| // called. |
| // |
| // The returned |LiveRefsArray| needs to be destructed when |lock_| is not |
| // held anymore. The recommended pattern is: |
| // |
| // LiveRefsArray refs; |
| // { |
| // Guard<fbl::Mutex> guard{get_lock()}; |
| // refs = ForEachChildInLocked(...); |
| // } |
| // |
| template <typename T, typename Fn> |
| JobDispatcher::LiveRefsArray JobDispatcher::ForEachChildInLocked(T& children, zx_status_t* result, |
| Fn func) { |
| // Convert child raw pointers into RefPtrs. This is tricky and requires |
| // special logic on the RefPtr class to handle a ref count that can be |
| // zero. |
| // |
| // The main requirement is that |lock_| is both controlling child |
| // list lookup and also making sure that the child destructor cannot |
| // make progress when doing so. In other words, when inspecting the |
| // |children| list we can be sure that a given child process or child |
| // job is either |
| // - alive, with refcount > 0 |
| // - in destruction process but blocked, refcount == 0 |
| |
| const uint32_t count = ChildCountLocked<typename T::ValueType>(); |
| |
| if (!count) { |
| *result = ZX_OK; |
| return LiveRefsArray(); |
| } |
| |
| fbl::AllocChecker ac; |
| LiveRefsArray refs(new (&ac) fbl::RefPtr<Dispatcher>[count], count); |
| if (!ac.check()) { |
| *result = ZX_ERR_NO_MEMORY; |
| return LiveRefsArray(); |
| } |
| |
| size_t ix = 0; |
| |
| for (auto& craw : children) { |
| auto cref = ::fbl::MakeRefPtrUpgradeFromRaw(&craw, get_lock()); |
| if (!cref) |
| continue; |
| |
| *result = func(cref); |
| // |cref| might be the last reference at this point. If so, |
| // when we drop it in the next iteration the object dtor |
| // would be called here with the |get_lock()| held. To avoid that |
| // we keep the reference alive in the |refs| array and pass |
| // the responsibility of releasing them outside the lock to |
| // the caller. |
| refs[ix++] = ktl::move(cref); |
| |
| if (*result != ZX_OK) |
| break; |
| } |
| |
| return refs; |
| } |
| |
| fbl::RefPtr<JobDispatcher> JobDispatcher::CreateRootJob() { |
| fbl::AllocChecker ac; |
| auto job = fbl::AdoptRef(new (&ac) JobDispatcher(0u, nullptr, JobPolicy())); |
| if (!ac.check()) |
| return nullptr; |
| job->set_name(kRootJobName, sizeof(kRootJobName)); |
| return job; |
| } |
| |
| zx_status_t JobDispatcher::Create(uint32_t flags, fbl::RefPtr<JobDispatcher> parent, |
| KernelHandle<JobDispatcher>* handle, zx_rights_t* rights) { |
| if (parent != nullptr && parent->max_height() == 0) { |
| // The parent job cannot have children. |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| |
| fbl::AllocChecker ac; |
| KernelHandle new_handle( |
| fbl::AdoptRef(new (&ac) JobDispatcher(flags, parent, parent->GetPolicy()))); |
| if (!ac.check()) |
| return ZX_ERR_NO_MEMORY; |
| |
| if (!parent->AddChildJob(new_handle.dispatcher())) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| *rights = default_rights(); |
| *handle = ktl::move(new_handle); |
| return ZX_OK; |
| } |
| |
| JobDispatcher::JobDispatcher(uint32_t /*flags*/, fbl::RefPtr<JobDispatcher> parent, |
| JobPolicy policy) |
| : SoloDispatcher(ZX_JOB_NO_PROCESSES | ZX_JOB_NO_JOBS), |
| parent_(ktl::move(parent)), |
| max_height_(parent_ ? parent_->max_height() - 1 : kRootJobMaxHeight), |
| state_(State::READY), |
| process_count_(0u), |
| job_count_(0u), |
| return_code_(0), |
| kill_on_oom_(false), |
| policy_(policy), |
| exceptionate_(ExceptionPort::Type::JOB), |
| debug_exceptionate_(ExceptionPort::Type::JOB_DEBUGGER) { |
| kcounter_add(dispatcher_job_create_count, 1); |
| } |
| |
| JobDispatcher::~JobDispatcher() { |
| kcounter_add(dispatcher_job_destroy_count, 1); |
| RemoveFromJobTreesUnlocked(); |
| } |
| |
| zx_koid_t JobDispatcher::get_related_koid() const { return parent_ ? parent_->get_koid() : 0u; } |
| |
| bool JobDispatcher::AddChildProcess(const fbl::RefPtr<ProcessDispatcher>& process) { |
| canary_.Assert(); |
| |
| Guard<fbl::Mutex> guard{get_lock()}; |
| if (state_ != State::READY) |
| return false; |
| procs_.push_back(process.get()); |
| ++process_count_; |
| UpdateSignalsIncrementLocked(); |
| return true; |
| } |
| |
| bool JobDispatcher::AddChildJob(const fbl::RefPtr<JobDispatcher>& job) { |
| canary_.Assert(); |
| |
| Guard<fbl::Mutex> guard{get_lock()}; |
| |
| if (state_ != State::READY) |
| return false; |
| |
| // Put the new job after our next-youngest child, or us if we have none. |
| // |
| // We try to make older jobs closer to the root (both hierarchically and |
| // temporally) show up earlier in enumeration. |
| JobDispatcher* neighbor = (jobs_.is_empty() ? this : &jobs_.back()); |
| |
| // This can only be called once, the job should not already be part |
| // of any job tree. |
| DEBUG_ASSERT(!job->dll_job_raw_.InContainer()); |
| DEBUG_ASSERT(neighbor != job.get()); |
| |
| jobs_.push_back(job.get()); |
| ++job_count_; |
| UpdateSignalsIncrementLocked(); |
| return true; |
| } |
| |
| void JobDispatcher::RemoveChildProcess(ProcessDispatcher* process) { |
| canary_.Assert(); |
| |
| bool should_die = false; |
| { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| // The process dispatcher can call us in its destructor, Kill(), |
| // or RemoveThread(). |
| if (!ProcessDispatcher::JobListTraitsRaw::node_state(*process).InContainer()) |
| return; |
| procs_.erase(*process); |
| --process_count_; |
| UpdateSignalsDecrementLocked(); |
| should_die = IsReadyForDeadTransitionLocked(); |
| } |
| |
| if (should_die) |
| FinishDeadTransitionUnlocked(); |
| } |
| |
| void JobDispatcher::RemoveChildJob(JobDispatcher* job) { |
| canary_.Assert(); |
| |
| bool should_die = false; |
| { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| if (!JobDispatcher::ListTraitsRaw::node_state(*job).InContainer()) |
| return; |
| jobs_.erase(*job); |
| --job_count_; |
| UpdateSignalsDecrementLocked(); |
| should_die = IsReadyForDeadTransitionLocked(); |
| } |
| |
| if (should_die) |
| FinishDeadTransitionUnlocked(); |
| } |
| |
| JobDispatcher::State JobDispatcher::GetState() const { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| return state_; |
| } |
| |
| void JobDispatcher::RemoveFromJobTreesUnlocked() { |
| canary_.Assert(); |
| |
| if (parent_) |
| parent_->RemoveChildJob(this); |
| } |
| |
| bool JobDispatcher::IsReadyForDeadTransitionLocked() { |
| canary_.Assert(); |
| return state_ == State::KILLING && job_count_ == 0 && process_count_ == 0; |
| } |
| |
| void JobDispatcher::FinishDeadTransitionUnlocked() { |
| canary_.Assert(); |
| |
| // Make sure we're killing from the bottom of the tree up or else parent |
| // jobs could die before their children. |
| // |
| // In particular, this means we have to finish dying before leaving the job |
| // trees, since the last child leaving the tree can trigger its parent to |
| // finish dying. |
| DEBUG_ASSERT(!parent_ || (parent_->GetState() != State::DEAD)); |
| { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| state_ = State::DEAD; |
| exceptionate_.Shutdown(); |
| debug_exceptionate_.Shutdown(); |
| UpdateStateLocked(0u, ZX_TASK_TERMINATED); |
| } |
| |
| RemoveFromJobTreesUnlocked(); |
| } |
| |
| void JobDispatcher::UpdateSignalsDecrementLocked() { |
| canary_.Assert(); |
| |
| DEBUG_ASSERT(get_lock()->lock().IsHeld()); |
| |
| // removing jobs or processes. |
| zx_signals_t set = 0u; |
| if (process_count_ == 0u) { |
| DEBUG_ASSERT(procs_.is_empty()); |
| set |= ZX_JOB_NO_PROCESSES; |
| } |
| if (job_count_ == 0u) { |
| DEBUG_ASSERT(jobs_.is_empty()); |
| set |= ZX_JOB_NO_JOBS; |
| } |
| |
| if (!parent_ && (job_count_ == 0) && (process_count_ == 0)) { |
| // There are no userspace process left. From here, there's |
| // no particular context as to whether this was |
| // intentional, or if a core devhost crashed due to a |
| // bug. Either way, shut down the kernel. |
| platform_halt(HALT_ACTION_HALT, HALT_REASON_SW_RESET); |
| } |
| |
| UpdateStateLocked(0u, set); |
| } |
| |
| void JobDispatcher::UpdateSignalsIncrementLocked() { |
| canary_.Assert(); |
| |
| DEBUG_ASSERT(get_lock()->lock().IsHeld()); |
| |
| // Adding jobs or processes. |
| zx_signals_t clear = 0u; |
| if (process_count_ == 1u) { |
| DEBUG_ASSERT(!procs_.is_empty()); |
| clear |= ZX_JOB_NO_PROCESSES; |
| } |
| if (job_count_ == 1u) { |
| DEBUG_ASSERT(!jobs_.is_empty()); |
| clear |= ZX_JOB_NO_JOBS; |
| } |
| UpdateStateLocked(clear, 0u); |
| } |
| |
| JobPolicy JobDispatcher::GetPolicy() const { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| return policy_; |
| } |
| |
| bool JobDispatcher::KillJobWithKillOnOOM() { |
| // Get list of jobs with kill bit set. |
| OOMBitJobArray oom_jobs; |
| int count = 0; |
| CollectJobsWithOOMBit(&oom_jobs, &count); |
| if (count == 0) { |
| printf("OOM: no jobs with kill_on_oom found\n"); |
| return false; |
| } |
| |
| // Sort by max height. TODO(scottmg): This is not stable, which makes the |
| // ordering unpredictable in theory. We don't currently have a stable sort in |
| // kernel. |
| qsort(reinterpret_cast<void*>(oom_jobs.begin()), count, sizeof(oom_jobs[0]), |
| [](const void* a, const void* b) -> int { |
| const auto& ta = *reinterpret_cast<const fbl::RefPtr<JobDispatcher>*>(a); |
| const auto& tb = *reinterpret_cast<const fbl::RefPtr<JobDispatcher>*>(b); |
| return ta->max_height() < tb->max_height(); |
| }); |
| |
| // Kill lowest to highest until we find something to kill. |
| for (int i = count - 1; i >= 0; --i) { |
| auto& job = oom_jobs[i]; |
| if (job->Kill(ZX_TASK_RETCODE_OOM_KILL)) { |
| char name[ZX_MAX_NAME_LEN]; |
| job->get_name(name); |
| printf("OOM: killing %" PRIu64 " '%s'\n", job->get_koid(), name); |
| return true; |
| } |
| } |
| |
| printf("OOM: no job found to kill\n"); |
| return false; |
| } |
| |
| void JobDispatcher::CollectJobsWithOOMBit(OOMBitJobArray* into, int* count) { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| |
| if (kill_on_oom_) { |
| if (*count >= static_cast<int>(into->size())) { |
| printf("OOM: skipping some jobs, exceeded max count\n"); |
| return; |
| } |
| |
| auto cref = ::fbl::MakeRefPtrUpgradeFromRaw(this, get_lock()); |
| if (!cref) |
| return; |
| (*into)[*count] = ktl::move(cref); |
| *count += 1; |
| } |
| |
| for (auto& job : jobs_) { |
| job.CollectJobsWithOOMBit(into, count); |
| } |
| } |
| |
| bool JobDispatcher::Kill(int64_t return_code) { |
| canary_.Assert(); |
| |
| JobList jobs_to_kill; |
| ProcessList procs_to_kill; |
| |
| LiveRefsArray jobs_refs; |
| LiveRefsArray proc_refs; |
| |
| bool should_die = false; |
| { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| if (state_ != State::READY) |
| return false; |
| |
| return_code_ = return_code; |
| state_ = State::KILLING; |
| zx_status_t result; |
| |
| // Safely gather refs to the children. |
| jobs_refs = ForEachChildInLocked(jobs_, &result, [&](fbl::RefPtr<JobDispatcher> job) { |
| jobs_to_kill.push_front(ktl::move(job)); |
| return ZX_OK; |
| }); |
| proc_refs = ForEachChildInLocked(procs_, &result, [&](fbl::RefPtr<ProcessDispatcher> proc) { |
| procs_to_kill.push_front(ktl::move(proc)); |
| return ZX_OK; |
| }); |
| |
| should_die = IsReadyForDeadTransitionLocked(); |
| } |
| |
| if (should_die) |
| FinishDeadTransitionUnlocked(); |
| |
| // Since we kill the child jobs first we have a depth-first massacre. |
| while (!jobs_to_kill.is_empty()) { |
| // TODO(cpu): This recursive call can overflow the stack. |
| jobs_to_kill.pop_front()->Kill(return_code); |
| } |
| |
| while (!procs_to_kill.is_empty()) { |
| procs_to_kill.pop_front()->Kill(return_code); |
| } |
| |
| return true; |
| } |
| |
| bool JobDispatcher::CanSetPolicy() TA_REQ(get_lock()) { |
| // Can't set policy when there are active processes or jobs. This constraint ensures that a |
| // process's policy cannot change over its lifetime. Because a process's policy cannot change, |
| // the risk of TOCTOU bugs is reduced and we are free to apply policy at the ProcessDispatcher |
| // without having to walk up the tree to its containing job. |
| if (!procs_.is_empty() || !jobs_.is_empty()) { |
| return false; |
| } |
| return true; |
| } |
| |
| zx_status_t JobDispatcher::SetBasicPolicy(uint32_t mode, const zx_policy_basic* in_policy, |
| size_t policy_count) { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| |
| if (!CanSetPolicy()) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| auto status = policy_.AddBasicPolicy(mode, in_policy, policy_count); |
| |
| if (status != ZX_OK) |
| return status; |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t JobDispatcher::SetTimerSlackPolicy(const zx_policy_timer_slack& policy) { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| |
| if (!CanSetPolicy()) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| // Is the policy valid? |
| if (policy.min_slack < 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| slack_mode new_mode; |
| switch (policy.default_mode) { |
| case ZX_TIMER_SLACK_CENTER: |
| new_mode = TIMER_SLACK_CENTER; |
| break; |
| case ZX_TIMER_SLACK_EARLY: |
| new_mode = TIMER_SLACK_EARLY; |
| break; |
| case ZX_TIMER_SLACK_LATE: |
| new_mode = TIMER_SLACK_LATE; |
| break; |
| default: |
| return ZX_ERR_INVALID_ARGS; |
| }; |
| |
| const TimerSlack old_slack = policy_.GetTimerSlack(); |
| const zx_duration_t new_amount = fbl::max(old_slack.amount(), policy.min_slack); |
| const TimerSlack new_slack(new_amount, new_mode); |
| |
| policy_.SetTimerSlack(new_slack); |
| |
| return ZX_OK; |
| } |
| |
| bool JobDispatcher::EnumerateChildren(JobEnumerator* je, bool recurse) { |
| canary_.Assert(); |
| |
| LiveRefsArray jobs_refs; |
| LiveRefsArray proc_refs; |
| |
| zx_status_t result = ZX_OK; |
| |
| { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| |
| proc_refs = ForEachChildInLocked(procs_, &result, [&](fbl::RefPtr<ProcessDispatcher> proc) { |
| return je->OnProcess(proc.get()) ? ZX_OK : ZX_ERR_STOP; |
| }); |
| if (result != ZX_OK) { |
| return false; |
| } |
| |
| jobs_refs = ForEachChildInLocked(jobs_, &result, [&](fbl::RefPtr<JobDispatcher> job) { |
| if (!je->OnJob(job.get())) { |
| return ZX_ERR_STOP; |
| } |
| if (recurse) { |
| // TODO(kulakowski): This recursive call can overflow the stack. |
| return job->EnumerateChildren(je, /* recurse */ true) ? ZX_OK : ZX_ERR_STOP; |
| } |
| return ZX_OK; |
| }); |
| } |
| |
| return result == ZX_OK; |
| } |
| |
| fbl::RefPtr<ProcessDispatcher> JobDispatcher::LookupProcessById(zx_koid_t koid) { |
| canary_.Assert(); |
| |
| LiveRefsArray proc_refs; |
| |
| fbl::RefPtr<ProcessDispatcher> found_proc; |
| { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| zx_status_t result; |
| |
| proc_refs = ForEachChildInLocked(procs_, &result, [&](fbl::RefPtr<ProcessDispatcher> proc) { |
| if (proc->get_koid() == koid) { |
| found_proc = ktl::move(proc); |
| return ZX_ERR_STOP; |
| } |
| return ZX_OK; |
| }); |
| } |
| return found_proc; // Null if not found. |
| } |
| |
| fbl::RefPtr<JobDispatcher> JobDispatcher::LookupJobById(zx_koid_t koid) { |
| canary_.Assert(); |
| |
| LiveRefsArray jobs_refs; |
| |
| fbl::RefPtr<JobDispatcher> found_job; |
| { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| zx_status_t result; |
| |
| jobs_refs = ForEachChildInLocked(jobs_, &result, [&](fbl::RefPtr<JobDispatcher> job) { |
| if (job->get_koid() == koid) { |
| found_job = ktl::move(job); |
| return ZX_ERR_STOP; |
| } |
| return ZX_OK; |
| }); |
| } |
| return found_job; // Null if not found. |
| } |
| |
| void JobDispatcher::get_name(char out_name[ZX_MAX_NAME_LEN]) const { |
| canary_.Assert(); |
| |
| name_.get(ZX_MAX_NAME_LEN, out_name); |
| } |
| |
| zx_status_t JobDispatcher::set_name(const char* name, size_t len) { |
| canary_.Assert(); |
| |
| return name_.set(name, len); |
| } |
| |
| zx_status_t JobDispatcher::SetExceptionPort(fbl::RefPtr<ExceptionPort> eport) { |
| canary_.Assert(); |
| bool debugger = false; |
| switch (eport->type()) { |
| case ExceptionPort::Type::JOB_DEBUGGER: |
| debugger = true; |
| break; |
| case ExceptionPort::Type::JOB: |
| break; |
| default: |
| DEBUG_ASSERT_MSG(false, "unexpected port type: %d", static_cast<int>(eport->type())); |
| break; |
| } |
| |
| Guard<fbl::Mutex> guard{get_lock()}; |
| if (debugger) { |
| if (debugger_exception_port_) |
| return ZX_ERR_ALREADY_BOUND; |
| debugger_exception_port_ = ktl::move(eport); |
| } else { |
| if (exception_port_) |
| return ZX_ERR_ALREADY_BOUND; |
| exception_port_ = ktl::move(eport); |
| } |
| return ZX_OK; |
| } |
| |
| class OnExceptionPortRemovalEnumerator final : public JobEnumerator { |
| public: |
| OnExceptionPortRemovalEnumerator(fbl::RefPtr<ExceptionPort> eport) : eport_(ktl::move(eport)) {} |
| OnExceptionPortRemovalEnumerator(const OnExceptionPortRemovalEnumerator&) = delete; |
| |
| private: |
| bool OnProcess(ProcessDispatcher* process) override { |
| process->OnExceptionPortRemoval(eport_); |
| // Keep looking. |
| return true; |
| } |
| |
| fbl::RefPtr<ExceptionPort> eport_; |
| }; |
| |
| bool JobDispatcher::ResetExceptionPort(bool debugger) { |
| canary_.Assert(); |
| |
| fbl::RefPtr<ExceptionPort> eport; |
| { |
| Guard<fbl::Mutex> lock{get_lock()}; |
| if (debugger) { |
| debugger_exception_port_.swap(eport); |
| } else { |
| exception_port_.swap(eport); |
| } |
| if (eport == nullptr) { |
| // Attempted to unbind when no exception port is bound. |
| return false; |
| } |
| // This method must guarantee that no caller will return until |
| // OnTargetUnbind has been called on the port-to-unbind. |
| // This becomes important when a manual unbind races with a |
| // PortDispatcher::on_zero_handles auto-unbind. |
| // |
| // If OnTargetUnbind were called outside of the lock, it would lead to |
| // a race (for threads A and B): |
| // |
| // A: Calls ResetExceptionPort; acquires the lock |
| // A: Sees a non-null exception_port_, swaps it into the eport local. |
| // exception_port_ is now null. |
| // A: Releases the lock |
| // |
| // B: Calls ResetExceptionPort; acquires the lock |
| // B: Sees a null exception_port_ and returns. But OnTargetUnbind() |
| // hasn't yet been called for the port. |
| // |
| // So, call it before releasing the lock. |
| eport->OnTargetUnbind(); |
| } |
| |
| OnExceptionPortRemovalEnumerator remover(eport); |
| if (!EnumerateChildren(&remover, true)) { |
| DEBUG_ASSERT(false); |
| } |
| return true; |
| } |
| |
| fbl::RefPtr<ExceptionPort> JobDispatcher::exception_port() { |
| Guard<fbl::Mutex> lock{get_lock()}; |
| return exception_port_; |
| } |
| |
| fbl::RefPtr<ExceptionPort> JobDispatcher::debugger_exception_port() { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| return debugger_exception_port_; |
| } |
| |
| Exceptionate* JobDispatcher::exceptionate(Exceptionate::Type type) { |
| canary_.Assert(); |
| return type == Exceptionate::Type::kDebug ? &debug_exceptionate_ : &exceptionate_; |
| } |
| |
| void JobDispatcher::set_kill_on_oom(bool value) { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| kill_on_oom_ = value; |
| } |
| |
| bool JobDispatcher::get_kill_on_oom() const { |
| Guard<fbl::Mutex> guard{get_lock()}; |
| return kill_on_oom_; |
| } |
| |
| void JobDispatcher::GetInfo(zx_info_job_t* info) const { |
| canary_.Assert(); |
| |
| Guard<fbl::Mutex> guard{get_lock()}; |
| info->return_code = return_code_; |
| info->exited = (state_ == State::DEAD); |
| info->kill_on_oom = kill_on_oom_; |
| info->debugger_attached = debugger_exception_port_ != nullptr; |
| } |