Skip to content

Commit 6176f07

Browse files
committed
Use NSIG_MAX insead of a constant
1 parent c345e8d commit 6176f07

File tree

4 files changed

+146
-95
lines changed

4 files changed

+146
-95
lines changed

Sources/Subprocess/Platforms/Subprocess+Unix.swift

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,24 @@ extension Execution {
116116
// pidfd_send_signal does not support sending signal to process group
117117
try _kill(pid, signal: signal)
118118
} else {
119-
guard _pidfd_send_signal(
119+
// Try pidfd_send_signal first
120+
if _pidfd_send_signal(
120121
processIdentifier.processDescriptor,
121122
signal.rawValue
122-
) == 0 else {
123-
throw SubprocessError(
124-
code: .init(.failedToSendSignal(signal.rawValue)),
125-
underlyingError: .init(rawValue: errno)
126-
)
123+
) == 0 {
124+
return
125+
} else {
126+
if errno == ENOSYS {
127+
// pidfd_send_signal is not implemented in this system
128+
// fallback to _kill()
129+
try _kill(pid, signal: signal)
130+
} else {
131+
// Throw all other errors
132+
throw SubprocessError(
133+
code: .init(.failedToSendSignal(signal.rawValue)),
134+
underlyingError: .init(rawValue: errno)
135+
)
136+
}
127137
}
128138
}
129139
#else

Sources/_SubprocessCShims/process_shims.c

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include <pthread.h>
3737
#include <dirent.h>
3838
#include <stdio.h>
39+
#include <limits.h>
3940

4041
#if __has_include(<linux/close_range.h>)
4142
#include <linux/close_range.h>
@@ -272,6 +273,16 @@ int _pidfd_open(pid_t pid) {
272273
return syscall(SYS_pidfd_open, pid, 0);
273274
}
274275

276+
// SYS_pidfd_send_signal is only defined on Linux Kernel 5.1 and above
277+
// Define our dummy value if it's not available
278+
#ifndef SYS_pidfd_send_signal
279+
#define SYS_pidfd_send_signal 424
280+
#endif
281+
282+
int _pidfd_send_signal(int pidfd, int signal) {
283+
return syscall(SYS_pidfd_send_signal, pidfd, signal, NULL, 0);
284+
}
285+
275286
// SYS_clone3 is only defined on Linux Kernel 5.3 and above
276287
// Define our dummy value if it's not available (as is the case with Musl libc)
277288
#ifndef SYS_clone3
@@ -308,6 +319,18 @@ static int _clone3(int *pidfd) {
308319
return syscall(SYS_clone3, &args, sizeof(args));
309320
}
310321

322+
struct linux_dirent64 {
323+
unsigned long d_ino;
324+
unsigned long d_off;
325+
unsigned short d_reclen;
326+
unsigned char d_type;
327+
char d_name[];
328+
};
329+
330+
static int _getdents64(int fd, struct linux_dirent64 *dirp, size_t nbytes) {
331+
return syscall(SYS_getdents64, fd, dirp, nbytes);
332+
}
333+
311334
static pthread_mutex_t _subprocess_fork_lock = PTHREAD_MUTEX_INITIALIZER;
312335

313336
static int _subprocess_make_critical_mask(sigset_t *old_mask) {
@@ -335,10 +358,18 @@ static int _subprocess_make_critical_mask(sigset_t *old_mask) {
335358
} \
336359
} while(0)
337360

338-
#if __DARWIN_NSIG
361+
#if __DARWIN_NSIG /* Darwin */
339362
# define _SUBPROCESS_SIG_MAX __DARWIN_NSIG
340-
#else
341-
# define _SUBPROCESS_SIG_MAX 32
363+
#elif defined(NSIG_MAX) /* POSIX issue 8 */
364+
# define _SUBPROCESS_SIG_MAX NSIG_MAX
365+
#elif defined(_SIG_MAXSIG) /* FreeBSD */
366+
# define _SUBPROCESS_SIG_MAX _SIG_MAXSIG
367+
#elif defined(_SIGMAX) /* QNX */
368+
# define _SUBPROCESS_SIG_MAX (_SIGMAX + 1)
369+
#elif defined(NSIG) /* 99% of everything else */
370+
# define _SUBPROCESS_SIG_MAX NSIG
371+
#else /* Last resort */
372+
# define _SUBPROCESS_SIG_MAX (sizeof(sigset_t) * CHAR_BIT + 1)
342373
#endif
343374

344375
int _shims_snprintf(
@@ -358,46 +389,66 @@ static int _positive_int_parse(const char *str) {
358389
// No digits found
359390
return -1;
360391
}
361-
if (errno == ERANGE || val <= 0 || val > INT_MAX) {
392+
if (errno == ERANGE || value <= 0 || value > INT_MAX) {
362393
// Out of range
363394
return -1;
364395
}
365396
return (int)value;
366397
}
367398

368-
static int _highest_possibly_open_fd_dir(const char *fd_dir) {
399+
// Linux-specific version that uses syscalls directly and doesn't allocate heap memory.
400+
// Safe to use after vfork() and before execve()
401+
static int _highest_possibly_open_fd_dir_linux(const char *fd_dir) {
369402
int highest_fd_so_far = 0;
370-
DIR *dir_ptr = opendir(fd_dir);
371-
if (dir_ptr == NULL) {
403+
int dir_fd = open(fd_dir, O_RDONLY);
404+
if (dir_fd < 0) {
405+
// errno set by `open`.
372406
return -1;
373407
}
374408

375-
struct dirent *dir_entry = NULL;
376-
while ((dir_entry = readdir(dir_ptr)) != NULL) {
377-
char *entry_name = dir_entry->d_name;
378-
int number = _positive_int_parse(entry_name);
379-
if (number > (long)highest_fd_so_far) {
380-
highest_fd_so_far = number;
409+
// Buffer for directory entries - allocated on stack, no heap allocation
410+
char buffer[4096] = {0};
411+
long bytes_read = -1;
412+
413+
while ((bytes_read = _getdents64(dir_fd, (struct linux_dirent64 *)buffer, sizeof(buffer))) > 0) {
414+
if (bytes_read < 0) {
415+
if (errno == EINTR) {
416+
continue;
417+
} else {
418+
// `errno` set by _getdents64.
419+
highest_fd_so_far = -1;
420+
goto error;
421+
}
422+
}
423+
long offset = 0;
424+
while (offset < bytes_read) {
425+
struct linux_dirent64 *entry = (struct linux_dirent64 *)(buffer + offset);
426+
427+
// Skip "." and ".." entries
428+
if (entry->d_name[0] != '.') {
429+
int number = _positive_int_parse(entry->d_name);
430+
if (number > highest_fd_so_far) {
431+
highest_fd_so_far = number;
432+
}
433+
}
434+
435+
offset += entry->d_reclen;
381436
}
382437
}
383438

384-
closedir(dir_ptr);
439+
error:
440+
close(dir_fd);
385441
return highest_fd_so_far;
386442
}
387443

388444
static int _highest_possibly_open_fd(void) {
389-
#if defined(__APPLE__)
390-
int hi = _highest_possibly_open_fd_dir("/dev/fd");
391-
if (hi < 0) {
392-
hi = getdtablesize();
393-
}
394-
#elif defined(__linux__)
395-
int hi = _highest_possibly_open_fd_dir("/proc/self/fd");
445+
#if defined(__linux__)
446+
int hi = _highest_possibly_open_fd_dir_linux("/dev/fd");
396447
if (hi < 0) {
397-
hi = getdtablesize();
448+
hi = sysconf(_SC_OPEN_MAX);
398449
}
399450
#else
400-
int hi = getdtablesize();
451+
int hi = sysconf(_SC_OPEN_MAX);
401452
#endif
402453
return hi;
403454
}

Tests/SubprocessTests/SubprocessTests+Linux.swift

Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -64,56 +64,31 @@ struct SubprocessLinuxTests {
6464
}
6565

6666
@Test func testSuspendResumeProcess() async throws {
67-
let result = try await Subprocess.run(
67+
_ = try await Subprocess.run(
6868
// This will intentionally hang
6969
.path("/usr/bin/sleep"),
7070
arguments: ["infinity"],
71-
output: .discarded,
7271
error: .discarded
73-
) { subprocess -> Error? in
74-
do {
75-
try await tryFinally {
76-
// First suspend the process
77-
try subprocess.send(signal: .suspend)
78-
try await waitForCondition(timeout: .seconds(30), comment: "Process did not transition from running to stopped state after $$") {
79-
let state = try subprocess.state()
80-
switch state {
81-
case .running:
82-
return false
83-
case .zombie:
84-
throw ProcessStateError(expectedState: .stopped, actualState: state)
85-
case .stopped, .sleeping, .uninterruptibleWait:
86-
return true
87-
}
88-
}
89-
// Now resume the process
90-
try subprocess.send(signal: .resume)
91-
try await waitForCondition(timeout: .seconds(30), comment: "Process did not transition from stopped to running state after $$") {
92-
let state = try subprocess.state()
93-
switch state {
94-
case .running, .sleeping, .uninterruptibleWait:
95-
return true
96-
case .zombie:
97-
throw ProcessStateError(expectedState: .running, actualState: state)
98-
case .stopped:
99-
return false
100-
}
101-
}
102-
} finally: { error in
103-
// Now kill the process
104-
try subprocess.send(signal: error != nil ? .kill : .terminate)
72+
) { subprocess, standardOutput in
73+
try await tryFinally {
74+
// First suspend the process
75+
try subprocess.send(signal: .suspend)
76+
try await waitForCondition(timeout: .seconds(30)) {
77+
let state = try subprocess.state()
78+
return state == .stopped
10579
}
106-
return nil
107-
} catch {
108-
return error
80+
// Now resume the process
81+
try subprocess.send(signal: .resume)
82+
try await waitForCondition(timeout: .seconds(30)) {
83+
let state = try subprocess.state()
84+
return state == .running
85+
}
86+
} finally: { error in
87+
// Now kill the process
88+
try subprocess.send(signal: error != nil ? .kill : .terminate)
89+
for try await _ in standardOutput {}
10990
}
11091
}
111-
if let error = result.value {
112-
#expect(result.terminationStatus == .unhandledException(SIGKILL))
113-
throw error
114-
} else {
115-
#expect(result.terminationStatus == .unhandledException(SIGTERM))
116-
}
11792
}
11893

11994
@Test func testUniqueProcessIdentifier() async throws {
@@ -141,15 +116,6 @@ fileprivate enum ProcessState: String {
141116
case stopped = "T"
142117
}
143118

144-
fileprivate struct ProcessStateError: Error, CustomStringConvertible {
145-
let expectedState: ProcessState
146-
let actualState: ProcessState
147-
148-
var description: String {
149-
"Process did not transition to \(expectedState) state, but was actually \(actualState)"
150-
}
151-
}
152-
153119
extension Execution {
154120
fileprivate func state() throws -> ProcessState {
155121
let processStatusFile = "/proc/\(processIdentifier.value)/status"
@@ -175,7 +141,7 @@ extension Execution {
175141
}
176142
}
177143

178-
func waitForCondition(timeout: Duration, comment: Comment, _ evaluateCondition: () throws -> Bool) async throws {
144+
func waitForCondition(timeout: Duration, _ evaluateCondition: () throws -> Bool) async throws {
179145
var currentCondition = try evaluateCondition()
180146
let deadline = ContinuousClock.now + timeout
181147
while ContinuousClock.now < deadline {
@@ -186,13 +152,11 @@ func waitForCondition(timeout: Duration, comment: Comment, _ evaluateCondition:
186152
currentCondition = try evaluateCondition()
187153
}
188154
struct TimeoutError: Error, CustomStringConvertible {
189-
let timeout: Duration
190-
let comment: Comment
191155
var description: String {
192-
comment.description.replacingOccurrences(of: "$$", with: "\(timeout)")
156+
"Timed out waiting for condition to be true"
193157
}
194158
}
195-
throw TimeoutError(timeout: timeout, comment: comment)
159+
throw TimeoutError()
196160
}
197161

198162
func tryFinally(_ work: () async throws -> (), finally: (Error?) async throws -> ()) async throws {

Tests/SubprocessTests/SubprocessTests+Unix.swift

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -953,21 +953,21 @@ extension SubprocessUnixTests {
953953
try FileManager.default.removeItem(at: testFilePath)
954954
}
955955

956-
@Test func testDoesNotInheritRandomFileDescriptorsByDefault() async throws {
956+
@Test(.requiresBash) func testDoesNotInheritRandomFileDescriptorsByDefault() async throws {
957957
// This tests makes sure POSIX_SPAWN_CLOEXEC_DEFAULT works on all platforms
958958
let pipe = try FileDescriptor.ssp_pipe()
959959
defer {
960960
close(pipe.readEnd.rawValue)
961-
close(pipe.writeEnd.rawValue)
962961
}
963962
let writeFd = pipe.writeEnd.rawValue
964-
let result = try await Subprocess.run(
965-
.path("/bin/sh"),
966-
arguments: ["-c", "echo hello from child >&\(writeFd); echo wrote into \(writeFd), echo exit code $?"],
967-
output: .string,
968-
error: .string
969-
)
970-
close(pipe.writeEnd.rawValue)
963+
let result = try await pipe.writeEnd.closeAfter {
964+
return try await Subprocess.run(
965+
.path("/bin/bash"),
966+
arguments: ["-c", "echo hello from child >&\(writeFd); echo wrote into \(writeFd), echo exit code $?"],
967+
output: .string(limit: .max),
968+
error: .string(limit: .max)
969+
)
970+
}
971971

972972
#expect(result.terminationStatus.isSuccess)
973973
#expect(
@@ -984,6 +984,32 @@ extension SubprocessUnixTests {
984984
}
985985

986986
// MARK: - Utils
987+
extension FileDescriptor {
988+
/// Runs a closure and then closes the FileDescriptor, even if an error occurs.
989+
///
990+
/// - Parameter body: The closure to run.
991+
/// If the closure throws an error,
992+
/// this method closes the file descriptor before it rethrows that error.
993+
///
994+
/// - Returns: The value returned by the closure.
995+
///
996+
/// If `body` throws an error
997+
/// or an error occurs while closing the file descriptor,
998+
/// this method rethrows that error.
999+
public func closeAfter<R>(_ body: () async throws -> R) async throws -> R {
1000+
// No underscore helper, since the closure's throw isn't necessarily typed.
1001+
let result: R
1002+
do {
1003+
result = try await body()
1004+
} catch {
1005+
_ = try? self.close() // Squash close error and throw closure's
1006+
throw error
1007+
}
1008+
try self.close()
1009+
return result
1010+
}
1011+
}
1012+
9871013
extension SubprocessUnixTests {
9881014
private func assertID(
9891015
withArgument argument: String,

0 commit comments

Comments
 (0)