/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /***************************************************************************** * * nsProcess is used to execute new processes and specify if you want to * wait (blocking) or continue (non-blocking). * ***************************************************************************** */ #include "mozilla/ArrayUtils.h" #include "nsCOMPtr.h" #include "nsIFile.h" #include "nsProcess.h" #include "prio.h" #include "prenv.h" #include "nsCRT.h" #include "nsThreadUtils.h" #include "nsIObserverService.h" #include "nsXULAppAPI.h" #include "mozilla/Services.h" #include #if defined(PROCESSMODEL_WINAPI) # include "nsString.h" # include "nsLiteralString.h" # include "nsReadableUtils.h" # include "mozilla/AssembleCmdLine.h" # include "mozilla/UniquePtrExtensions.h" #else # ifdef XP_MACOSX # include # include # endif # ifdef XP_UNIX # ifndef XP_MACOSX # include "base/process_util.h" # endif # include # include # endif # include # include #endif using namespace mozilla; //-------------------------------------------------------------------// // nsIProcess implementation //-------------------------------------------------------------------// NS_IMPL_ISUPPORTS(nsProcess, nsIProcess, nsIObserver) // Constructor nsProcess::nsProcess() : mThread(nullptr), mLock("nsProcess.mLock"), mShutdown(false), mBlocking(false), mStartHidden(false), mNoShell(false), mPid(-1), mExitValue(-1) #if !defined(XP_UNIX) , mProcess(nullptr) #endif { } // Destructor nsProcess::~nsProcess() = default; NS_IMETHODIMP nsProcess::Init(nsIFile* aExecutable) { if (mExecutable) { return NS_ERROR_ALREADY_INITIALIZED; } if (NS_WARN_IF(!aExecutable)) { return NS_ERROR_INVALID_ARG; } bool isFile; // First make sure the file exists nsresult rv = aExecutable->IsFile(&isFile); if (NS_FAILED(rv)) { return rv; } if (!isFile) { return NS_ERROR_FAILURE; } // Store the nsIFile in mExecutable mExecutable = aExecutable; // Get the path because it is needed by the NSPR process creation #ifdef XP_WIN rv = mExecutable->GetTarget(mTargetPath); if (NS_FAILED(rv) || mTargetPath.IsEmpty()) #endif rv = mExecutable->GetPath(mTargetPath); return rv; } void nsProcess::Monitor(void* aArg) { RefPtr process = dont_AddRef(static_cast(aArg)); if (!process->mBlocking) { NS_SetCurrentThreadName("RunProcess"); } #if defined(PROCESSMODEL_WINAPI) HANDLE processHandle; { // The mutex region cannot include WaitForSingleObject otherwise we'll // block calls such as Kill. So lock on access and store a local. MutexAutoLock lock(process->mLock); processHandle = process->mProcess; } DWORD dwRetVal; unsigned long exitCode = -1; dwRetVal = WaitForSingleObject(processHandle, INFINITE); if (dwRetVal != WAIT_FAILED) { if (GetExitCodeProcess(processHandle, &exitCode) == FALSE) { exitCode = -1; } } // Lock in case Kill or GetExitCode are called during this. { MutexAutoLock lock(process->mLock); CloseHandle(process->mProcess); process->mProcess = nullptr; process->mExitValue = exitCode; if (process->mShutdown) { return; } } #else # ifdef XP_UNIX int exitCode = -1; int status = 0; pid_t result; do { result = waitpid(process->mPid, &status, 0); } while (result == -1 && errno == EINTR); if (result == process->mPid) { if (WIFEXITED(status)) { exitCode = WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { exitCode = 256; // match NSPR's signal exit status } } # else int32_t exitCode = -1; PRProcess* prProcess; { // The mutex region cannot include PR_WaitProcess otherwise we'll // block calls such as Kill. So lock on access and store a local. MutexAutoLock lock(process->mLock); prProcess = process->mProcess; } if (PR_WaitProcess(prProcess, &exitCode) != PR_SUCCESS) { exitCode = -1; } # endif // Lock in case Kill or GetExitCode are called during this { MutexAutoLock lock(process->mLock); # if !defined(XP_UNIX) process->mProcess = nullptr; # endif process->mExitValue = exitCode; if (process->mShutdown) { return; } } #endif // If we ran a background thread for the monitor then notify on the main // thread if (NS_IsMainThread()) { process->ProcessComplete(); } else { NS_DispatchToMainThread(NewRunnableMethod( "nsProcess::ProcessComplete", process, &nsProcess::ProcessComplete)); } } void nsProcess::ProcessComplete() { if (mThread) { nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->RemoveObserver(this, "xpcom-shutdown"); } PR_JoinThread(mThread); mThread = nullptr; } const char* topic; { MutexAutoLock lock(mLock); if (mExitValue != 0) { topic = "process-failed"; } else { topic = "process-finished"; } } mPid = -1; nsCOMPtr observer = mObserver.GetValue(); mObserver = nullptr; if (observer) { observer->Observe(NS_ISUPPORTS_CAST(nsIProcess*, this), topic, nullptr); } } // XXXldb |aArgs| has the wrong const-ness NS_IMETHODIMP nsProcess::Run(bool aBlocking, const char** aArgs, uint32_t aCount) { return CopyArgsAndRunProcess(aBlocking, aArgs, aCount, nullptr, false); } // XXXldb |aArgs| has the wrong const-ness NS_IMETHODIMP nsProcess::RunAsync(const char** aArgs, uint32_t aCount, nsIObserver* aObserver, bool aHoldWeak) { return CopyArgsAndRunProcess(false, aArgs, aCount, aObserver, aHoldWeak); } nsresult nsProcess::CopyArgsAndRunProcess(bool aBlocking, const char** aArgs, uint32_t aCount, nsIObserver* aObserver, bool aHoldWeak) { // Add one to the aCount for the program name and one for null termination. char** my_argv = nullptr; my_argv = (char**)moz_xmalloc(sizeof(char*) * (aCount + 2)); my_argv[0] = ToNewUTF8String(mTargetPath); for (uint32_t i = 0; i < aCount; ++i) { my_argv[i + 1] = const_cast(aArgs[i]); } my_argv[aCount + 1] = nullptr; nsresult rv = RunProcess(aBlocking, my_argv, aObserver, aHoldWeak, false); free(my_argv[0]); free(my_argv); return rv; } // XXXldb |aArgs| has the wrong const-ness NS_IMETHODIMP nsProcess::Runw(bool aBlocking, const char16_t** aArgs, uint32_t aCount) { return CopyArgsAndRunProcessw(aBlocking, aArgs, aCount, nullptr, false); } // XXXldb |aArgs| has the wrong const-ness NS_IMETHODIMP nsProcess::RunwAsync(const char16_t** aArgs, uint32_t aCount, nsIObserver* aObserver, bool aHoldWeak) { return CopyArgsAndRunProcessw(false, aArgs, aCount, aObserver, aHoldWeak); } nsresult nsProcess::CopyArgsAndRunProcessw(bool aBlocking, const char16_t** aArgs, uint32_t aCount, nsIObserver* aObserver, bool aHoldWeak) { // Add one to the aCount for the program name and one for null termination. char** my_argv = nullptr; my_argv = (char**)moz_xmalloc(sizeof(char*) * (aCount + 2)); my_argv[0] = ToNewUTF8String(mTargetPath); for (uint32_t i = 0; i < aCount; i++) { my_argv[i + 1] = ToNewUTF8String(nsDependentString(aArgs[i])); } my_argv[aCount + 1] = nullptr; nsresult rv = RunProcess(aBlocking, my_argv, aObserver, aHoldWeak, true); for (uint32_t i = 0; i <= aCount; ++i) { free(my_argv[i]); } free(my_argv); return rv; } nsresult nsProcess::RunProcess(bool aBlocking, char** aMyArgv, nsIObserver* aObserver, bool aHoldWeak, bool aArgsUTF8) { NS_WARNING_ASSERTION(!XRE_IsContentProcess(), "No launching of new processes in the content process"); if (NS_WARN_IF(!mExecutable)) { return NS_ERROR_NOT_INITIALIZED; } if (NS_WARN_IF(mThread)) { return NS_ERROR_ALREADY_INITIALIZED; } if (aObserver) { if (aHoldWeak) { nsresult rv = NS_OK; mObserver = do_GetWeakReference(aObserver, &rv); NS_ENSURE_SUCCESS(rv, rv); } else { mObserver = aObserver; } } { MutexAutoLock lock(mLock); mExitValue = -1; mPid = -1; } #if defined(PROCESSMODEL_WINAPI) BOOL retVal; UniqueFreePtr cmdLine; // |aMyArgv| is null-terminated and always starts with the program path. If // the second slot is non-null then arguments are being passed. if (aMyArgv[1] || mNoShell) { // Pass the executable path as argv[0] to the launched program when calling // CreateProcess(). char** argv = mNoShell ? aMyArgv : aMyArgv + 1; wchar_t* assembledCmdLine = nullptr; if (assembleCmdLine(argv, &assembledCmdLine, aArgsUTF8 ? CP_UTF8 : CP_ACP) == -1) { return NS_ERROR_FILE_EXECUTION_FAILED; } cmdLine.reset(assembledCmdLine); } // The program name in aMyArgv[0] is always UTF-8 NS_ConvertUTF8toUTF16 wideFile(aMyArgv[0]); if (mNoShell) { STARTUPINFO startupInfo; ZeroMemory(&startupInfo, sizeof(startupInfo)); startupInfo.cb = sizeof(startupInfo); startupInfo.dwFlags = STARTF_USESHOWWINDOW; startupInfo.wShowWindow = mStartHidden ? SW_HIDE : SW_SHOWNORMAL; PROCESS_INFORMATION processInfo; retVal = CreateProcess(/* lpApplicationName = */ wideFile.get(), /* lpCommandLine */ cmdLine.get(), /* lpProcessAttributes = */ NULL, /* lpThreadAttributes = */ NULL, /* bInheritHandles = */ FALSE, /* dwCreationFlags = */ 0, /* lpEnvironment = */ NULL, /* lpCurrentDirectory = */ NULL, /* lpStartupInfo = */ &startupInfo, /* lpProcessInformation */ &processInfo); if (!retVal) { return NS_ERROR_FILE_EXECUTION_FAILED; } CloseHandle(processInfo.hThread); // TODO(bug 1763051): assess if we need further work around this locking. MutexAutoLock lock(mLock); mProcess = processInfo.hProcess; } else { SHELLEXECUTEINFOW sinfo; memset(&sinfo, 0, sizeof(SHELLEXECUTEINFOW)); sinfo.cbSize = sizeof(SHELLEXECUTEINFOW); sinfo.hwnd = nullptr; sinfo.lpFile = wideFile.get(); sinfo.nShow = mStartHidden ? SW_HIDE : SW_SHOWNORMAL; /* The SEE_MASK_NO_CONSOLE flag is important to prevent console windows * from appearing. This makes behavior the same on all platforms. The flag * will not have any effect on non-console applications. */ sinfo.fMask = SEE_MASK_FLAG_DDEWAIT | SEE_MASK_NO_CONSOLE | SEE_MASK_NOCLOSEPROCESS; if (cmdLine) { sinfo.lpParameters = cmdLine.get(); } retVal = ShellExecuteExW(&sinfo); if (!retVal) { return NS_ERROR_FILE_EXECUTION_FAILED; } MutexAutoLock lock(mLock); mProcess = sinfo.hProcess; } { MutexAutoLock lock(mLock); mPid = GetProcessId(mProcess); } #elif defined(XP_MACOSX) // Note: |aMyArgv| is already null-terminated as required by posix_spawnp. pid_t newPid = 0; int result = posix_spawnp(&newPid, aMyArgv[0], nullptr, nullptr, aMyArgv, *_NSGetEnviron()); mPid = static_cast(newPid); if (result != 0) { return NS_ERROR_FAILURE; } #elif defined(XP_UNIX) base::LaunchOptions options; std::vector argvVec; for (char** arg = aMyArgv; *arg != nullptr; ++arg) { argvVec.push_back(*arg); } pid_t newPid; if (base::LaunchApp(argvVec, options, &newPid)) { static_assert(sizeof(pid_t) <= sizeof(int32_t), "mPid is large enough to hold a pid"); mPid = static_cast(newPid); } else { return NS_ERROR_FAILURE; } #else { PRProcess* prProcess = PR_CreateProcess(aMyArgv[0], aMyArgv, nullptr, nullptr); if (!prProcess) { return NS_ERROR_FAILURE; } { MutexAutoLock lock(mLock); mProcess = prProcess; } struct MYProcess { uint32_t pid; }; MYProcess* ptrProc = (MYProcess*)mProcess; mPid = ptrProc->pid; } #endif NS_ADDREF_THIS(); mBlocking = aBlocking; if (aBlocking) { Monitor(this); MutexAutoLock lock(mLock); if (mExitValue < 0) { return NS_ERROR_FILE_EXECUTION_FAILED; } } else { mThread = CreateMonitorThread(); if (!mThread) { NS_RELEASE_THIS(); return NS_ERROR_FAILURE; } // It isn't a failure if we just can't watch for shutdown nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->AddObserver(this, "xpcom-shutdown", false); } } return NS_OK; } // We don't guarantee that monitor threads are joined before Gecko exits, which // can cause TSAN to complain about thread leaks. We handle this with a TSAN // suppression, and route thread creation through this helper so that the // suppression is as narrowly-scoped as possible. PRThread* nsProcess::CreateMonitorThread() { return PR_CreateThread(PR_SYSTEM_THREAD, Monitor, this, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0); } NS_IMETHODIMP nsProcess::GetIsRunning(bool* aIsRunning) { if (mThread) { *aIsRunning = true; } else { *aIsRunning = false; } return NS_OK; } NS_IMETHODIMP nsProcess::GetStartHidden(bool* aStartHidden) { *aStartHidden = mStartHidden; return NS_OK; } NS_IMETHODIMP nsProcess::SetStartHidden(bool aStartHidden) { mStartHidden = aStartHidden; return NS_OK; } NS_IMETHODIMP nsProcess::GetNoShell(bool* aNoShell) { *aNoShell = mNoShell; return NS_OK; } NS_IMETHODIMP nsProcess::SetNoShell(bool aNoShell) { mNoShell = aNoShell; return NS_OK; } NS_IMETHODIMP nsProcess::GetPid(uint32_t* aPid) { if (!mThread) { return NS_ERROR_FAILURE; } if (mPid < 0) { return NS_ERROR_NOT_IMPLEMENTED; } *aPid = mPid; return NS_OK; } NS_IMETHODIMP nsProcess::Kill() { if (!mThread) { return NS_ERROR_FAILURE; } { MutexAutoLock lock(mLock); #if defined(PROCESSMODEL_WINAPI) if (TerminateProcess(mProcess, 0) == 0) { return NS_ERROR_FAILURE; } #elif defined(XP_UNIX) if (kill(mPid, SIGKILL) != 0) { return NS_ERROR_FAILURE; } #else if (!mProcess || (PR_KillProcess(mProcess) != PR_SUCCESS)) { return NS_ERROR_FAILURE; } #endif } // We must null out mThread if we want IsRunning to return false immediately // after this call. nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->RemoveObserver(this, "xpcom-shutdown"); } PR_JoinThread(mThread); mThread = nullptr; return NS_OK; } NS_IMETHODIMP nsProcess::GetExitValue(int32_t* aExitValue) { MutexAutoLock lock(mLock); *aExitValue = mExitValue; return NS_OK; } NS_IMETHODIMP nsProcess::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { // Shutting down, drop all references if (mThread) { nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->RemoveObserver(this, "xpcom-shutdown"); } mThread = nullptr; } mObserver = nullptr; MutexAutoLock lock(mLock); mShutdown = true; return NS_OK; }