Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactor ProcessSingleton so that it may be used distinctly from a fu…

…ll browser process.

The main effect is that ProcessSingleton is much more restricted to its basic functionality, which is to ensure that a single instance of Chrome runs per profile directory and to pass messages from new Chrome invocations to the existing instance.

Exactly how those messages were handled has been moved from the implementations of ProcessSingleton into ChromeBrowserMain where (I think) it more rightly belongs.

This will allow the Chrome Frame net tests to use ProcessSingleton to implement a stub Chrome for the purpose of handling network-related IPC from Chrome Frame without having to launch the rest of Chrome, upon which ProcessSingleton previously depended directly.

BUG=None
TEST=None

Review URL: http://codereview.chromium.org/9968053

git-svn-id: http://src.chromium.org/svn/trunk/src/chrome/browser@130853 4ff67af0-8c30-449e-8e8b-ad334ec8d88c
  • Loading branch information...
commit fdd7fc1573cfe89b33ad3131506540e8a2ddd189 1 parent 1e656bb
erikwright@chromium.org authored
View
36 chrome_browser_main.cc
@@ -31,6 +31,7 @@
#include "chrome/browser/about_flags.h"
#include "chrome/browser/auto_launch_trial.h"
#include "chrome/browser/autocomplete/autocomplete_field_trial.h"
+#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_impl.h"
#include "chrome/browser/browser_shutdown.h"
#include "chrome/browser/chrome_browser_main_extra_parts.h"
@@ -538,6 +539,38 @@ void AddPreReadHistogramTime(const char* name, base::TimeDelta time) {
counter->AddTime(time);
}
+bool ProcessSingletonNotificationCallback(const CommandLine& command_line,
+ const FilePath& current_directory) {
+ // Drop the request if the browser process is already in shutdown path.
+ if (!g_browser_process || g_browser_process->IsShuttingDown())
+ return false;
+
+ // TODO(erikwright): Consider removing this - AFAIK it is no longer used.
+ // Handle the --uninstall-extension startup action. This needs to done here
+ // in the process that is running with the target profile, otherwise the
+ // uninstall will fail to unload and remove all components.
+ if (command_line.HasSwitch(switches::kUninstallExtension)) {
+ // The uninstall extension switch can't be combined with the profile
+ // directory switch.
+ DCHECK(!command_line.HasSwitch(switches::kProfileDirectory));
+
+ Profile* profile = ProfileManager::GetLastUsedProfile();
+ if (!profile) {
+ // We should never be called before the profile has been created.
+ NOTREACHED();
+ return true;
+ }
+
+ ExtensionsStartupUtil ext_startup_util;
+ ext_startup_util.UninstallExtension(command_line, profile);
+ return true;
+ }
+
+ BrowserInit::ProcessCommandLineAlreadyRunning(
+ command_line, current_directory);
+ return true;
+}
+
} // namespace
namespace chrome_browser {
@@ -1465,7 +1498,8 @@ int ChromeBrowserMainParts::PreMainMessageLoopRunImpl() {
// new one. NotifyOtherProcess will currently give the other process up to
// 20 seconds to respond. Note that this needs to be done before we attempt
// to read the profile.
- notify_result_ = process_singleton_->NotifyOtherProcessOrCreate();
+ notify_result_ = process_singleton_->NotifyOtherProcessOrCreate(
+ base::Bind(&ProcessSingletonNotificationCallback));
switch (notify_result_) {
case ProcessSingleton::PROCESS_NONE:
// No process already running, fall through to starting a new one.
View
2  process_singleton.cc
@@ -17,7 +17,7 @@ void ProcessSingleton::Unlock() {
if (replayed_messages.find(*it) !=
replayed_messages.end())
continue;
- ProcessCommandLine(CommandLine(it->first), it->second);
+ notification_callback_.Run(CommandLine(it->first), it->second);
replayed_messages.insert(*it);
}
saved_startup_messages_.clear();
View
28 process_singleton.h
@@ -16,6 +16,7 @@
#include <vector>
#include "base/basictypes.h"
+#include "base/callback.h"
#include "base/command_line.h"
#include "base/file_path.h"
#include "base/logging.h"
@@ -54,6 +55,15 @@ class ProcessSingleton : public base::NonThreadSafe {
LOCK_ERROR,
};
+ // Implement this callback to handle notifications from other processes. The
+ // callback will receive the command line and directory with which the other
+ // Chrome process was launched. Return true if the command line will be
+ // handled within the current browser instance or false if the remote process
+ // should handle it (i.e., because the current process is shutting down).
+ typedef base::Callback<
+ bool(const CommandLine& command_line, const FilePath& current_directory)>
+ NotificationCallback;
+
explicit ProcessSingleton(const FilePath& user_data_dir);
~ProcessSingleton();
@@ -67,10 +77,12 @@ class ProcessSingleton : public base::NonThreadSafe {
// first one, so this function won't find it.
NotifyResult NotifyOtherProcess();
- // Notify another process, if available. Otherwise sets ourselves as the
- // singleton instance. Returns PROCESS_NONE if we became the singleton
+ // Notify another process, if available. Otherwise sets ourselves as the
+ // singleton instance and stores the provided callback for notification from
+ // future processes. Returns PROCESS_NONE if we became the singleton
// instance.
- NotifyResult NotifyOtherProcessOrCreate();
+ NotifyResult NotifyOtherProcessOrCreate(
+ const NotificationCallback& notification_callback);
#if defined(OS_LINUX) || defined(OS_OPENBSD)
// Exposed for testing. We use a timeout on Linux, and in tests we want
@@ -80,6 +92,7 @@ class ProcessSingleton : public base::NonThreadSafe {
bool kill_unresponsive);
NotifyResult NotifyOtherProcessWithTimeoutOrCreate(
const CommandLine& command_line,
+ const NotificationCallback& notification_callback,
int timeout_seconds);
#endif // defined(OS_LINUX) || defined(OS_OPENBSD)
@@ -96,8 +109,10 @@ class ProcessSingleton : public base::NonThreadSafe {
// Sets ourself up as the singleton instance. Returns true on success. If
// false is returned, we are not the singleton instance and the caller must
- // exit.
- bool Create();
+ // exit. Otherwise, stores the provided callback for notification from
+ // future processes.
+ bool Create(
+ const NotificationCallback& notification_callback);
// Clear any lock state during shutdown.
void Cleanup();
@@ -132,8 +147,6 @@ class ProcessSingleton : public base::NonThreadSafe {
private:
typedef std::pair<CommandLine::StringVector, FilePath> DelayedStartupMessage;
- void ProcessCommandLine(const CommandLine& command_line,
- const FilePath& current_directory);
#if !defined(OS_MACOSX)
// Timeout for the current browser process to respond. 20 seconds should be
@@ -143,6 +156,7 @@ class ProcessSingleton : public base::NonThreadSafe {
bool locked_;
gfx::NativeWindow foreground_window_;
+ NotificationCallback notification_callback_; // Handler for notifications.
#if defined(OS_WIN)
// This ugly behemoth handles startup commands sent from another process.
View
52 process_singleton_linux.cc
@@ -80,16 +80,10 @@
#include "base/time.h"
#include "base/timer.h"
#include "base/utf_string_conversions.h"
-#include "chrome/browser/browser_process.h"
#if defined(TOOLKIT_GTK)
#include "chrome/browser/ui/gtk/process_singleton_dialog.h"
#endif
-#include "chrome/browser/io_thread.h"
-#include "chrome/browser/profiles/profile.h"
-#include "chrome/browser/profiles/profile_manager.h"
-#include "chrome/browser/ui/browser_init.h"
#include "chrome/common/chrome_constants.h"
-#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "content/public/browser/browser_thread.h"
#include "grit/chromium_strings.h"
@@ -630,8 +624,11 @@ void ProcessSingleton::LinuxWatcher::HandleMessage(
return;
}
- // Ignore the request if the browser process is already in shutdown path.
- if (!g_browser_process || g_browser_process->IsShuttingDown()) {
+ if (parent_->notification_callback_.Run(CommandLine(argv),
+ FilePath(current_dir))) {
+ // Send back "ACK" message to prevent the client process from starting up.
+ reader->FinishWithACK(kACKToken, arraysize(kACKToken) - 1);
+ } else {
LOG(WARNING) << "Not handling interprocess notification as browser"
" is shutting down";
// Send back "SHUTDOWN" message, so that the client process can start up
@@ -639,12 +636,6 @@ void ProcessSingleton::LinuxWatcher::HandleMessage(
reader->FinishWithACK(kShutdownToken, arraysize(kShutdownToken) - 1);
return;
}
-
- CommandLine parsed_command_line(argv);
- parent_->ProcessCommandLine(parsed_command_line, FilePath(current_dir));
-
- // Send back "ACK" message to prevent the client process from starting up.
- reader->FinishWithACK(kACKToken, arraysize(kACKToken) - 1);
}
void ProcessSingleton::LinuxWatcher::RemoveSocketReader(SocketReader* reader) {
@@ -886,21 +877,24 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout(
return PROCESS_NOTIFIED;
}
-ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate() {
+ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate(
+ const NotificationCallback& notification_callback) {
return NotifyOtherProcessWithTimeoutOrCreate(
*CommandLine::ForCurrentProcess(),
+ notification_callback,
kTimeoutInSeconds);
}
ProcessSingleton::NotifyResult
ProcessSingleton::NotifyOtherProcessWithTimeoutOrCreate(
const CommandLine& command_line,
+ const NotificationCallback& notification_callback,
int timeout_seconds) {
NotifyResult result = NotifyOtherProcessWithTimeout(command_line,
timeout_seconds, true);
if (result != PROCESS_NONE)
return result;
- if (Create())
+ if (Create(notification_callback))
return PROCESS_NONE;
// If the Create() failed, try again to notify. (It could be that another
// instance was starting at the same time and managed to grab the lock before
@@ -914,7 +908,8 @@ ProcessSingleton::NotifyOtherProcessWithTimeoutOrCreate(
return LOCK_ERROR;
}
-bool ProcessSingleton::Create() {
+bool ProcessSingleton::Create(
+ const NotificationCallback& notification_callback) {
int sock;
sockaddr_un addr;
@@ -971,6 +966,8 @@ bool ProcessSingleton::Create() {
if (listen(sock, 5) < 0)
NOTREACHED() << "listen failed: " << safe_strerror(errno);
+ notification_callback_ = notification_callback;
+
DCHECK(BrowserThread::IsMessageLoopValid(BrowserThread::IO));
BrowserThread::PostTask(
BrowserThread::IO,
@@ -987,24 +984,3 @@ void ProcessSingleton::Cleanup() {
UnlinkPath(cookie_path_);
UnlinkPath(lock_path_);
}
-
-void ProcessSingleton::ProcessCommandLine(const CommandLine& command_line,
- const FilePath& current_directory) {
- PrefService* prefs = g_browser_process->local_state();
- DCHECK(prefs);
-
- // Ignore the request if the process was passed the --product-version flag.
- // Normally we wouldn't get here if that flag had been passed, but it can
- // happen if it is passed to an older version of chrome. Since newer versions
- // of chrome do this in the background, we want to avoid spawning extra
- // windows.
- if (command_line.HasSwitch(switches::kProductVersion)) {
- DLOG(WARNING) << "Remote process was passed product version flag, "
- << "but ignored it. Doing nothing.";
- } else {
- // Run the browser startup sequence again, with the command line of the
- // signalling process.
- BrowserInit::ProcessCommandLineAlreadyRunning(command_line,
- current_directory);
- }
-}
View
23 process_singleton_linux_uitest.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@@ -11,7 +11,10 @@
#include <vector>
#include <string>
+#include "base/bind.h"
+#include "base/command_line.h"
#include "base/eintr_wrapper.h"
+#include "base/file_path.h"
#include "base/path_service.h"
#include "base/stringprintf.h"
#include "base/test/test_timeouts.h"
@@ -27,6 +30,14 @@
namespace {
+bool UnexpectedNotificationCallback(const CommandLine& command_line,
+ const FilePath& current_directory) {
+ ADD_FAILURE() << "This callback should never be invoked because the active "
+ << "ProcessSingleton is that of the Browser process hosted by "
+ << "UITest.";
+ return false;
+}
+
class ProcessSingletonLinuxTest : public UITest {
public:
virtual void SetUp() {
@@ -94,7 +105,9 @@ ProcessSingleton::NotifyResult NotifyOtherProcessOrCreate(
int timeout_ms) {
scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton());
return process_singleton->NotifyOtherProcessWithTimeoutOrCreate(
- CommandLineForUrl(url), timeout_ms / 1000);
+ CommandLineForUrl(url),
+ base::Bind(&UnexpectedNotificationCallback),
+ timeout_ms / 1000);
}
} // namespace
@@ -271,7 +284,8 @@ TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessOrCreate_DifferingHost) {
// Test that Create fails when another browser is using the profile directory.
TEST_F(ProcessSingletonLinuxTest, CreateFailsWithExistingBrowser) {
scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton());
- EXPECT_FALSE(process_singleton->Create());
+ EXPECT_FALSE(process_singleton->Create(
+ base::Bind(&UnexpectedNotificationCallback)));
}
// Test that Create fails when another browser is using the profile directory
@@ -289,7 +303,8 @@ TEST_F(ProcessSingletonLinuxTest, CreateChecksCompatibilitySocket) {
socket_path_.value().c_str()));
ASSERT_EQ(0, unlink(cookie_path_.value().c_str()));
- EXPECT_FALSE(process_singleton->Create());
+ EXPECT_FALSE(process_singleton->Create(
+ base::Bind(&UnexpectedNotificationCallback)));
}
// Test that we fail when lock says process is on another host and we can't
View
14 process_singleton_mac.cc
@@ -56,19 +56,23 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() {
return PROCESS_NONE;
}
-ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate() {
+ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate(
+ const NotificationCallback& notification_callback) {
// Windows tries NotifyOtherProcess() first.
- return Create() ? PROCESS_NONE : PROFILE_IN_USE;
+ return Create(notification_callback) ? PROCESS_NONE : PROFILE_IN_USE;
}
// Attempt to acquire an exclusive lock on an empty file in the
// profile directory. Returns |true| if it gets the lock. Returns
// |false| if the lock is held, or if there is an error.
+// |notification_callback| is not actually used. See the comments at the top of
+// this file for details.
// TODO(shess): Rather than logging failures, popup an alert. Punting
// that for now because it would require confidence that this code is
// never called in a situation where an alert wouldn't work.
// http://crbug.com/59061
-bool ProcessSingleton::Create() {
+bool ProcessSingleton::Create(
+ const NotificationCallback& notification_callback) {
DCHECK_EQ(-1, lock_fd_) << "lock_path_ is already open.";
lock_fd_ = HANDLE_EINTR(open(lock_path_.value().c_str(),
@@ -114,7 +118,3 @@ void ProcessSingleton::Cleanup() {
}
lock_fd_ = -1;
}
-
-void ProcessSingleton::ProcessCommandLine(const CommandLine& command_line,
- const FilePath& current_directory) {
-}
View
30 process_singleton_mac_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@@ -69,7 +69,7 @@ class ProcessSingletonMacTest : public PlatformTest {
TEST_F(ProcessSingletonMacTest, Basic) {
ProcessSingleton ps(temp_dir_.path());
EXPECT_FALSE(IsLocked());
- EXPECT_TRUE(ps.Create());
+ EXPECT_TRUE(ps.Create(ProcessSingleton::NotificationCallback()));
EXPECT_TRUE(IsLocked());
ps.Cleanup();
EXPECT_FALSE(IsLocked());
@@ -80,7 +80,7 @@ TEST_F(ProcessSingletonMacTest, DestructorReleases) {
EXPECT_FALSE(IsLocked());
{
ProcessSingleton ps(temp_dir_.path());
- EXPECT_TRUE(ps.Create());
+ EXPECT_TRUE(ps.Create(ProcessSingleton::NotificationCallback()));
EXPECT_TRUE(IsLocked());
}
EXPECT_FALSE(IsLocked());
@@ -99,16 +99,16 @@ TEST_F(ProcessSingletonMacTest, Interlock) {
// When |ps1| has the lock, |ps2| cannot get it.
EXPECT_FALSE(IsLocked());
- EXPECT_TRUE(ps1.Create());
+ EXPECT_TRUE(ps1.Create(ProcessSingleton::NotificationCallback()));
EXPECT_TRUE(IsLocked());
- EXPECT_FALSE(ps2.Create());
+ EXPECT_FALSE(ps2.Create(ProcessSingleton::NotificationCallback()));
ps1.Cleanup();
// And when |ps2| has the lock, |ps1| cannot get it.
EXPECT_FALSE(IsLocked());
- EXPECT_TRUE(ps2.Create());
+ EXPECT_TRUE(ps2.Create(ProcessSingleton::NotificationCallback()));
EXPECT_TRUE(IsLocked());
- EXPECT_FALSE(ps1.Create());
+ EXPECT_FALSE(ps1.Create(ProcessSingleton::NotificationCallback()));
ps2.Cleanup();
EXPECT_FALSE(IsLocked());
}
@@ -126,16 +126,24 @@ TEST_F(ProcessSingletonMacTest, NotifyOtherProcessOrCreate) {
// When |ps1| has the lock, |ps2| cannot get it.
EXPECT_FALSE(IsLocked());
- EXPECT_EQ(ProcessSingleton::PROCESS_NONE, ps1.NotifyOtherProcessOrCreate());
+ EXPECT_EQ(
+ ProcessSingleton::PROCESS_NONE,
+ ps1.NotifyOtherProcessOrCreate(ProcessSingleton::NotificationCallback()));
EXPECT_TRUE(IsLocked());
- EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, ps2.NotifyOtherProcessOrCreate());
+ EXPECT_EQ(
+ ProcessSingleton::PROFILE_IN_USE,
+ ps2.NotifyOtherProcessOrCreate(ProcessSingleton::NotificationCallback()));
ps1.Cleanup();
// And when |ps2| has the lock, |ps1| cannot get it.
EXPECT_FALSE(IsLocked());
- EXPECT_EQ(ProcessSingleton::PROCESS_NONE, ps2.NotifyOtherProcessOrCreate());
+ EXPECT_EQ(
+ ProcessSingleton::PROCESS_NONE,
+ ps2.NotifyOtherProcessOrCreate(ProcessSingleton::NotificationCallback()));
EXPECT_TRUE(IsLocked());
- EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, ps1.NotifyOtherProcessOrCreate());
+ EXPECT_EQ(
+ ProcessSingleton::PROFILE_IN_USE,
+ ps1.NotifyOtherProcessOrCreate(ProcessSingleton::NotificationCallback()));
ps2.Cleanup();
EXPECT_FALSE(IsLocked());
}
View
67 process_singleton_win.cc
@@ -12,15 +12,8 @@
#include "base/utf_string_conversions.h"
#include "base/win/scoped_handle.h"
#include "base/win/wrapped_window_proc.h"
-#include "chrome/browser/browser_process.h"
-#include "chrome/browser/extensions/extensions_startup.h"
-#include "chrome/browser/profiles/profile.h"
-#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/simple_message_box.h"
-#include "chrome/browser/ui/browser_init.h"
#include "chrome/common/chrome_constants.h"
-#include "chrome/common/chrome_paths.h"
-#include "chrome/common/chrome_switches.h"
#include "chrome/installer/util/wmi.h"
#include "content/public/common/result_codes.h"
#include "grit/chromium_strings.h"
@@ -193,9 +186,6 @@ ProcessSingleton::ProcessSingleton(const FilePath& user_data_dir)
ATOM clazz = ::RegisterClassEx(&wc);
DCHECK(clazz);
- FilePath user_data_dir;
- PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
-
// Set the window's title to the path of our user data directory so other
// Chrome instances can decide if they should forward to us or not.
window_ = ::CreateWindow(MAKEINTATOM(clazz),
@@ -289,19 +279,26 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() {
return PROCESS_NONE;
}
-ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate() {
+ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate(
+ const NotificationCallback& notification_callback) {
NotifyResult result = NotifyOtherProcess();
if (result != PROCESS_NONE)
return result;
- return window_ ? PROCESS_NONE : PROFILE_IN_USE;
+ return Create(notification_callback) ? PROCESS_NONE : PROFILE_IN_USE;
}
// On Windows, there is no need to call Create() since the message
// window is created in the constructor but to avoid having more
// platform specific code in browser_main.cc we tolerate calls to
-// Create(), which will do nothing.
-bool ProcessSingleton::Create() {
+// Create().
+bool ProcessSingleton::Create(
+ const NotificationCallback& notification_callback) {
DCHECK(!remote_window_);
+ DCHECK(notification_callback_.is_null());
+
+ if (window_ != NULL)
+ notification_callback_ = notification_callback;
+
return window_ != NULL;
}
@@ -343,18 +340,12 @@ LRESULT ProcessSingleton::OnCopyData(HWND hwnd, const COPYDATASTRUCT* cds) {
return TRUE;
}
- // Ignore the request if the browser process is already in shutdown path.
- if (!g_browser_process || g_browser_process->IsShuttingDown()) {
- LOG(WARNING) << "Not handling WM_COPYDATA as browser is shutting down";
- return FALSE;
- }
-
CommandLine parsed_command_line(CommandLine::NO_PROGRAM);
FilePath current_directory;
if (!ParseCommandLine(cds, &parsed_command_line, &current_directory))
return TRUE;
- ProcessCommandLine(parsed_command_line, current_directory);
- return TRUE;
+ return notification_callback_.Run(parsed_command_line, current_directory) ?
+ TRUE : FALSE;
}
LRESULT ProcessSingleton::WndProc(HWND hwnd, UINT message,
@@ -369,35 +360,3 @@ LRESULT ProcessSingleton::WndProc(HWND hwnd, UINT message,
return ::DefWindowProc(hwnd, message, wparam, lparam);
}
-
-void ProcessSingleton::ProcessCommandLine(const CommandLine& command_line,
- const FilePath& current_directory) {
- PrefService* prefs = g_browser_process->local_state();
- DCHECK(prefs);
-
- // Handle the --uninstall-extension startup action. This needs to done here
- // in the process that is running with the target profile, otherwise the
- // uninstall will fail to unload and remove all components.
- if (command_line.HasSwitch(switches::kUninstallExtension)) {
- // The uninstall extension switch can't be combined with the profile
- // directory switch.
- DCHECK(!command_line.HasSwitch(switches::kProfileDirectory));
-
- Profile* profile = ProfileManager::GetLastUsedProfile();
- if (!profile) {
- // We should only be able to get here if the profile already exists and
- // has been created.
- NOTREACHED();
- return;
- }
-
- ExtensionsStartupUtil ext_startup_util;
- ext_startup_util.UninstallExtension(command_line, profile);
- return;
- }
-
- // Run the browser startup sequence again, with the command line of the
- // signalling process.
- BrowserInit::ProcessCommandLineAlreadyRunning(command_line,
- current_directory);
-}
Please sign in to comment.
Something went wrong with that request. Please try again.