Skip to content

Commit 4793d93

Browse files
committed
[android] Implement Process and modify TestProcess to pass.
Until recent API levels, Android haven't provide posix_spawn and other functions from that family, so Process was not going to work with the path used for the POSIX OS. Instead of creating an specific path for Android, which will probably be forgotten by other changes, we implement a subset of the posix_spawn family of functions for Android, using fork and exec underneath, and other functions from the POSIX APIs. The already code in Process do not have to be modified. The tests have to be modified slightly for working on Android. Many changes are related to the environment needing the key LD_LIBRARY_PATH, or the xdgTestHelper will not be able to start in some of the tests. The other change is using NSTemporaryDirectory() instead of a hardcoded `/tmp`, which doesn't exist in Android (the code also removes the last slash, to match the original code expectations). It also includes a small fix for when launch() is used and a error is thrown which is not related to the current working directory. Before the error was ignored silently, and now it will fatalError.
1 parent 7e67c02 commit 4793d93

File tree

2 files changed

+169
-8
lines changed

2 files changed

+169
-8
lines changed

Foundation/Process.swift

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
88
//
99

10-
#if !os(Android) // not available
1110
import CoreFoundation
1211

1312
extension Process {
@@ -287,6 +286,8 @@ open class Process: NSObject {
287286
default:
288287
fatalError("Process: The specified working directory cannot be set.")
289288
}
289+
} else {
290+
fatalError(String(describing: nserror))
290291
}
291292
} catch {
292293
fatalError(String(describing: error))
@@ -608,7 +609,7 @@ open class Process: NSObject {
608609
}
609610

610611
var taskSocketPair : [Int32] = [0, 0]
611-
#if os(macOS) || os(iOS)
612+
#if os(macOS) || os(iOS) || os(Android)
612613
socketpair(AF_UNIX, SOCK_STREAM, 0, &taskSocketPair)
613614
#else
614615
socketpair(AF_UNIX, Int32(SOCK_STREAM.rawValue), 0, &taskSocketPair)
@@ -978,4 +979,133 @@ private func posix(_ code: Int32) {
978979
default: fatalError("POSIX command failed with error: \(code)")
979980
}
980981
}
982+
983+
#if os(Android)
984+
// Android doesn't provide posix_spawn APIs until recent API level, so we cannot
985+
// depend on them, but we can imitate the API, and perform the same work.
986+
987+
private struct posix_spawn_file_actions_t {
988+
enum Action {
989+
case dup2(old: Int32, new: Int32)
990+
case close(fd: Int32)
991+
}
992+
993+
private(set) var actions: [Action] = []
994+
995+
mutating func add(_ action: Action) {
996+
actions.append(action)
997+
}
998+
}
999+
1000+
private struct posix_spawnattr_t {}
1001+
1002+
private func posix_spawn_file_actions_init(_ actions: inout posix_spawn_file_actions_t) -> Int32 {
1003+
// NOTE: Nothing to do here, the struct initializes itself.
1004+
return 0
1005+
}
1006+
1007+
@discardableResult
1008+
private func posix_spawn_file_actions_destroy(_ actions: inout posix_spawn_file_actions_t) -> Int32 {
1009+
// NOTE: Nothing to do here, the struct handles its own memory.
1010+
return 0
1011+
}
1012+
1013+
private func posix_spawn_file_actions_adddup2(_ actions: inout posix_spawn_file_actions_t, _ old: Int32, _ new: Int32) -> Int32 {
1014+
actions.add(.dup2(old: old, new: new))
1015+
return 0
1016+
}
1017+
1018+
private func posix_spawn_file_actions_addclose(_ actions: inout posix_spawn_file_actions_t, _ fd: Int32) -> Int32 {
1019+
actions.add(.close(fd: fd))
1020+
return 0
1021+
}
1022+
1023+
private func posix_spawn(_ pidPtr: UnsafeMutablePointer<pid_t>?,
1024+
_ launchPath: UnsafePointer<Int8>?,
1025+
_ actions: UnsafeMutablePointer<posix_spawn_file_actions_t>?,
1026+
_ attrp: UnsafeMutablePointer<posix_spawnattr_t>?,
1027+
_ argv: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?,
1028+
_ envp: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?) -> Int32 {
1029+
// TODO: We completely ignore attrp, because at the moment, the only
1030+
// invocation doesn't pass a value.
1031+
1032+
// Block signals during this fork/execv dance.
1033+
var signalSet = sigset_t()
1034+
sigfillset(&signalSet)
1035+
var oldmask = sigset_t()
1036+
guard sigprocmask(SIG_BLOCK, &signalSet, &oldmask) == 0 else {
1037+
fatalError("sigprocmask() call failed: \(errno)")
1038+
}
1039+
1040+
let pid = fork()
1041+
guard pid == 0 else {
1042+
// This is the parent. fork call might have been successful or not.
1043+
1044+
// Unblock signals.
1045+
guard sigprocmask(SIG_SETMASK, &oldmask, nil) == 0 else {
1046+
fatalError("sigprocmask() call failed: \(errno)")
1047+
}
1048+
1049+
if pid < 0 {
1050+
return pid
1051+
} else {
1052+
if let pidPtr = pidPtr {
1053+
pidPtr.pointee = pid
1054+
}
1055+
return 0
1056+
}
1057+
}
1058+
1059+
// This is the child.
1060+
1061+
// Clean up the parent signal handlers
1062+
for idx in 1..<(NSIG as Int32) {
1063+
// Seems that SIGKILL/SIGSTOP are sometimes silently ignored, and
1064+
// sometimes return EINVAL. Since one cannot change the handlers anyway,
1065+
// skip them.
1066+
if idx == SIGKILL || idx == SIGSTOP {
1067+
continue
1068+
}
1069+
var sigAction = sigaction()
1070+
guard sigaction(idx, nil, &sigAction) == 0 else {
1071+
exit(127)
1072+
}
1073+
1074+
// One cannot compare @convention(c) function pointers without the
1075+
// conversion.
1076+
if unsafeBitCast(sigAction.sa_handler, to: UnsafeRawPointer?.self)
1077+
!= unsafeBitCast(SIG_IGN, to: UnsafeRawPointer?.self) {
1078+
sigAction.sa_handler = SIG_DFL;
1079+
guard sigaction(idx, &sigAction, nil) == 0 else {
1080+
exit(127)
1081+
}
1082+
}
1083+
}
1084+
1085+
// Perform the actions
1086+
if let actions = actions?.pointee {
1087+
for action in actions.actions {
1088+
switch action {
1089+
case .dup2(let old, let new):
1090+
guard dup2(old, new) >= 0 else {
1091+
exit(127)
1092+
}
1093+
case .close(let fd):
1094+
guard close(fd) == 0 else {
1095+
exit(127)
1096+
}
1097+
}
1098+
}
1099+
}
1100+
1101+
// Unblock the signals
1102+
guard sigprocmask(SIG_SETMASK, &oldmask, nil) == 0 else {
1103+
fatalError("sigprocmask() call failed: \(errno)")
1104+
}
1105+
1106+
// If execv fails, we will simply exit 127 as the standard says.
1107+
execve(launchPath, argv, envp ?? environ)
1108+
exit(127)
1109+
// no need for return here
1110+
}
9811111
#endif

TestFoundation/TestProcess.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
88
//
99

10-
#if !os(Android)
1110
class TestProcess : XCTestCase {
1211

1312
func test_exit0() throws {
@@ -155,6 +154,13 @@ class TestProcess : XCTestCase {
155154
// Clear the environment to stop the malloc debug flags used in Xcode debug being
156155
// set in the subprocess.
157156
process.environment = [:]
157+
#if os(Android)
158+
// In Android, we have to provide at least an LD_LIBRARY_PATH, or
159+
// xdgTestHelper will not be able to find the Swift libraries.
160+
if let ldLibraryPath = ProcessInfo.processInfo.environment["LD_LIBRARY_PATH"] {
161+
process.environment?["LD_LIBRARY_PATH"] = ldLibraryPath
162+
}
163+
#endif
158164
try? process.run()
159165
process.waitUntilExit()
160166
XCTAssertEqual(process.terminationStatus, 1)
@@ -231,7 +237,16 @@ class TestProcess : XCTestCase {
231237
}
232238

233239
func test_current_working_directory() {
234-
let tmpDir = "/tmp" //.standardizingPath
240+
let tmpDir = { () -> String in
241+
// NSTemporaryDirectory might return a final slash, but
242+
// FileManager.currentDirectoryPath seems to avoid it.
243+
var dir = NSTemporaryDirectory()
244+
if dir.hasSuffix("/") && dir != "/",
245+
let idx = dir.lastIndex(of: "/") {
246+
dir.remove(at: idx)
247+
}
248+
return dir
249+
}()
235250

236251
let fm = FileManager.default
237252
let previousWorkingDirectory = fm.currentDirectoryPath
@@ -637,7 +652,16 @@ internal func runTask(_ arguments: [String], environment: [String: String]? = ni
637652
process.arguments = arguments
638653
// Darwin Foundation doesnt allow .environment to be set to nil although the documentation
639654
// says it is an optional. https://developer.apple.com/documentation/foundation/process/1409412-environment
640-
if let e = environment {
655+
if var e = environment {
656+
#if os(Android)
657+
// In Android, we have to provide at least an LD_LIBRARY_PATH, or
658+
// xdgTestHelper will not be able to find the Swift libraries.
659+
if e["LD_LIBRARY_PATH"] == nil {
660+
if let ldLibraryPath = ProcessInfo.processInfo.environment["LD_LIBRARY_PATH"] {
661+
e["LD_LIBRARY_PATH"] = ldLibraryPath
662+
}
663+
}
664+
#endif
641665
process.environment = e
642666
}
643667

@@ -701,9 +725,16 @@ private func parseEnv(_ env: String) throws -> [String: String] {
701725
guard let range = line.range(of: "=") else {
702726
throw Error.InvalidEnvironmentVariable(line)
703727
}
704-
result[String(line[..<range.lowerBound])] = String(line[range.upperBound...])
728+
let key = String(line[..<range.lowerBound])
729+
#if os(Android)
730+
// NOTE: this works because the results of parseEnv are never checked
731+
// against the parent environment, where this key will be set. If that
732+
// ever happen, the checks should be changed.
733+
if key == "LD_LIBRARY_PATH" {
734+
continue
735+
}
736+
#endif
737+
result[key] = String(line[range.upperBound...])
705738
}
706739
return result
707740
}
708-
#endif // !os(Android)
709-

0 commit comments

Comments
 (0)