Skip to content

Commit 66cabe5

Browse files
committed
LibCore+Userland+Ladybird: Explicitly manage Core::Process's end-state
This removes the implicit `disown`ing of the Process in the destructor in favor of up front requests in the constructor. We also force users to exclusively and explicitly disown, wait or take the pid of the child. I hope that it will a good way to prevent API misuses, like calling C functions with the pid or `wait_for_termination` on a disowned child. It unfortunately prevents the usage of the "old" API with KeepAsChild::Yes, this is why there are some refactorings that come with the patch. This also should make it possible to implement KeepAsChild::No on other platforms.
1 parent 5ea55f3 commit 66cabe5

File tree

8 files changed

+141
-67
lines changed

8 files changed

+141
-67
lines changed

Ladybird/HelperProcess.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ static ErrorOr<NonnullRefPtr<ClientType>> launch_server_process_impl(
3333
}
3434

3535
for (auto [i, path] : enumerate(candidate_server_paths)) {
36-
Core::ProcessSpawnOptions options { .name = server_name, .arguments = arguments };
36+
Core::ProcessSpawnOptions options { .name = server_name, .arguments = arguments, .keep_as_child = Core::KeepAsChild::Yes };
3737

3838
if (enable_callgrind_profiling == Ladybird::EnableCallgrindProfiling::Yes) {
3939
options.executable = "valgrind"sv;
@@ -47,12 +47,13 @@ static ErrorOr<NonnullRefPtr<ClientType>> launch_server_process_impl(
4747

4848
if (!result.is_error()) {
4949
auto process = result.release_value();
50+
auto pid = process.process.take_pid();
5051

5152
if constexpr (requires { process.client->set_pid(pid_t {}); })
52-
process.client->set_pid(process.process.pid());
53+
process.client->set_pid(pid);
5354

5455
if (register_with_process_manager == RegisterWithProcessManager::Yes)
55-
WebView::ProcessManager::the().add_process(WebView::process_type_from_name(server_name), process.process.pid());
56+
WebView::ProcessManager::the().add_process(WebView::process_type_from_name(server_name), pid);
5657

5758
if (enable_callgrind_profiling == Ladybird::EnableCallgrindProfiling::Yes) {
5859
dbgln();

Ladybird/WebDriver/main.cpp

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,22 @@
1818

1919
static Vector<ByteString> certificates;
2020

21-
static ErrorOr<pid_t> launch_process(StringView application, ReadonlySpan<char const*> arguments)
21+
static ErrorOr<pid_t> launch_process(StringView application, Vector<ByteString> arguments)
2222
{
2323
auto paths = TRY(get_paths_for_helper_process(application));
2424

25-
ErrorOr<pid_t> result = -1;
2625
for (auto const& path : paths) {
2726
auto path_view = path.view();
28-
result = Core::Process::spawn(path_view, arguments, {}, Core::Process::KeepAsChild::Yes);
29-
if (!result.is_error())
30-
break;
27+
auto maybe_process = Core::Process::spawn(Core::ProcessSpawnOptions { .executable = path_view, .arguments = arguments, .keep_as_child = Core::KeepAsChild::Yes });
28+
if (!maybe_process.is_error())
29+
return maybe_process.value().take_pid();
3130
}
32-
return result;
31+
return -1;
3332
}
3433

3534
static ErrorOr<pid_t> launch_browser(ByteString const& socket_path)
3635
{
37-
auto arguments = Vector {
36+
auto arguments = Vector<ByteString> {
3837
"--webdriver-content-path",
3938
socket_path.characters(),
4039
};
@@ -50,14 +49,14 @@ static ErrorOr<pid_t> launch_browser(ByteString const& socket_path)
5049

5150
arguments.append("about:blank");
5251

53-
return launch_process("Ladybird"sv, arguments.span());
52+
return launch_process("Ladybird"sv, arguments);
5453
}
5554

5655
static ErrorOr<pid_t> launch_headless_browser(ByteString const& socket_path)
5756
{
5857
auto resources = ByteString::formatted("{}/res", s_serenity_resource_root);
5958
return launch_process("headless-browser"sv,
60-
Array {
59+
Vector<ByteString> {
6160
"--resources",
6261
resources.characters(),
6362
"--webdriver-ipc-path",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2025, Lucas Chollet <[email protected]>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include <LibCore/Process.h>
8+
#include <LibTest/TestCase.h>
9+
10+
TEST_CASE(crash_on_api_misuse)
11+
{
12+
{
13+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
14+
.keep_as_child = Core::KeepAsChild::No }));
15+
16+
EXPECT_CRASH("calling wait_for_termination() on disowned child", [&] {
17+
EXPECT(!process.wait_for_termination().is_error());
18+
return Test::Crash::Failure::DidNotCrash;
19+
});
20+
21+
EXPECT_CRASH("calling take_pid() on disowned child", [&] {
22+
process.take_pid();
23+
return Test::Crash::Failure::DidNotCrash;
24+
});
25+
}
26+
27+
{
28+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
29+
.keep_as_child = Core::KeepAsChild::Yes }));
30+
31+
EXPECT_CRASH("calling take_pid() after wait_for_termination()", [&] {
32+
EXPECT(!process.wait_for_termination().is_error());
33+
process.take_pid();
34+
return Test::Crash::Failure::DidNotCrash;
35+
});
36+
37+
EXPECT_CRASH("calling wait_for_termination() after take_pid()", [&] {
38+
EXPECT(!process.wait_for_termination().is_error());
39+
process.take_pid();
40+
return Test::Crash::Failure::DidNotCrash;
41+
});
42+
// This creates a zombie process.
43+
process.take_pid();
44+
}
45+
46+
EXPECT_CRASH("Require explicit call to wait_for_termination() of take_pid()", [&] {
47+
{
48+
auto maybe_process = Core::Process::spawn(
49+
{ .executable = "/bin/true",
50+
.keep_as_child = Core::KeepAsChild::Yes });
51+
EXPECT(!maybe_process.is_error());
52+
}
53+
return Test::Crash::Failure::DidNotCrash;
54+
});
55+
}
56+
57+
TEST_CASE(no_crash)
58+
{
59+
{
60+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
61+
.keep_as_child = Core::KeepAsChild::No }));
62+
}
63+
{
64+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
65+
.keep_as_child = Core::KeepAsChild::Yes }));
66+
TRY_OR_FAIL(process.wait_for_termination());
67+
}
68+
{
69+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
70+
.keep_as_child = Core::KeepAsChild::Yes }));
71+
// This creates a zombie process.
72+
process.take_pid();
73+
}
74+
}

Tests/LibELF/TestOrder.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ static ByteBuffer run(ByteString executable)
2626
.fd = 1,
2727
},
2828
},
29+
.keep_as_child = Core::KeepAsChild::Yes,
2930
}));
3031
MUST(process.wait_for_termination());
3132
auto output = MUST(Core::File::open(path_to_captured_output.string(), Core::File::OpenMode::Read));

Userland/Applications/Run/RunWindow.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,15 @@ void RunWindow::do_run()
108108
bool RunWindow::run_as_command(ByteString const& run_input)
109109
{
110110
// TODO: Query and use the user's preferred shell.
111-
auto maybe_child_pid = Core::Process::spawn("/bin/Shell"sv, Array { "-c", run_input.characters() }, {}, Core::Process::KeepAsChild::Yes);
112-
if (maybe_child_pid.is_error())
111+
auto maybe_child_process = Core::Process::spawn(Core::ProcessSpawnOptions {
112+
.executable = "/bin/Shell"sv,
113+
.arguments = { "-c", run_input.characters() },
114+
.keep_as_child = Core::KeepAsChild::Yes });
115+
116+
if (maybe_child_process.is_error())
113117
return false;
114118

115-
pid_t child_pid = maybe_child_pid.release_value();
119+
pid_t child_pid = maybe_child_process.value().take_pid();
116120

117121
// The child shell was able to start. Let's save it to the history immediately so users can see it as the first entry the next time they run this program.
118122
prepend_history(run_input);

Userland/Applications/Terminal/main.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class TerminalChangeListener : public Config::Listener {
103103
static ErrorOr<void> utmp_update(StringView tty, pid_t pid, bool create)
104104
{
105105
auto pid_string = String::number(pid);
106-
Array utmp_update_command {
106+
Vector<ByteString> utmp_update_command {
107107
"-f"sv,
108108
"Terminal"sv,
109109
"-p"sv,
@@ -112,7 +112,11 @@ static ErrorOr<void> utmp_update(StringView tty, pid_t pid, bool create)
112112
tty,
113113
};
114114

115-
auto utmpupdate_pid = TRY(Core::Process::spawn("/bin/utmpupdate"sv, utmp_update_command, {}, Core::Process::KeepAsChild::Yes));
115+
auto utmpupdate_pid = TRY(Core::Process::spawn(Core::ProcessSpawnOptions {
116+
.executable = "/bin/utmpupdate"sv,
117+
.arguments = utmp_update_command,
118+
.keep_as_child = Core::KeepAsChild::Yes }))
119+
.take_pid();
116120

117121
Core::System::WaitPidResult status;
118122
auto wait_successful = false;

Userland/Libraries/LibCore/Process.cpp

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,12 @@ ErrorOr<Process> Process::spawn(ProcessSpawnOptions const& options)
114114
} else {
115115
pid = TRY(System::posix_spawn(options.executable.view(), &spawn_actions, nullptr, const_cast<char**>(argv_list.get().data()), Core::Environment::raw_environ()));
116116
}
117-
return Process { pid };
117+
118+
auto process = Process { pid };
119+
if (options.keep_as_child == KeepAsChild::No)
120+
TRY(process.disown());
121+
122+
return process;
118123
}
119124

120125
ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<ByteString> arguments, ByteString working_directory, KeepAsChild keep_as_child)
@@ -123,15 +128,10 @@ ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<ByteString> argument
123128
.executable = path,
124129
.arguments = Vector<ByteString> { arguments },
125130
.working_directory = working_directory.is_empty() ? Optional<ByteString> {} : Optional<ByteString> { working_directory },
131+
.keep_as_child = keep_as_child,
126132
}));
127133

128-
if (keep_as_child == KeepAsChild::No)
129-
TRY(process.disown());
130-
else {
131-
// FIXME: This won't be needed if return value is changed to Process.
132-
process.m_should_disown = false;
133-
}
134-
return process.pid();
134+
return process.m_pid;
135135
}
136136

137137
ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<StringView> arguments, ByteString working_directory, KeepAsChild keep_as_child)
@@ -145,13 +145,10 @@ ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<StringView> argument
145145
.executable = path,
146146
.arguments = backing_strings,
147147
.working_directory = working_directory.is_empty() ? Optional<ByteString> {} : Optional<ByteString> { working_directory },
148+
.keep_as_child = keep_as_child,
148149
}));
149150

150-
if (keep_as_child == KeepAsChild::No)
151-
TRY(process.disown());
152-
else
153-
process.m_should_disown = false;
154-
return process.pid();
151+
return process.m_pid;
155152
}
156153

157154
ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<char const*> arguments, ByteString working_directory, KeepAsChild keep_as_child)
@@ -165,13 +162,10 @@ ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<char const*> argumen
165162
.executable = path,
166163
.arguments = backing_strings,
167164
.working_directory = working_directory.is_empty() ? Optional<ByteString> {} : Optional<ByteString> { working_directory },
165+
.keep_as_child = keep_as_child,
168166
}));
169167

170-
if (keep_as_child == KeepAsChild::No)
171-
TRY(process.disown());
172-
else
173-
process.m_should_disown = false;
174-
return process.pid();
168+
return process.m_pid;
175169
}
176170

177171
ErrorOr<String> Process::get_name()
@@ -320,22 +314,20 @@ void Process::wait_for_debugger_and_break()
320314

321315
ErrorOr<void> Process::disown()
322316
{
323-
if (m_pid != 0 && m_should_disown) {
324317
#ifdef AK_OS_SERENITY
325-
TRY(System::disown(m_pid));
318+
TRY(System::disown(m_pid));
326319
#else
327-
// FIXME: Support disown outside Serenity.
320+
// FIXME: Support disown outside Serenity.
328321
#endif
329-
m_should_disown = false;
330-
return {};
331-
} else {
332-
return Error::from_errno(EINVAL);
333-
}
322+
m_was_managed = true;
323+
return {};
334324
}
335325

336326
ErrorOr<bool> Process::wait_for_termination()
337327
{
338328
VERIFY(m_pid > 0);
329+
VERIFY(!m_was_managed);
330+
m_was_managed = true;
339331

340332
bool exited_with_code_0 = true;
341333
int status;
@@ -353,7 +345,6 @@ ErrorOr<bool> Process::wait_for_termination()
353345
VERIFY_NOT_REACHED();
354346
}
355347

356-
m_should_disown = false;
357348
return exited_with_code_0;
358349
}
359350

@@ -457,7 +448,7 @@ ErrorOr<IPCProcess::ProcessPaths> IPCProcess::paths_for_process(StringView proce
457448
ErrorOr<IPCProcess::ProcessAndIPCSocket> IPCProcess::spawn_singleton_and_connect_to_process(ProcessSpawnOptions const& options)
458449
{
459450
auto [socket_path, pid_path] = TRY(paths_for_process(options.name));
460-
Process process { -1 };
451+
Optional<Process> process;
461452

462453
if (auto existing_pid = TRY(get_process_pid(options.name, pid_path)); existing_pid.has_value()) {
463454
process = Process { *existing_pid };
@@ -485,7 +476,7 @@ ErrorOr<IPCProcess::ProcessAndIPCSocket> IPCProcess::spawn_singleton_and_connect
485476
auto process = TRY(Process::spawn(options));
486477
{
487478
auto pid_file = TRY(File::open(pid_path, File::OpenMode::Write));
488-
TRY(pid_file->write_until_depleted(ByteString::number(process.pid())));
479+
TRY(pid_file->write_until_depleted(ByteString::number(process.m_pid)));
489480
}
490481

491482
TRY(System::kill(getpid(), SIGTERM));
@@ -504,7 +495,7 @@ ErrorOr<IPCProcess::ProcessAndIPCSocket> IPCProcess::spawn_singleton_and_connect
504495
auto ipc_socket = TRY(LocalSocket::connect(socket_path));
505496
TRY(ipc_socket->set_blocking(true));
506497

507-
return ProcessAndIPCSocket { move(process), move(ipc_socket) };
498+
return ProcessAndIPCSocket { process.release_value(), move(ipc_socket) };
508499
}
509500

510501
}

0 commit comments

Comments
 (0)