| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
| file Copyright.txt or https://cmake.org/licensing for details. */ |
| #include "cmUVProcessChain.h" |
| |
| #include <array> |
| #include <csignal> |
| #include <cstdio> |
| #include <istream> // IWYU pragma: keep |
| #include <utility> |
| |
| #include <cm/memory> |
| |
| #include <cm3p/uv.h> |
| |
| #include "cmGetPipes.h" |
| #include "cmUVHandlePtr.h" |
| |
| struct cmUVProcessChain::InternalData |
| { |
| struct StreamData |
| { |
| int BuiltinStream = -1; |
| uv_stdio_container_t Stdio; |
| }; |
| |
| struct ProcessData |
| { |
| cmUVProcessChain::InternalData* Data; |
| cm::uv_process_ptr Process; |
| cm::uv_pipe_ptr InputPipe; |
| cm::uv_pipe_ptr OutputPipe; |
| Status ProcessStatus; |
| |
| void Finish(); |
| }; |
| |
| const cmUVProcessChainBuilder* Builder = nullptr; |
| |
| bool Valid = false; |
| |
| cm::uv_loop_ptr Loop; |
| |
| StreamData InputStreamData; |
| StreamData OutputStreamData; |
| StreamData ErrorStreamData; |
| cm::uv_pipe_ptr TempOutputPipe; |
| cm::uv_pipe_ptr TempErrorPipe; |
| |
| unsigned int ProcessesCompleted = 0; |
| std::vector<std::unique_ptr<ProcessData>> Processes; |
| |
| bool Prepare(const cmUVProcessChainBuilder* builder); |
| void SpawnProcess( |
| std::size_t index, |
| const cmUVProcessChainBuilder::ProcessConfiguration& config, bool first, |
| bool last); |
| void Finish(); |
| }; |
| |
| cmUVProcessChainBuilder::cmUVProcessChainBuilder() |
| { |
| this->SetNoStream(Stream_INPUT) |
| .SetNoStream(Stream_OUTPUT) |
| .SetNoStream(Stream_ERROR); |
| } |
| |
| cmUVProcessChainBuilder& cmUVProcessChainBuilder::AddCommand( |
| const std::vector<std::string>& arguments) |
| { |
| if (!arguments.empty()) { |
| this->Processes.emplace_back(); |
| this->Processes.back().Arguments = arguments; |
| } |
| return *this; |
| } |
| |
| cmUVProcessChainBuilder& cmUVProcessChainBuilder::SetNoStream(Stream stdio) |
| { |
| switch (stdio) { |
| case Stream_INPUT: |
| case Stream_OUTPUT: |
| case Stream_ERROR: { |
| auto& streamData = this->Stdio[stdio]; |
| streamData.Type = None; |
| break; |
| } |
| } |
| return *this; |
| } |
| |
| cmUVProcessChainBuilder& cmUVProcessChainBuilder::SetBuiltinStream( |
| Stream stdio) |
| { |
| switch (stdio) { |
| case Stream_INPUT: |
| // FIXME |
| break; |
| |
| case Stream_OUTPUT: |
| case Stream_ERROR: { |
| auto& streamData = this->Stdio[stdio]; |
| streamData.Type = Builtin; |
| break; |
| } |
| } |
| return *this; |
| } |
| |
| cmUVProcessChainBuilder& cmUVProcessChainBuilder::SetExternalStream( |
| Stream stdio, int fd) |
| { |
| switch (stdio) { |
| case Stream_INPUT: |
| case Stream_OUTPUT: |
| case Stream_ERROR: { |
| auto& streamData = this->Stdio[stdio]; |
| streamData.Type = External; |
| streamData.FileDescriptor = fd; |
| break; |
| } |
| } |
| return *this; |
| } |
| |
| cmUVProcessChainBuilder& cmUVProcessChainBuilder::SetMergedBuiltinStreams() |
| { |
| this->MergedBuiltinStreams = true; |
| return this->SetBuiltinStream(Stream_OUTPUT).SetBuiltinStream(Stream_ERROR); |
| } |
| |
| cmUVProcessChainBuilder& cmUVProcessChainBuilder::SetWorkingDirectory( |
| std::string dir) |
| { |
| this->WorkingDirectory = std::move(dir); |
| return *this; |
| } |
| |
| cmUVProcessChain cmUVProcessChainBuilder::Start() const |
| { |
| cmUVProcessChain chain; |
| |
| if (!chain.Data->Prepare(this)) { |
| return chain; |
| } |
| |
| for (std::size_t i = 0; i < this->Processes.size(); i++) { |
| chain.Data->SpawnProcess(i, this->Processes[i], i == 0, |
| i == this->Processes.size() - 1); |
| } |
| |
| chain.Data->Finish(); |
| |
| return chain; |
| } |
| |
| bool cmUVProcessChain::InternalData::Prepare( |
| const cmUVProcessChainBuilder* builder) |
| { |
| this->Builder = builder; |
| |
| auto const& input = |
| this->Builder->Stdio[cmUVProcessChainBuilder::Stream_INPUT]; |
| auto& inputData = this->InputStreamData; |
| switch (input.Type) { |
| case cmUVProcessChainBuilder::None: |
| inputData.Stdio.flags = UV_IGNORE; |
| break; |
| |
| case cmUVProcessChainBuilder::Builtin: { |
| // FIXME |
| break; |
| } |
| |
| case cmUVProcessChainBuilder::External: |
| inputData.Stdio.flags = UV_INHERIT_FD; |
| inputData.Stdio.data.fd = input.FileDescriptor; |
| break; |
| } |
| |
| auto const& error = |
| this->Builder->Stdio[cmUVProcessChainBuilder::Stream_ERROR]; |
| auto& errorData = this->ErrorStreamData; |
| switch (error.Type) { |
| case cmUVProcessChainBuilder::None: |
| errorData.Stdio.flags = UV_IGNORE; |
| break; |
| |
| case cmUVProcessChainBuilder::Builtin: { |
| int pipeFd[2]; |
| if (cmGetPipes(pipeFd) < 0) { |
| return false; |
| } |
| |
| errorData.BuiltinStream = pipeFd[0]; |
| errorData.Stdio.flags = UV_INHERIT_FD; |
| errorData.Stdio.data.fd = pipeFd[1]; |
| |
| if (this->TempErrorPipe.init(*this->Loop, 0) < 0) { |
| return false; |
| } |
| if (uv_pipe_open(this->TempErrorPipe, errorData.Stdio.data.fd) < 0) { |
| return false; |
| } |
| |
| break; |
| } |
| |
| case cmUVProcessChainBuilder::External: |
| errorData.Stdio.flags = UV_INHERIT_FD; |
| errorData.Stdio.data.fd = error.FileDescriptor; |
| break; |
| } |
| |
| auto const& output = |
| this->Builder->Stdio[cmUVProcessChainBuilder::Stream_OUTPUT]; |
| auto& outputData = this->OutputStreamData; |
| switch (output.Type) { |
| case cmUVProcessChainBuilder::None: |
| outputData.Stdio.flags = UV_IGNORE; |
| break; |
| |
| case cmUVProcessChainBuilder::Builtin: |
| if (this->Builder->MergedBuiltinStreams) { |
| outputData.BuiltinStream = errorData.BuiltinStream; |
| outputData.Stdio.flags = UV_INHERIT_FD; |
| outputData.Stdio.data.fd = errorData.Stdio.data.fd; |
| } else { |
| int pipeFd[2]; |
| if (cmGetPipes(pipeFd) < 0) { |
| return false; |
| } |
| |
| outputData.BuiltinStream = pipeFd[0]; |
| outputData.Stdio.flags = UV_INHERIT_FD; |
| outputData.Stdio.data.fd = pipeFd[1]; |
| |
| if (this->TempOutputPipe.init(*this->Loop, 0) < 0) { |
| return false; |
| } |
| if (uv_pipe_open(this->TempOutputPipe, outputData.Stdio.data.fd) < 0) { |
| return false; |
| } |
| } |
| break; |
| |
| case cmUVProcessChainBuilder::External: |
| outputData.Stdio.flags = UV_INHERIT_FD; |
| outputData.Stdio.data.fd = output.FileDescriptor; |
| break; |
| } |
| |
| bool first = true; |
| for (std::size_t i = 0; i < this->Builder->Processes.size(); i++) { |
| this->Processes.emplace_back(cm::make_unique<ProcessData>()); |
| auto& process = *this->Processes.back(); |
| process.Data = this; |
| process.ProcessStatus.Finished = false; |
| |
| if (!first) { |
| auto& prevProcess = *this->Processes[i - 1]; |
| |
| int pipeFd[2]; |
| if (cmGetPipes(pipeFd) < 0) { |
| return false; |
| } |
| |
| if (prevProcess.OutputPipe.init(*this->Loop, 0) < 0) { |
| return false; |
| } |
| if (uv_pipe_open(prevProcess.OutputPipe, pipeFd[1]) < 0) { |
| return false; |
| } |
| if (process.InputPipe.init(*this->Loop, 0) < 0) { |
| return false; |
| } |
| if (uv_pipe_open(process.InputPipe, pipeFd[0]) < 0) { |
| return false; |
| } |
| } |
| |
| first = false; |
| } |
| |
| return true; |
| } |
| |
| void cmUVProcessChain::InternalData::SpawnProcess( |
| std::size_t index, |
| const cmUVProcessChainBuilder::ProcessConfiguration& config, bool first, |
| bool last) |
| { |
| auto& process = *this->Processes[index]; |
| |
| auto options = uv_process_options_t(); |
| |
| // Bounds were checked at add time, first element is guaranteed to exist |
| options.file = config.Arguments[0].c_str(); |
| |
| std::vector<const char*> arguments; |
| arguments.reserve(config.Arguments.size()); |
| for (auto const& arg : config.Arguments) { |
| arguments.push_back(arg.c_str()); |
| } |
| arguments.push_back(nullptr); |
| options.args = const_cast<char**>(arguments.data()); |
| options.flags = UV_PROCESS_WINDOWS_HIDE; |
| if (!this->Builder->WorkingDirectory.empty()) { |
| options.cwd = this->Builder->WorkingDirectory.c_str(); |
| } |
| |
| std::array<uv_stdio_container_t, 3> stdio; |
| if (first) { |
| stdio[0] = this->InputStreamData.Stdio; |
| } else { |
| stdio[0] = uv_stdio_container_t(); |
| stdio[0].flags = UV_INHERIT_STREAM; |
| stdio[0].data.stream = process.InputPipe; |
| } |
| if (last) { |
| stdio[1] = this->OutputStreamData.Stdio; |
| } else { |
| stdio[1] = uv_stdio_container_t(); |
| stdio[1].flags = UV_INHERIT_STREAM; |
| stdio[1].data.stream = process.OutputPipe; |
| } |
| stdio[2] = this->ErrorStreamData.Stdio; |
| |
| options.stdio = stdio.data(); |
| options.stdio_count = 3; |
| options.exit_cb = [](uv_process_t* handle, int64_t exitStatus, |
| int termSignal) { |
| auto* processData = static_cast<ProcessData*>(handle->data); |
| processData->ProcessStatus.ExitStatus = exitStatus; |
| processData->ProcessStatus.TermSignal = termSignal; |
| processData->Finish(); |
| }; |
| |
| if ((process.ProcessStatus.SpawnResult = |
| process.Process.spawn(*this->Loop, options, &process)) < 0) { |
| process.Finish(); |
| } |
| process.InputPipe.reset(); |
| process.OutputPipe.reset(); |
| } |
| |
| void cmUVProcessChain::InternalData::Finish() |
| { |
| this->TempOutputPipe.reset(); |
| this->TempErrorPipe.reset(); |
| this->Valid = true; |
| } |
| |
| cmUVProcessChain::cmUVProcessChain() |
| : Data(cm::make_unique<InternalData>()) |
| { |
| this->Data->Loop.init(); |
| } |
| |
| cmUVProcessChain::cmUVProcessChain(cmUVProcessChain&& other) noexcept |
| : Data(std::move(other.Data)) |
| { |
| } |
| |
| cmUVProcessChain::~cmUVProcessChain() = default; |
| |
| cmUVProcessChain& cmUVProcessChain::operator=( |
| cmUVProcessChain&& other) noexcept |
| { |
| this->Data = std::move(other.Data); |
| return *this; |
| } |
| |
| uv_loop_t& cmUVProcessChain::GetLoop() |
| { |
| return *this->Data->Loop; |
| } |
| |
| int cmUVProcessChain::OutputStream() |
| { |
| return this->Data->OutputStreamData.BuiltinStream; |
| } |
| |
| int cmUVProcessChain::ErrorStream() |
| { |
| return this->Data->ErrorStreamData.BuiltinStream; |
| } |
| |
| bool cmUVProcessChain::Valid() const |
| { |
| return this->Data->Valid; |
| } |
| |
| bool cmUVProcessChain::Wait(uint64_t milliseconds) |
| { |
| bool timeout = false; |
| cm::uv_timer_ptr timer; |
| |
| if (milliseconds > 0) { |
| timer.init(*this->Data->Loop, &timeout); |
| timer.start( |
| [](uv_timer_t* handle) { |
| auto* timeoutPtr = static_cast<bool*>(handle->data); |
| *timeoutPtr = true; |
| }, |
| milliseconds, 0); |
| } |
| |
| while (!timeout && |
| this->Data->ProcessesCompleted < this->Data->Processes.size()) { |
| uv_run(this->Data->Loop, UV_RUN_ONCE); |
| } |
| |
| return !timeout; |
| } |
| |
| std::vector<const cmUVProcessChain::Status*> cmUVProcessChain::GetStatus() |
| const |
| { |
| std::vector<const cmUVProcessChain::Status*> statuses( |
| this->Data->Processes.size(), nullptr); |
| for (std::size_t i = 0; i < statuses.size(); i++) { |
| statuses[i] = &this->GetStatus(i); |
| } |
| return statuses; |
| } |
| |
| const cmUVProcessChain::Status& cmUVProcessChain::GetStatus( |
| std::size_t index) const |
| { |
| return this->Data->Processes[index]->ProcessStatus; |
| } |
| |
| bool cmUVProcessChain::Finished() const |
| { |
| return this->Data->ProcessesCompleted >= this->Data->Processes.size(); |
| } |
| |
| std::pair<cmUVProcessChain::ExceptionCode, std::string> |
| cmUVProcessChain::Status::GetException() const |
| { |
| if (this->SpawnResult) { |
| return std::make_pair(ExceptionCode::Spawn, |
| uv_strerror(this->SpawnResult)); |
| } |
| #ifdef _WIN32 |
| if (this->Finished && (this->ExitStatus & 0xF0000000) == 0xC0000000) { |
| // Child terminated due to exceptional behavior. |
| switch (this->ExitStatus) { |
| case STATUS_CONTROL_C_EXIT: |
| return std::make_pair(ExceptionCode::Interrupt, "User interrupt"); |
| |
| case STATUS_FLOAT_DENORMAL_OPERAND: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Floating-point exception (denormal operand)"); |
| case STATUS_FLOAT_DIVIDE_BY_ZERO: |
| return std::make_pair(ExceptionCode::Numerical, "Divide-by-zero"); |
| case STATUS_FLOAT_INEXACT_RESULT: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Floating-point exception (inexact result)"); |
| case STATUS_FLOAT_INVALID_OPERATION: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Invalid floating-point operation"); |
| case STATUS_FLOAT_OVERFLOW: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Floating-point overflow"); |
| case STATUS_FLOAT_STACK_CHECK: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Floating-point stack check failed"); |
| case STATUS_FLOAT_UNDERFLOW: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Floating-point underflow"); |
| # ifdef STATUS_FLOAT_MULTIPLE_FAULTS |
| case STATUS_FLOAT_MULTIPLE_FAULTS: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Floating-point exception (multiple faults)"); |
| # endif |
| # ifdef STATUS_FLOAT_MULTIPLE_TRAPS |
| case STATUS_FLOAT_MULTIPLE_TRAPS: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Floating-point exception (multiple traps)"); |
| # endif |
| case STATUS_INTEGER_DIVIDE_BY_ZERO: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Integer divide-by-zero"); |
| case STATUS_INTEGER_OVERFLOW: |
| return std::make_pair(ExceptionCode::Numerical, "Integer overflow"); |
| |
| case STATUS_DATATYPE_MISALIGNMENT: |
| return std::make_pair(ExceptionCode::Fault, "Datatype misalignment"); |
| case STATUS_ACCESS_VIOLATION: |
| return std::make_pair(ExceptionCode::Fault, "Access violation"); |
| case STATUS_IN_PAGE_ERROR: |
| return std::make_pair(ExceptionCode::Fault, "In-page error"); |
| case STATUS_INVALID_HANDLE: |
| return std::make_pair(ExceptionCode::Fault, "Invalid handle"); |
| case STATUS_NONCONTINUABLE_EXCEPTION: |
| return std::make_pair(ExceptionCode::Fault, |
| "Noncontinuable exception"); |
| case STATUS_INVALID_DISPOSITION: |
| return std::make_pair(ExceptionCode::Fault, "Invalid disposition"); |
| case STATUS_ARRAY_BOUNDS_EXCEEDED: |
| return std::make_pair(ExceptionCode::Fault, "Array bounds exceeded"); |
| case STATUS_STACK_OVERFLOW: |
| return std::make_pair(ExceptionCode::Fault, "Stack overflow"); |
| |
| case STATUS_ILLEGAL_INSTRUCTION: |
| return std::make_pair(ExceptionCode::Illegal, "Illegal instruction"); |
| case STATUS_PRIVILEGED_INSTRUCTION: |
| return std::make_pair(ExceptionCode::Illegal, |
| "Privileged instruction"); |
| |
| case STATUS_NO_MEMORY: |
| default: { |
| char buf[256]; |
| snprintf(buf, sizeof(buf), "Exit code 0x%x\n", |
| static_cast<unsigned int>(this->ExitStatus)); |
| return std::make_pair(ExceptionCode::Other, buf); |
| } |
| } |
| } |
| #else |
| if (this->Finished && this->TermSignal) { |
| switch (this->TermSignal) { |
| # ifdef SIGSEGV |
| case SIGSEGV: |
| return std::make_pair(ExceptionCode::Fault, "Segmentation fault"); |
| # endif |
| # ifdef SIGBUS |
| # if !defined(SIGSEGV) || SIGBUS != SIGSEGV |
| case SIGBUS: |
| return std::make_pair(ExceptionCode::Fault, "Bus error"); |
| # endif |
| # endif |
| # ifdef SIGFPE |
| case SIGFPE: |
| return std::make_pair(ExceptionCode::Numerical, |
| "Floating-point exception"); |
| # endif |
| # ifdef SIGILL |
| case SIGILL: |
| return std::make_pair(ExceptionCode::Illegal, "Illegal instruction"); |
| # endif |
| # ifdef SIGINT |
| case SIGINT: |
| return std::make_pair(ExceptionCode::Interrupt, "User interrupt"); |
| # endif |
| # ifdef SIGABRT |
| case SIGABRT: |
| return std::make_pair(ExceptionCode::Other, "Subprocess aborted"); |
| # endif |
| # ifdef SIGKILL |
| case SIGKILL: |
| return std::make_pair(ExceptionCode::Other, "Subprocess killed"); |
| # endif |
| # ifdef SIGTERM |
| case SIGTERM: |
| return std::make_pair(ExceptionCode::Other, "Subprocess terminated"); |
| # endif |
| # ifdef SIGHUP |
| case SIGHUP: |
| return std::make_pair(ExceptionCode::Other, "SIGHUP"); |
| # endif |
| # ifdef SIGQUIT |
| case SIGQUIT: |
| return std::make_pair(ExceptionCode::Other, "SIGQUIT"); |
| # endif |
| # ifdef SIGTRAP |
| case SIGTRAP: |
| return std::make_pair(ExceptionCode::Other, "SIGTRAP"); |
| # endif |
| # ifdef SIGIOT |
| # if !defined(SIGABRT) || SIGIOT != SIGABRT |
| case SIGIOT: |
| return std::make_pair(ExceptionCode::Other, "SIGIOT"); |
| # endif |
| # endif |
| # ifdef SIGUSR1 |
| case SIGUSR1: |
| return std::make_pair(ExceptionCode::Other, "SIGUSR1"); |
| # endif |
| # ifdef SIGUSR2 |
| case SIGUSR2: |
| return std::make_pair(ExceptionCode::Other, "SIGUSR2"); |
| # endif |
| # ifdef SIGPIPE |
| case SIGPIPE: |
| return std::make_pair(ExceptionCode::Other, "SIGPIPE"); |
| # endif |
| # ifdef SIGALRM |
| case SIGALRM: |
| return std::make_pair(ExceptionCode::Other, "SIGALRM"); |
| # endif |
| # ifdef SIGSTKFLT |
| case SIGSTKFLT: |
| return std::make_pair(ExceptionCode::Other, "SIGSTKFLT"); |
| # endif |
| # ifdef SIGCHLD |
| case SIGCHLD: |
| return std::make_pair(ExceptionCode::Other, "SIGCHLD"); |
| # elif defined(SIGCLD) |
| case SIGCLD: |
| return std::make_pair(ExceptionCode::Other, "SIGCLD"); |
| # endif |
| # ifdef SIGCONT |
| case SIGCONT: |
| return std::make_pair(ExceptionCode::Other, "SIGCONT"); |
| # endif |
| # ifdef SIGSTOP |
| case SIGSTOP: |
| return std::make_pair(ExceptionCode::Other, "SIGSTOP"); |
| # endif |
| # ifdef SIGTSTP |
| case SIGTSTP: |
| return std::make_pair(ExceptionCode::Other, "SIGTSTP"); |
| # endif |
| # ifdef SIGTTIN |
| case SIGTTIN: |
| return std::make_pair(ExceptionCode::Other, "SIGTTIN"); |
| # endif |
| # ifdef SIGTTOU |
| case SIGTTOU: |
| return std::make_pair(ExceptionCode::Other, "SIGTTOU"); |
| # endif |
| # ifdef SIGURG |
| case SIGURG: |
| return std::make_pair(ExceptionCode::Other, "SIGURG"); |
| # endif |
| # ifdef SIGXCPU |
| case SIGXCPU: |
| return std::make_pair(ExceptionCode::Other, "SIGXCPU"); |
| # endif |
| # ifdef SIGXFSZ |
| case SIGXFSZ: |
| return std::make_pair(ExceptionCode::Other, "SIGXFSZ"); |
| # endif |
| # ifdef SIGVTALRM |
| case SIGVTALRM: |
| return std::make_pair(ExceptionCode::Other, "SIGVTALRM"); |
| # endif |
| # ifdef SIGPROF |
| case SIGPROF: |
| return std::make_pair(ExceptionCode::Other, "SIGPROF"); |
| # endif |
| # ifdef SIGWINCH |
| case SIGWINCH: |
| return std::make_pair(ExceptionCode::Other, "SIGWINCH"); |
| # endif |
| # ifdef SIGPOLL |
| case SIGPOLL: |
| return std::make_pair(ExceptionCode::Other, "SIGPOLL"); |
| # endif |
| # ifdef SIGIO |
| # if !defined(SIGPOLL) || SIGIO != SIGPOLL |
| case SIGIO: |
| return std::make_pair(ExceptionCode::Other, "SIGIO"); |
| # endif |
| # endif |
| # ifdef SIGPWR |
| case SIGPWR: |
| return std::make_pair(ExceptionCode::Other, "SIGPWR"); |
| # endif |
| # ifdef SIGSYS |
| case SIGSYS: |
| return std::make_pair(ExceptionCode::Other, "SIGSYS"); |
| # endif |
| # ifdef SIGUNUSED |
| # if !defined(SIGSYS) || SIGUNUSED != SIGSYS |
| case SIGUNUSED: |
| return std::make_pair(ExceptionCode::Other, "SIGUNUSED"); |
| # endif |
| # endif |
| default: { |
| char buf[256]; |
| snprintf(buf, sizeof(buf), "Signal %d", this->TermSignal); |
| return std::make_pair(ExceptionCode::Other, buf); |
| } |
| } |
| } |
| #endif |
| return std::make_pair(ExceptionCode::None, ""); |
| } |
| |
| void cmUVProcessChain::InternalData::ProcessData::Finish() |
| { |
| this->ProcessStatus.Finished = true; |
| this->Data->ProcessesCompleted++; |
| } |