Skip to content

Commit 613ec3c

Browse files
authored
[k2] implement exec (#1408)
1 parent 30a6fe1 commit 613ec3c

7 files changed

Lines changed: 266 additions & 113 deletions

File tree

builtin-functions/kphp-light/stdlib/system-functions.txt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@ function escapeshellarg($arg ::: string): string;
3333

3434
function escapeshellcmd($cmd ::: string): string;
3535

36-
// === UNSUPPORTED ===
36+
function exec($command ::: string, &$output ::: mixed = [], int &$result_code = TODO): string|false;
3737

38-
/** @kphp-extern-func-info stub */
39-
function exec($command ::: string, &$output ::: mixed = [], int &$result_code = 0): string|false;
38+
// === UNSUPPORTED ===
4039

4140
/** @kphp-extern-func-info stub generation-required */
4241
function getenv(string $varname = '', bool $local_only = false): mixed;
@@ -48,11 +47,9 @@ function sleep ($seconds ::: int) ::: void;
4847
/** @kphp-extern-func-info stub generation-required */
4948
function raise_sigsegv () ::: void;
5049

51-
5250
/** @kphp-extern-func-info stub */
5351
function system($command ::: string, int &$result_code = 0): int;
5452

55-
5653
/** @kphp-extern-func-info stub generation-required */
5754
function ctype_alnum(mixed $text): bool;
5855
/** @kphp-extern-func-info stub generation-required */

common/algorithms/string-algorithms.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ inline bool is_ascii_whitespace(char c) noexcept {
7575
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
7676
}
7777

78+
inline vk::string_view rstrip_ascii_whitespace(vk::string_view view) noexcept {
79+
return vk::string_view{view.begin(), std::find_if_not(view.rbegin(), view.rend(), is_ascii_whitespace).base()};
80+
}
81+
7882
inline vk::string_view strip_ascii_whitespace(vk::string_view view) noexcept {
7983
const auto* not_space = std::find_if_not(view.begin(), view.end(), is_ascii_whitespace);
8084
view = vk::string_view{not_space, view.end()};

runtime-light/k2-platform/k2-api.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <sys/stat.h>
2020
#include <sys/types.h>
2121
#include <sys/utsname.h>
22+
#include <tuple>
2223
#include <type_traits>
2324
#include <utility>
2425

@@ -356,6 +357,38 @@ inline int32_t stat(std::string_view path, struct stat* stat) noexcept {
356357
return k2::errno_ok;
357358
}
358359

360+
using CommandArg = CommandArg;
361+
362+
enum class CommandStdoutPolicy : uint8_t { NoCapture, Capture };
363+
364+
inline auto command(std::string_view cmd, std::span<const k2::CommandArg> args, k2::CommandStdoutPolicy policy) noexcept {
365+
char* output{};
366+
size_t output_len{};
367+
size_t output_align{};
368+
int32_t exit_code{};
369+
static constexpr auto deleter_creator{[](size_t output_len, size_t output_align) noexcept {
370+
return [output_len, output_align](void* ptr) noexcept { k2::free_checked(ptr, output_len, output_align); };
371+
}};
372+
373+
using deleter_type = std::invoke_result_t<decltype(deleter_creator), size_t, size_t>;
374+
using unique_ptr_type = std::unique_ptr<std::byte, deleter_type>;
375+
using return_type = std::expected<std::tuple<int32_t, unique_ptr_type, size_t>, int32_t>;
376+
377+
if (auto error_code{k2_command(cmd.data(), cmd.size(), args.data(), args.size(), std::addressof(exit_code),
378+
policy == k2::CommandStdoutPolicy::Capture ? std::addressof(output) : nullptr,
379+
policy == k2::CommandStdoutPolicy::Capture ? std::addressof(output_len) : nullptr,
380+
policy == k2::CommandStdoutPolicy::Capture ? std::addressof(output_align) : nullptr)};
381+
error_code != k2::errno_ok) [[unlikely]] {
382+
return return_type{std::unexpected{error_code}};
383+
}
384+
385+
auto output_deleter{std::invoke(deleter_creator, output_len, output_align)};
386+
return return_type{{exit_code,
387+
policy == k2::CommandStdoutPolicy::Capture ? unique_ptr_type{reinterpret_cast<std::byte*>(output), std::move(output_deleter)}
388+
: unique_ptr_type{nullptr, std::move(output_deleter)},
389+
output_len}};
390+
}
391+
359392
} // namespace k2
360393

361394
template<>

runtime-light/k2-platform/k2-header.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,54 @@ int32_t k2_canonicalize(const char* path, size_t pathlen, char* const* resolved_
665665
*/
666666
int32_t k2_stat(const char* pathname, size_t pathname_len, struct stat* statbuf);
667667

668+
struct CommandArg {
669+
const char* arg;
670+
size_t arg_len;
671+
};
672+
673+
/**
674+
* Executes a specified command as a child process, waits for it to complete, and optionally captures
675+
* its exit code and output.
676+
*
677+
* This function allocates memory for the output buffer and returns a pointer to this memory, the size and
678+
* alignment of the memory.The caller is responsible for freeing the allocated memory when it is no longer needed.
679+
*
680+
* @param `cmd` The command to be executed.
681+
* @param `cmd_len` The length of the command.
682+
* @param `args` A pointer to an array of `CommandArg` structures representing the command-line
683+
* arguments to be passed to the command.
684+
* @param `args_len` The number of elements in the `args` array.
685+
* @param `exit_code` A pointer to a memory where the exit code of the command will be stored.
686+
* If you do not need the exit code, you can pass `nullptr`.
687+
* @param `output` A pointer to a memory where the address of the allocated output
688+
* buffer will be stored. The buffer will contain the command's output. If you
689+
* pass `nullptr`, the function will skip capturing the command's output.
690+
* @param `output_len` A pointer to a memory where the total length of the command's output will be
691+
* stored. If you pass `nullptr`, the function will skip capturing the command's
692+
* output.
693+
* @param `output_align` A pointer to a memory where the alignment of the output will be stored. If
694+
* you pass `nullptr`, the function will skip capturing the command's output.
695+
*
696+
* @return The status of the command execution. A return value of 0 indicates success, while a non-zero
697+
* value indicates an error.
698+
*
699+
* @note This function is more likely to be temporary and may be replaced by a more generic one
700+
* in the future. Consider this when integrating it into your application.
701+
*
702+
* @note The function blocks until the command has finished executing.
703+
*
704+
* @note The caller is responsible for freeing the allocated output buffer using `k2_free` or `k2_free_checked`
705+
* to prevent memory leaks.
706+
*
707+
* Possible `errno`:
708+
* `EINVAL` => `cmd` or `args.arg` is `NULL`.
709+
* `ENOMEM` => Out of memory (i.e., kernel memory).
710+
* `ENOSYS` => Internal error.
711+
*
712+
*/
713+
int32_t k2_command(const char* cmd, size_t cmd_len, const struct CommandArg* args, size_t args_len, int32_t* exit_code, char* const* output, size_t* output_len,
714+
size_t* output_align);
715+
668716
#ifdef __cplusplus
669717
}
670718
#endif

runtime-light/stdlib/system/system-functions.h

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@
44

55
#pragma once
66

7+
#include <algorithm>
8+
#include <array>
79
#include <chrono>
10+
#include <concepts>
811
#include <cstddef>
912
#include <cstdint>
13+
#include <expected>
14+
#include <functional>
1015
#include <memory>
16+
#include <optional>
1117
#include <pwd.h>
18+
#include <ranges>
1219
#include <span>
1320
#include <string_view>
1421
#include <sys/types.h>
22+
#include <type_traits>
23+
#include <utility>
1524

25+
#include "common/algorithms/string-algorithms.h"
1626
#include "runtime-common/core/allocator/script-malloc-interface.h"
1727
#include "runtime-common/core/runtime-core.h"
1828
#include "runtime-common/stdlib/serialization/json-functions.h"
@@ -38,6 +48,35 @@ constexpr std::string_view SHELL_PWUID_KEY = "shell";
3848

3949
} // namespace kphp::posix::impl
4050

51+
namespace kphp::system {
52+
53+
template<std::invocable<std::span<std::byte>> output_handler_type = std::identity>
54+
auto exec(std::string_view cmd, const output_handler_type& output_handler = {}) noexcept -> std::expected<int32_t, int32_t> {
55+
static constexpr std::string_view program{"sh"};
56+
static constexpr std::string_view arg1{"-c"};
57+
58+
if (cmd.empty()) [[unlikely]] {
59+
kphp::log::warning("exec command must not be empty");
60+
return std::unexpected{k2::errno_einval};
61+
}
62+
63+
std::array args{k2::CommandArg{.arg = arg1.data(), .arg_len = arg1.size()}, k2::CommandArg{.arg = cmd.data(), .arg_len = cmd.size()}};
64+
auto expected{
65+
k2::command(program, args, std::is_same_v<output_handler_type, std::identity> ? k2::CommandStdoutPolicy::NoCapture : k2::CommandStdoutPolicy::Capture)};
66+
if (!expected) [[unlikely]] {
67+
kphp::log::warning("error executing command: error code -> {}, cmd -> '{}'", expected.error(), cmd);
68+
return std::unexpected{expected.error()};
69+
}
70+
71+
const auto [exit_code, output, output_len]{*std::move(expected)};
72+
if constexpr (!std::is_same_v<output_handler_type, std::identity>) {
73+
std::invoke(output_handler, std::span<std::byte>{output.get(), output_len});
74+
}
75+
return std::expected<int32_t, int32_t>{exit_code};
76+
}
77+
78+
} // namespace kphp::system
79+
4180
template<typename F>
4281
bool f$register_kphp_on_oom_callback(F&& /*callback*/) {
4382
kphp::log::error("call to unsupported function");
@@ -159,13 +198,42 @@ inline kphp::coro::task<> f$usleep(int64_t microseconds) noexcept {
159198
co_await kphp::forks::id_managed(kphp::coro::io_scheduler::get().yield_for(std::chrono::microseconds{microseconds}));
160199
}
161200

162-
inline Optional<string> f$exec([[maybe_unused]] const string& command) noexcept {
163-
kphp::log::error("call to unsupported function");
201+
inline Optional<string> f$exec(const string& cmd, mixed& output, std::optional<std::reference_wrapper<int64_t>> exit_code = {}) noexcept {
202+
string last_line{};
203+
const auto output_handler{[&last_line, &output_mixed = output](std::span<std::byte> output_bytes) noexcept {
204+
std::string_view output{reinterpret_cast<const char*>(output_bytes.data()), output_bytes.size()};
205+
// PHP doesn't include trailing whitespace
206+
if (!output.empty() && vk::is_ascii_whitespace(output.back())) {
207+
output.remove_suffix(1);
208+
}
209+
210+
const auto pos{output.rfind('\n')};
211+
const auto last_line_view{vk::rstrip_ascii_whitespace(output.substr(pos == std::string_view::npos ? 0 : (pos + 1)))};
212+
last_line = {last_line_view.data(), static_cast<string::size_type>(last_line_view.size())};
213+
214+
if (output_mixed.is_array()) {
215+
std::ranges::for_each(output | std::views::split('\n'), [&output_arr = output_mixed.as_array()](auto rng) noexcept {
216+
const auto line_view{vk::rstrip_ascii_whitespace(std::string_view{rng})};
217+
output_arr.emplace_back(string{line_view.data(), static_cast<string::size_type>(line_view.size())});
218+
});
219+
}
220+
}};
221+
222+
auto expected{kphp::system::exec({cmd.c_str(), cmd.size()}, output_handler)};
223+
if (!expected) [[unlikely]] {
224+
return false;
225+
}
226+
227+
if (exit_code) {
228+
(*exit_code).get() = static_cast<int64_t>(*expected);
229+
}
230+
231+
return last_line;
164232
}
165233

166-
inline Optional<string> f$exec([[maybe_unused]] const string& command, [[maybe_unused]] mixed& output,
167-
[[maybe_unused]] int64_t& result_code = SystemInstanceState::get().result_code_dummy) noexcept {
168-
kphp::log::error("call to unsupported function");
234+
inline Optional<string> f$exec(const string& cmd) noexcept {
235+
mixed output_arr{};
236+
return f$exec(cmd, output_arr);
169237
}
170238

171239
inline string f$get_engine_version() noexcept {

tests/phpt/dl/1040_exec.php

Lines changed: 1 addition & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,6 @@
1-
@ok k2_skip
1+
@ok
22
<?php
33

4-
function test_system_call() {
5-
$status = 0;
6-
$last_line = system('');
7-
echo "Returned with status: $status, last_line: $last_line";
8-
9-
$last_line = system('', $status);
10-
echo "Returned with status: $status, last_line: $last_line";
11-
12-
$last_line = system('echo "foo"');
13-
echo "Returned with status: $status, last_line: $last_line";
14-
15-
$last_line = system('echo "foo"', $status);
16-
echo "Returned with status: $status, last_line: $last_line";
17-
18-
$last_line = system('echo "foo "', $status);
19-
echo "Returned with status: $status, last_line: $last_line";
20-
21-
$last_line = system('echo "foo\t"', $status);
22-
echo "Returned with status: $status, last_line: $last_line";
23-
24-
$last_line = system('echo "foo\n"', $status);
25-
echo "Returned with status: $status, last_line: $last_line";
26-
27-
$last_line = system('echo "foo\n "', $status);
28-
echo "Returned with status: $status, last_line: $last_line";
29-
30-
$last_line = system('echo "foo\n\t"', $status);
31-
echo "Returned with status: $status, last_line: $last_line";
32-
33-
$last_line = system('echo "foo \nqux bar \nquz foo \n baz foo qux "', $status);
34-
echo "Returned with status: $status, last_line: ";
35-
var_dump($last_line);
36-
}
37-
384
function test_exec_call() {
395
$status = 0;
406
$output = [];
@@ -89,55 +55,6 @@ function test_exec_preserve_input() {
8955
var_dump($output);
9056
}
9157

92-
function test_passthru_call() {
93-
$status = 0;
94-
$result = passthru('');
95-
echo "Returned with status: $status, result: $result";
96-
97-
$result = passthru('', $status);
98-
echo "Returned with status: $status, result: $result";
99-
100-
$result = passthru('echo "foo"');
101-
echo "Returned with status: $status, result: $result";
102-
103-
$result = passthru('echo "foo"', $status);
104-
echo "Returned with status: $status, result: $result";
105-
106-
$result = passthru('echo "foo "', $status);
107-
echo "Returned with status: $status, result: $result";
108-
109-
$result = passthru('echo "foo\t"', $status);
110-
echo "Returned with status: $status, result: $result";
111-
112-
$result = passthru('echo "foo\n"', $status);
113-
echo "Returned with status: $status, result: $result";
114-
115-
$result = passthru('echo "foo\n "', $status);
116-
echo "Returned with status: $status, result: $result";
117-
118-
$result = passthru('echo "foo\n\t"', $status);
119-
echo "Returned with status: $status, result: $result";
120-
121-
$result = passthru('echo "foo \nqux bar \nquz foo \n baz foo qux "', $status);
122-
echo "Returned with status: $status, result: ";
123-
var_dump($result);
124-
}
125-
126-
function test_system_calls_errors() {
127-
$status = 0;
128-
$last_line = system('mv /unknown/file/here', $status);
129-
echo "Returned with status: $status, last_line: ";
130-
var_dump($last_line);
131-
132-
$last_line = system('/call/to/unknown/executable', $status);
133-
echo "Returned with status: $status, last_line: ";
134-
var_dump($last_line);
135-
136-
$last_line = system('', $status);
137-
echo "Returned with status: $status, last_line: ";
138-
var_dump($last_line);
139-
}
140-
14158
function test_exec_calls_errors() {
14259
$status = 0;
14360
$output = [];
@@ -158,25 +75,6 @@ function test_exec_calls_errors() {
15875
var_dump($last_line);
15976
}
16077

161-
function test_system_passthru_errors() {
162-
$status = 0;
163-
$result = passthru('mv /unknown/file/here', $status);
164-
echo "Returned with status: $status, result: ";
165-
var_dump($result);
166-
167-
$result = passthru('/call/to/unknown/executable', $status);
168-
echo "Returned with status: $status, result: ";
169-
var_dump($result);
170-
171-
$result = passthru('', $status);
172-
echo "Returned with status: $status, result: ";
173-
var_dump($result);
174-
}
175-
176-
// test_system_call();
177-
// test_system_calls_errors();
17878
test_exec_call();
17979
test_exec_preserve_input();
180-
test_passthru_call();
18180
test_exec_calls_errors();
182-
test_system_passthru_errors();

0 commit comments

Comments
 (0)