Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee8968e2d6 | |||
| 4bc36019c2 | |||
| 1b537eb395 |
+1
-5
@@ -9,10 +9,6 @@ let package = Package(
|
|||||||
.macOS("14.2")
|
.macOS("14.2")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(name: "audiotee")
|
||||||
name: "audiotee",
|
|
||||||
swiftSettings: [
|
|
||||||
.define("ENABLE_TCC_SPI")
|
|
||||||
])
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -243,35 +243,13 @@ Info, error, and debug messages (useful for monitoring):
|
|||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
AudioTee requires system audio recording permissions to function. You can handle these permissions in two ways:
|
There is no provision in the code to pre-emptively check for the required `NSAudioCaptureUsageDescription` permission,
|
||||||
|
so you'll be prompted the first time AudioTee tries to record anything. If you want to check and/or request permissions ahead of time, check out [AudioCap's clever TCC probing approach](https://github.com/insidegui/AudioCap/blob/main/AudioCap/ProcessTap/AudioRecordingPermission.swift).
|
||||||
### Lazy permissions (default approach)
|
|
||||||
|
|
||||||
Simply run `./audiotee` and you'll be prompted for permissions the first time AudioTee tries to record audio from the tap. Note that some terminal emulators (at least `iTerm`) will **not** prompt at all, nor will the process fail: instead, AudioTee will happily run but will record a stream of empty data. The built in macOS terminal **does** prompt for permissions and blocks until granted.
|
|
||||||
|
|
||||||
### Explicit permissions management
|
|
||||||
|
|
||||||
Use the `--permissions` flag to check or request permissions ahead of time:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check current permission status
|
|
||||||
./audiotee --permissions
|
|
||||||
|
|
||||||
# Request permissions with user prompt
|
|
||||||
./audiotee --permissions --request
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the same caveat as above exists here regarding terminal emulators. If you know why, or how to fix it, please help out.
|
|
||||||
|
|
||||||
**Exit codes** indicate permission status, making this approach ideal for scripting:
|
|
||||||
- `0`: Permissions granted
|
|
||||||
- `1`: Permission status unknown
|
|
||||||
- `2`: Permissions denied
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [Apple Core Audio Taps Documentation](https://developer.apple.com/documentation/coreaudio/capturing-system-audio-with-core-audio-taps)
|
- [Apple Core Audio Taps Documentation](https://developer.apple.com/documentation/coreaudio/capturing-system-audio-with-core-audio-taps)
|
||||||
- [AudioCap Implementation](https://github.com/insidegui/AudioCap) - in particular, their awesome TCC probing approach to check for the audio capture permissions, which AudioTee lifts almost in its entirety. Thank you.
|
- [AudioCap Implementation](https://github.com/insidegui/AudioCap)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ struct AudioTee {
|
|||||||
var mute: Bool = false
|
var mute: Bool = false
|
||||||
var sampleRate: Double?
|
var sampleRate: Double?
|
||||||
var chunkDuration: Double = 0.2
|
var chunkDuration: Double = 0.2
|
||||||
var permissionsMode: Bool = false
|
|
||||||
var requestPermissions: Bool = false
|
|
||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
|
|
||||||
@@ -20,10 +18,6 @@ struct AudioTee {
|
|||||||
discussion: """
|
discussion: """
|
||||||
AudioTee captures system audio using Core Audio taps and streams it as structured output.
|
AudioTee captures system audio using Core Audio taps and streams it as structured output.
|
||||||
|
|
||||||
Permission modes:
|
|
||||||
• --permissions: Check current audio recording permissions
|
|
||||||
• --permissions --request: Request audio recording permissions
|
|
||||||
|
|
||||||
Output formats:
|
Output formats:
|
||||||
• json: Base64-encoded audio in JSON messages (safe for terminals)
|
• json: Base64-encoded audio in JSON messages (safe for terminals)
|
||||||
• binary: Raw binary audio with JSON metadata headers (efficient for pipes)
|
• binary: Raw binary audio with JSON metadata headers (efficient for pipes)
|
||||||
@@ -35,8 +29,6 @@ struct AudioTee {
|
|||||||
• mute: How to handle processes being tapped
|
• mute: How to handle processes being tapped
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
audiotee --permissions # Check audio recording permissions
|
|
||||||
audiotee --permissions --request # Request audio recording permissions
|
|
||||||
audiotee # Auto format, tap all processes
|
audiotee # Auto format, tap all processes
|
||||||
audiotee --format=json # Always use JSON format
|
audiotee --format=json # Always use JSON format
|
||||||
audiotee --format=binary # Always use binary format
|
audiotee --format=binary # Always use binary format
|
||||||
@@ -50,8 +42,6 @@ struct AudioTee {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Configure arguments
|
// Configure arguments
|
||||||
parser.addFlag(name: "permissions", help: "Check audio recording permissions")
|
|
||||||
parser.addFlag(name: "request", help: "Request permissions (use with --permissions)")
|
|
||||||
parser.addOption(name: "format", shortName: "f", help: "Output format", defaultValue: "auto")
|
parser.addOption(name: "format", shortName: "f", help: "Output format", defaultValue: "auto")
|
||||||
parser.addArrayOption(
|
parser.addArrayOption(
|
||||||
name: "include-processes",
|
name: "include-processes",
|
||||||
@@ -72,8 +62,6 @@ struct AudioTee {
|
|||||||
var audioTee = AudioTee()
|
var audioTee = AudioTee()
|
||||||
|
|
||||||
// Extract values
|
// Extract values
|
||||||
audioTee.permissionsMode = parser.getFlag("permissions")
|
|
||||||
audioTee.requestPermissions = parser.getFlag("request")
|
|
||||||
audioTee.format = try parser.getValue("format", as: OutputFormat.self)
|
audioTee.format = try parser.getValue("format", as: OutputFormat.self)
|
||||||
audioTee.includeProcesses = try parser.getArrayValue("include-processes", as: Int32.self)
|
audioTee.includeProcesses = try parser.getArrayValue("include-processes", as: Int32.self)
|
||||||
audioTee.excludeProcesses = try parser.getArrayValue("exclude-processes", as: Int32.self)
|
audioTee.excludeProcesses = try parser.getArrayValue("exclude-processes", as: Int32.self)
|
||||||
@@ -108,21 +96,9 @@ struct AudioTee {
|
|||||||
throw ArgumentParserError.validationFailed(
|
throw ArgumentParserError.validationFailed(
|
||||||
"Cannot specify both --include-processes and --exclude-processes")
|
"Cannot specify both --include-processes and --exclude-processes")
|
||||||
}
|
}
|
||||||
|
|
||||||
if requestPermissions && !permissionsMode {
|
|
||||||
throw ArgumentParserError.validationFailed(
|
|
||||||
"--request can only be used with --permissions")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() throws {
|
func run() throws {
|
||||||
// Handle permissions mode
|
|
||||||
if permissionsMode {
|
|
||||||
let permissionsHandler = PermissionsHandler(shouldRequest: requestPermissions)
|
|
||||||
permissionsHandler.handle() // This will exit with appropriate code
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with normal audio tapping functionality
|
|
||||||
setupSignalHandlers()
|
setupSignalHandlers()
|
||||||
|
|
||||||
Logger.info("Starting AudioTee...")
|
Logger.info("Starting AudioTee...")
|
||||||
@@ -224,7 +200,7 @@ struct AudioTee {
|
|||||||
// Helper for stderr output
|
// Helper for stderr output
|
||||||
var standardError = FileHandle.standardError
|
var standardError = FileHandle.standardError
|
||||||
|
|
||||||
extension FileHandle: @retroactive TextOutputStream {
|
extension FileHandle: TextOutputStream {
|
||||||
public func write(_ string: String) {
|
public func write(_ string: String) {
|
||||||
let data = Data(string.utf8)
|
let data = Data(string.utf8)
|
||||||
self.write(data)
|
self.write(data)
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import CoreFoundation
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// Handles audio recording permissions for the CLI, including checking status and requesting permissions.
|
|
||||||
/// Uses exit codes to communicate permission status:
|
|
||||||
/// - 0: granted (authorized)
|
|
||||||
/// - 1: unknown
|
|
||||||
/// - 2: denied
|
|
||||||
struct PermissionsHandler {
|
|
||||||
private let shouldRequest: Bool
|
|
||||||
|
|
||||||
init(shouldRequest: Bool) {
|
|
||||||
self.shouldRequest = shouldRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles the permissions workflow and exits with appropriate exit code
|
|
||||||
func handle() -> Never {
|
|
||||||
let permissionHandler = AudioRecordingPermission()
|
|
||||||
|
|
||||||
if shouldRequest {
|
|
||||||
print("Requesting audio recording permissions...")
|
|
||||||
permissionHandler.request()
|
|
||||||
|
|
||||||
// Wait for the permission request to complete
|
|
||||||
while permissionHandler.status == .unknown {
|
|
||||||
// Run the main run loop to allow DispatchQueue.main.async to execute
|
|
||||||
let result = CFRunLoopRunInMode(CFRunLoopMode.defaultMode, 0.1, true)
|
|
||||||
if result == CFRunLoopRunResult.stopped || result == CFRunLoopRunResult.finished {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get final status and exit with appropriate code
|
|
||||||
let status = permissionHandler.status
|
|
||||||
print("Audio recording permission status: \(status.rawValue)")
|
|
||||||
|
|
||||||
switch status {
|
|
||||||
case .authorized:
|
|
||||||
exit(0) // granted
|
|
||||||
case .unknown:
|
|
||||||
exit(1) // unknown
|
|
||||||
case .denied:
|
|
||||||
exit(2) // denied
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,17 +2,62 @@ import CoreAudio
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class AudioBuffer {
|
public class AudioBuffer {
|
||||||
private var buffer = Data()
|
private var buffer: [UInt8]
|
||||||
private let targetChunkDuration: Double
|
private var writeIndex: Int = 0
|
||||||
private let streamFormat: AudioStreamBasicDescription
|
private var readIndex: Int = 0
|
||||||
|
private var availableBytes: Int = 0
|
||||||
|
private let maxBufferSize: Int
|
||||||
|
|
||||||
|
private let bytesPerChunk: Int
|
||||||
|
private let chunkDuration: Double
|
||||||
|
|
||||||
public init(format: AudioStreamBasicDescription, chunkDuration: Double = 0.2) {
|
public init(format: AudioStreamBasicDescription, chunkDuration: Double = 0.2) {
|
||||||
self.streamFormat = format
|
|
||||||
self.targetChunkDuration = chunkDuration
|
// Pre-calculate chunk parameters
|
||||||
|
let bytesPerFrame = Int(format.mBytesPerFrame)
|
||||||
|
let samplesPerChunk = Int(format.mSampleRate * chunkDuration)
|
||||||
|
self.bytesPerChunk = samplesPerChunk * bytesPerFrame
|
||||||
|
self.chunkDuration = Double(samplesPerChunk) / format.mSampleRate
|
||||||
|
|
||||||
|
// Calculate max buffer size to hold ~10 seconds of audio, way more than the maximum we allow
|
||||||
|
let bytesPerSecond = Int(format.mSampleRate) * bytesPerFrame
|
||||||
|
self.maxBufferSize = bytesPerSecond * 10
|
||||||
|
|
||||||
|
// Pre-allocated ring buffer
|
||||||
|
self.buffer = Array(repeating: 0, count: maxBufferSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func append(_ data: Data) {
|
public func append(_ data: Data) {
|
||||||
buffer.append(data)
|
guard availableBytes + data.count <= maxBufferSize else {
|
||||||
|
Logger.error("Audio buffer overflow", context: [
|
||||||
|
"requested": String(data.count),
|
||||||
|
"available": String(maxBufferSize - availableBytes)
|
||||||
|
])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.withUnsafeBytes { bytes in
|
||||||
|
let sourceBytes = bytes.bindMemory(to: UInt8.self)
|
||||||
|
let dataSize = sourceBytes.count
|
||||||
|
|
||||||
|
// Check if we can copy in one block (no wrap-around)
|
||||||
|
if writeIndex + dataSize <= maxBufferSize {
|
||||||
|
// only one write needed
|
||||||
|
buffer.replaceSubrange(writeIndex..<writeIndex + dataSize, with: sourceBytes)
|
||||||
|
writeIndex = (writeIndex + dataSize) % maxBufferSize
|
||||||
|
} else {
|
||||||
|
// two writes needed due to wrap-around
|
||||||
|
let firstChunkSize = maxBufferSize - writeIndex
|
||||||
|
let secondChunkSize = dataSize - firstChunkSize
|
||||||
|
|
||||||
|
buffer.replaceSubrange(writeIndex..<maxBufferSize, with: sourceBytes.prefix(firstChunkSize))
|
||||||
|
buffer.replaceSubrange(0..<secondChunkSize, with: sourceBytes.suffix(secondChunkSize))
|
||||||
|
|
||||||
|
writeIndex = secondChunkSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableBytes += data.count
|
||||||
}
|
}
|
||||||
|
|
||||||
public func processChunks() -> [AudioPacket] {
|
public func processChunks() -> [AudioPacket] {
|
||||||
@@ -25,37 +70,37 @@ public class AudioBuffer {
|
|||||||
return packets
|
return packets
|
||||||
}
|
}
|
||||||
|
|
||||||
public func flushRemaining() -> AudioPacket? {
|
|
||||||
guard !buffer.isEmpty else { return nil }
|
|
||||||
|
|
||||||
let packet = AudioPacket(
|
|
||||||
timestamp: Date(),
|
|
||||||
duration: 0.0, // Unknown duration for final chunk
|
|
||||||
peakAmplitude: 0.0,
|
|
||||||
rawAudioData: buffer
|
|
||||||
)
|
|
||||||
|
|
||||||
buffer.removeAll()
|
|
||||||
return packet
|
|
||||||
}
|
|
||||||
|
|
||||||
private func nextChunk() -> AudioPacket? {
|
private func nextChunk() -> AudioPacket? {
|
||||||
let bytesPerFrame = Int(streamFormat.mBytesPerFrame)
|
// Check if we have enough data for a complete chunk
|
||||||
let samplesPerChunk = Int(streamFormat.mSampleRate * targetChunkDuration)
|
guard availableBytes >= bytesPerChunk else { return nil }
|
||||||
let bytesPerChunk = samplesPerChunk * bytesPerFrame
|
|
||||||
|
|
||||||
guard buffer.count >= bytesPerChunk else { return nil }
|
var chunkData = Data(capacity: bytesPerChunk)
|
||||||
|
|
||||||
let chunkData = buffer.prefix(bytesPerChunk)
|
// Check if we can copy in one block (no wrap-around)
|
||||||
|
if readIndex + bytesPerChunk <= maxBufferSize {
|
||||||
|
// one copy needed
|
||||||
|
chunkData.append(contentsOf: buffer[readIndex..<readIndex + bytesPerChunk])
|
||||||
|
readIndex = (readIndex + bytesPerChunk) % maxBufferSize
|
||||||
|
} else {
|
||||||
|
// two copies needed due to wrap-around
|
||||||
|
let firstChunkSize = maxBufferSize - readIndex
|
||||||
|
let secondChunkSize = bytesPerChunk - firstChunkSize
|
||||||
|
|
||||||
|
chunkData.append(contentsOf: buffer[readIndex..<maxBufferSize])
|
||||||
|
chunkData.append(contentsOf: buffer[0..<secondChunkSize])
|
||||||
|
|
||||||
|
readIndex = secondChunkSize
|
||||||
|
}
|
||||||
|
|
||||||
|
availableBytes -= bytesPerChunk
|
||||||
|
|
||||||
let packet = AudioPacket(
|
let packet = AudioPacket(
|
||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
duration: Double(samplesPerChunk) / streamFormat.mSampleRate,
|
duration: chunkDuration,
|
||||||
peakAmplitude: 0.0, // No analysis in raw mode
|
peakAmplitude: 0.0,
|
||||||
rawAudioData: Data(chunkData)
|
rawAudioData: chunkData
|
||||||
)
|
)
|
||||||
|
|
||||||
buffer.removeFirst(bytesPerChunk)
|
|
||||||
return packet
|
return packet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ public class AudioRecorder {
|
|||||||
Logger.info("Audio device started successfully")
|
Logger.info("Audio device started successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: note to self, what about installTap? Would require audio engine and a node?
|
// Note to self, what about installTap? Would require audio engine and a node?
|
||||||
|
// No; AudioEngine.installTap() can only fire as often as 100ms. too slow for us
|
||||||
private func setupAndStartIOProc() {
|
private func setupAndStartIOProc() {
|
||||||
Logger.debug("Creating IO proc")
|
Logger.debug("Creating IO proc")
|
||||||
var status = AudioDeviceCreateIOProcID(
|
var status = AudioDeviceCreateIOProcID(
|
||||||
@@ -126,8 +127,8 @@ public class AudioRecorder {
|
|||||||
|
|
||||||
func stopRecording() {
|
func stopRecording() {
|
||||||
// Send any remaining buffered audio, applying conversion if needed
|
// Send any remaining buffered audio, applying conversion if needed
|
||||||
if let finalPacket = audioBuffer?.flushRemaining() {
|
audioBuffer?.processChunks().forEach { packet in
|
||||||
let processedPacket = converter?.transform(finalPacket) ?? finalPacket
|
let processedPacket = converter?.transform(packet) ?? packet
|
||||||
outputHandler.handleAudioPacket(processedPacket)
|
outputHandler.handleAudioPacket(processedPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
import OSLog
|
|
||||||
import Observation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// Adapted with a huge debt of gratitude from https://github.com/insidegui/AudioCap/blob/main/AudioCap/ProcessTap/AudioRecordingPermission.swift
|
|
||||||
|
|
||||||
/// Uses TCC SPI in order to check/request system audio recording permission.
|
|
||||||
@Observable
|
|
||||||
final class AudioRecordingPermission {
|
|
||||||
// private let logger = Logger(subsystem: kAppSubsystem, category: String(describing: AudioRecordingPermission.self))
|
|
||||||
|
|
||||||
enum Status: String {
|
|
||||||
case unknown
|
|
||||||
case denied
|
|
||||||
case authorized
|
|
||||||
}
|
|
||||||
|
|
||||||
private(set) var status: Status = .unknown
|
|
||||||
|
|
||||||
init() {
|
|
||||||
#if ENABLE_TCC_SPI
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main
|
|
||||||
) { [weak self] _ in
|
|
||||||
guard let self else { return }
|
|
||||||
self.updateStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus()
|
|
||||||
#else
|
|
||||||
status = .authorized
|
|
||||||
#endif // ENABLE_TCC_SPI
|
|
||||||
}
|
|
||||||
|
|
||||||
func request() {
|
|
||||||
#if ENABLE_TCC_SPI
|
|
||||||
// logger.debug(#function)
|
|
||||||
print("DEBUG: TCC SPI request called")
|
|
||||||
|
|
||||||
guard let request = Self.requestSPI else {
|
|
||||||
// logger.fault("Request SPI missing")
|
|
||||||
print("DEBUG: Request SPI is nil - TCC framework loading failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("DEBUG: Calling TCC request function...")
|
|
||||||
request("kTCCServiceAudioCapture" as CFString, nil) { [weak self] granted in
|
|
||||||
guard let self else { return }
|
|
||||||
|
|
||||||
// self.logger.info("Request finished with result: \(granted, privacy: .public)")
|
|
||||||
print("DEBUG: TCC request completed with result: \(granted)")
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
print("DEBUG: Updating status on main queue...")
|
|
||||||
if granted {
|
|
||||||
self.status = .authorized
|
|
||||||
print("DEBUG: Status set to authorized")
|
|
||||||
} else {
|
|
||||||
self.status = .denied
|
|
||||||
print("DEBUG: Status set to denied")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
print("DEBUG: ENABLE_TCC_SPI not defined")
|
|
||||||
#endif // ENABLE_TCC_SPI
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateStatus() {
|
|
||||||
#if ENABLE_TCC_SPI
|
|
||||||
// logger.debug(#function)
|
|
||||||
|
|
||||||
guard let preflight = Self.preflightSPI else {
|
|
||||||
// logger.fault("Preflight SPI missing")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = preflight("kTCCServiceAudioCapture" as CFString, nil)
|
|
||||||
|
|
||||||
if result == 1 {
|
|
||||||
status = .denied
|
|
||||||
} else if result == 0 {
|
|
||||||
status = .authorized
|
|
||||||
} else {
|
|
||||||
status = .unknown
|
|
||||||
}
|
|
||||||
#endif // ENABLE_TCC_SPI
|
|
||||||
}
|
|
||||||
|
|
||||||
#if ENABLE_TCC_SPI
|
|
||||||
private typealias PreflightFuncType = @convention(c) (CFString, CFDictionary?) -> Int
|
|
||||||
private typealias RequestFuncType = @convention(c) (
|
|
||||||
CFString, CFDictionary?, @escaping (Bool) -> Void
|
|
||||||
) -> Void
|
|
||||||
|
|
||||||
/// `dlopen` handle to the TCC framework.
|
|
||||||
private static let apiHandle: UnsafeMutableRawPointer? = {
|
|
||||||
let tccPath = "/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC"
|
|
||||||
print("DEBUG: Attempting to load TCC framework from: \(tccPath)")
|
|
||||||
|
|
||||||
guard let handle = dlopen(tccPath, RTLD_NOW) else {
|
|
||||||
print("DEBUG: dlopen failed for TCC framework")
|
|
||||||
assertionFailure("dlopen failed")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
print("DEBUG: TCC framework loaded successfully")
|
|
||||||
return handle
|
|
||||||
}()
|
|
||||||
|
|
||||||
/// `dlsym` function handle for `TCCAccessPreflight`.
|
|
||||||
private static let preflightSPI: PreflightFuncType? = {
|
|
||||||
guard let apiHandle else { return nil }
|
|
||||||
|
|
||||||
let fnName = "TCCAccessPreflight"
|
|
||||||
|
|
||||||
guard let funcSym = dlsym(apiHandle, fnName) else {
|
|
||||||
assertionFailure("Couldn't find symbol")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let fn = unsafeBitCast(funcSym, to: PreflightFuncType.self)
|
|
||||||
|
|
||||||
return fn
|
|
||||||
}()
|
|
||||||
|
|
||||||
/// `dlsym` function handle for `TCCAccessRequest`.
|
|
||||||
private static let requestSPI: RequestFuncType? = {
|
|
||||||
guard let apiHandle else {
|
|
||||||
print("DEBUG: No API handle for TCCAccessRequest")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let fnName = "TCCAccessRequest"
|
|
||||||
print("DEBUG: Looking for symbol: \(fnName)")
|
|
||||||
|
|
||||||
guard let funcSym = dlsym(apiHandle, fnName) else {
|
|
||||||
print("DEBUG: Couldn't find symbol: \(fnName)")
|
|
||||||
assertionFailure("Couldn't find symbol")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
print("DEBUG: Found TCCAccessRequest symbol successfully")
|
|
||||||
let fn = unsafeBitCast(funcSym, to: RequestFuncType.self)
|
|
||||||
|
|
||||||
return fn
|
|
||||||
}()
|
|
||||||
#endif // ENABLE_TCC_SPI
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user