Skip to content

Commit 769ea70

Browse files
committed
Use NSIG_MAX insead of a constant
1 parent 0feb84b commit 769ea70

File tree

4 files changed

+122
-90
lines changed

4 files changed

+122
-90
lines changed

Sources/Subprocess/Platforms/Subprocess+Unix.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ extension Execution {
115115
if shouldSendToProcessGroup {
116116
// pidfd_send_signal does not support sending signal to process group
117117
try _kill(pid, signal: signal)
118-
} else {
118+
} else if processIdentifier.processDescriptor > 0 {
119119
guard _pidfd_send_signal(
120120
processIdentifier.processDescriptor,
121121
signal.rawValue
@@ -125,6 +125,9 @@ extension Execution {
125125
underlyingError: .init(rawValue: errno)
126126
)
127127
}
128+
} else {
129+
// fall back to _kill()
130+
try _kill(pid, signal: signal)
128131
}
129132
#else
130133
try _kill(pid, signal: signal)

Sources/_SubprocessCShims/process_shims.c

Lines changed: 62 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 0xFFFF
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
277288
#ifndef SYS_clone3
@@ -320,10 +331,18 @@ static int _subprocess_make_critical_mask(sigset_t *old_mask) {
320331
} \
321332
} while(0)
322333

323-
#if __DARWIN_NSIG
334+
#if __DARWIN_NSIG /* Darwin */
324335
# define _SUBPROCESS_SIG_MAX __DARWIN_NSIG
325-
#else
326-
# define _SUBPROCESS_SIG_MAX 32
336+
#elif defined(NSIG_MAX) /* POSIX issue 8 */
337+
# define _SUBPROCESS_SIG_MAX NSIG_MAX
338+
#elif defined(_SIG_MAXSIG) /* FreeBSD */
339+
# define _SUBPROCESS_SIG_MAX _SIG_MAXSIG
340+
#elif defined(_SIGMAX) /* QNX */
341+
# define _SUBPROCESS_SIG_MAX (_SIGMAX + 1)
342+
#elif defined(NSIG) /* 99% of everything else */
343+
# define _SUBPROCESS_SIG_MAX NSIG
344+
#else /* Last resort */
345+
# define _SUBPROCESS_SIG_MAX (sizeof(sigset_t) * CHAR_BIT + 1)
327346
#endif
328347

329348
int _shims_snprintf(
@@ -343,46 +362,66 @@ static int _positive_int_parse(const char *str) {
343362
// No digits found
344363
return -1;
345364
}
346-
if (errno == ERANGE || val <= 0 || val > INT_MAX) {
365+
if (errno == ERANGE || value <= 0 || value > INT_MAX) {
347366
// Out of range
348367
return -1;
349368
}
350369
return (int)value;
351370
}
352371

353-
static int _highest_possibly_open_fd_dir(const char *fd_dir) {
372+
// Linux-specific version that uses syscalls directly and doesn't allocate heap memory.
373+
// Safe to use after vfork() and before execve()
374+
static int _highest_possibly_open_fd_dir_linux(const char *fd_dir) {
354375
int highest_fd_so_far = 0;
355-
DIR *dir_ptr = opendir(fd_dir);
356-
if (dir_ptr == NULL) {
376+
int dir_fd = open(fd_dir, O_RDONLY);
377+
if (dir_fd < 0) {
378+
// errno set by `open`.
357379
return -1;
358380
}
359381

360-
struct dirent *dir_entry = NULL;
361-
while ((dir_entry = readdir(dir_ptr)) != NULL) {
362-
char *entry_name = dir_entry->d_name;
363-
int number = _positive_int_parse(entry_name);
364-
if (number > (long)highest_fd_so_far) {
365-
highest_fd_so_far = number;
382+
// Buffer for directory entries - allocated on stack, no heap allocation
383+
char buffer[4096] = {0};
384+
long bytes_read = -1;
385+
386+
while ((bytes_read = getdents64(dir_fd, (struct dirent64 *)buffer, sizeof(buffer))) > 0) {
387+
if (bytes_read < 0) {
388+
if (errno == EINTR) {
389+
continue;
390+
} else {
391+
// `errno` set by getdents64.
392+
highest_fd_so_far = -1;
393+
goto error;
394+
}
395+
}
396+
long offset = 0;
397+
while (offset < bytes_read) {
398+
struct dirent64 *entry = (struct dirent64 *)(buffer + offset);
399+
400+
// Skip "." and ".." entries
401+
if (entry->d_name[0] != '.') {
402+
int number = _positive_int_parse(entry->d_name);
403+
if (number > highest_fd_so_far) {
404+
highest_fd_so_far = number;
405+
}
406+
}
407+
408+
offset += entry->d_reclen;
366409
}
367410
}
368411

369-
closedir(dir_ptr);
412+
error:
413+
close(dir_fd);
370414
return highest_fd_so_far;
371415
}
372416

373417
static int _highest_possibly_open_fd(void) {
374-
#if defined(__APPLE__)
375-
int hi = _highest_possibly_open_fd_dir("/dev/fd");
376-
if (hi < 0) {
377-
hi = getdtablesize();
378-
}
379-
#elif defined(__linux__)
380-
int hi = _highest_possibly_open_fd_dir("/proc/self/fd");
418+
#if defined(__linux__)
419+
int hi = _highest_possibly_open_fd_dir_linux("/dev/fd");
381420
if (hi < 0) {
382-
hi = getdtablesize();
421+
hi = sysconf(_SC_OPEN_MAX);
383422
}
384423
#else
385-
int hi = getdtablesize();
424+
int hi = sysconf(_SC_OPEN_MAX);
386425
#endif
387426
return hi;
388427
}

Tests/SubprocessTests/SubprocessTests+Linux.swift

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

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

@@ -124,15 +99,6 @@ fileprivate enum ProcessState: String {
12499
case stopped = "T"
125100
}
126101

127-
fileprivate struct ProcessStateError: Error, CustomStringConvertible {
128-
let expectedState: ProcessState
129-
let actualState: ProcessState
130-
131-
var description: String {
132-
"Process did not transition to \(expectedState) state, but was actually \(actualState)"
133-
}
134-
}
135-
136102
extension Execution {
137103
fileprivate func state() throws -> ProcessState {
138104
let processStatusFile = "/proc/\(processIdentifier.value)/status"
@@ -158,7 +124,7 @@ extension Execution {
158124
}
159125
}
160126

161-
func waitForCondition(timeout: Duration, comment: Comment, _ evaluateCondition: () throws -> Bool) async throws {
127+
func waitForCondition(timeout: Duration, _ evaluateCondition: () throws -> Bool) async throws {
162128
var currentCondition = try evaluateCondition()
163129
let deadline = ContinuousClock.now + timeout
164130
while ContinuousClock.now < deadline {
@@ -169,13 +135,11 @@ func waitForCondition(timeout: Duration, comment: Comment, _ evaluateCondition:
169135
currentCondition = try evaluateCondition()
170136
}
171137
struct TimeoutError: Error, CustomStringConvertible {
172-
let timeout: Duration
173-
let comment: Comment
174138
var description: String {
175-
comment.description.replacingOccurrences(of: "$$", with: "\(timeout)")
139+
"Timed out waiting for condition to be true"
176140
}
177141
}
178-
throw TimeoutError(timeout: timeout, comment: comment)
142+
throw TimeoutError()
179143
}
180144

181145
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)