Swift macOS Process.run() port leak - swift

Current code:
#!/usr/bin/swift
import Foundation
func runSleepProcess() {
let sleepProcess = Process()
sleepProcess.executableURL = URL(fileURLWithPath: "/bin/sleep")
sleepProcess.arguments = ["0"]
try? sleepProcess.run()
sleepProcess.waitUntilExit()
}
while true {
runSleepProcess()
}
Looking in activity monitor, it seems that the mach port usage increases by 1 each loop. Is this expected behavior when running an external process? If not, how do I fix the leak? Thanks.

It's not expected behaviour and this problem is already reported. The workaround for this is using posix_spawn instead of Process.

Related

AVAudioEngine gives seemingly random errors in different environments

I am trying to write a MIDIPlayer class which is a wrapper for an AVAudioEngine and an AVAudioUnitMIDIInstrument. I have written a loop that gets the names and ASBDs for all AudioComponents of type MusicDevice and then chooses the most desired unit according to a list which works quite similar to font substitution, with Apples DLS MusicDevice as the ultimate fallback. Here's my sample code:
import AVKit
fileprivate func setupAVAudioEngine(engine: inout AVAudioEngine, instrumentAU: inout AVAudioUnitMIDIInstrument?) {
var instrumentACD = AudioComponentDescription(componentType: kAudioUnitType_MusicDevice, componentSubType: 0, componentManufacturer: 0, componentFlags: 0, componentFlagsMask: 0)
var instrumentComponents: [(AudioComponentDescription, String)] = []
var instrumentComponent: AudioComponent? = nil
repeat {
instrumentComponent = AudioComponentFindNext(instrumentComponent, &instrumentACD)
if instrumentComponent == nil {
break
}
var compDescr = AudioComponentDescription()
var name: Unmanaged<CFString>?
AudioComponentCopyName(instrumentComponent!, &name)
AudioComponentGetDescription(instrumentComponent!, &compDescr)
let nameString = name!.takeRetainedValue() as String
instrumentComponents.append((compDescr, nameString))
name?.release()
} while true
let instrumentComponentSubstitutionList = ["MakeMusic: SmartMusicSoftSynth","Apple: AUMIDISynth","Apple: DLSMusicDevice"]
var found = false
for instrument in instrumentComponentSubstitutionList {
for member in instrumentComponents {
if member.1 == instrument {
instrumentACD = member.0
print("\(member.1) found")
found = true
break
}
if found {break}
}
}
print("Try to create InstrumentNode with ACD: \(instrumentACD)")
instrumentAU = AVAudioUnitMIDIInstrument(audioComponentDescription: instrumentACD)
print("InstrumentNode created: \(instrumentAU!.name)")
print()
engine.attach(instrumentAU!)
engine.connect(instrumentAU!, to: engine.mainMixerNode, format: nil)
}
open class MIDIPlayer {
private var audioEngine: AVAudioEngine
private var instrumentUnit: AVAudioUnitMIDIInstrument
private var mainMixer: AVAudioMixerNode
public init() {
self.audioEngine = AVAudioEngine()
self.mainMixer = audioEngine.mainMixerNode
var instrumentAU: AVAudioUnitMIDIInstrument?
setupAVAudioEngine(engine: &audioEngine, instrumentAU: &instrumentAU)
self.instrumentUnit = instrumentAU!
try! audioEngine.start()
}
public func playMIDINote(_ note: UInt8) {
print("Playing MIDI Note \(note)")
instrumentUnit.startNote(note, withVelocity: 70, onChannel: 0)
sleep(1)
instrumentUnit.stopNote(note, onChannel: 0)
}
}
let midiPlayer = MIDIPlayer()
midiPlayer.playMIDINote(60)
midiPlayer.playMIDINote(62)
midiPlayer.playMIDINote(64)
midiPlayer.playMIDINote(65)
midiPlayer.playMIDINote(67)
The code works perfectly fine in an Xcode 11.3.1 playground, howewer, when I use the exact same code outside the playground I get different kinds of errors:
When I copy the same code into a command line project and run it from XCode it still works but the console gives me the following error:
[AudioHAL_Client] AudioHardware.cpp:666:AudioObjectGetPropertyData: AudioObjectGetPropertyData: no object with given ID 0
When I run the executable without Xcode no error is reported.
When I create a single View app, put the class and its setup func in its own source file and create an instance in the applicationDidFinishLaunching method of the AppDelegate I get the following errors:
[AudioHAL_Client] HALC_ShellDriverPlugIn.cpp:104:Open: HALC_ShellDriverPlugIn::Open: opening the plug-in failed, Error: 2003329396 (what)
[AudioHAL_Client] AudioHardware.cpp:666:AudioObjectGetPropertyData: AudioObjectGetPropertyData: no object with given ID 0
These errors are written to the console even before my setup function is called. However, the code still works (I hear notes playing), but only if I change the instrumentComponentSubstitutionList so that one of the Apple AUs will be found (in other words: not the SmartMusicSoftSynth). When I keep the SoftSynth the preferred device the code crashes and I get the additional error:
[avae] AVAEInternal.h:103:_AVAE_CheckNoErr: [AUInterface.mm:461:AUInterfaceBaseV3: (AudioComponentInstanceNew(comp, &_auv2)): error -3000
Note: In the playground and the command line app the SoftSynth works.
Some observations which may or may not relate to the issue:
In this blogpost http://www.rockhoppertech.com/blog/multi-timbral-avaudiounitmidiinstrument/ Gene DeLisa mentions that the AVAudioUnitSampler is the only subclass of the abstract class AVAudioUnitMIDIInstrument. This post is from 2016, but I do not find any further information about that. But, obviously, the DLS MusicDevice as well as the 3rd party SoftSynth work – at least in some environments.
The ASBD taken from the found AudioComponents for both of the Apple Units have their componentFlags property set to 2. The documentation says: must be set to zero unless a known specific value is requested. The componentFlags property of the SoftSynth is 0.
I still have a somehow related problem when I try to capture audio from input How do I allow Xcode to access the microphone?? - related in so far that the command line app shows different behavior when run from XCode or via the terminal, and in both issues CoreAudio is involved.
My Question(s):
Why do I get these errors?
Why does the 3rd party plugIn work in the playground and the command line app but not in a single view app?
Is AVAudioEngine ready to host other Instrument Units than the monotimbral SamplerUnit?
Or do I have to step down and use the instrumentAUs directly and not the AVAudioUnitMIDIInstrument wrapper?

NSSavePanel name not user editable

I'm a total beginner to OSX GUI programming, so please be gentle with me.
I'm trying some experiments with adding light GUI elements from appkit to a CLI, so I'm working on a very small program to take the contents of a PDF and save it to a text file.
Here's the code I have
import AppKit
import Foundation
import Quartz
func helperReadPDF(_ filename: String) -> String {
let pdata = try! NSData(contentsOfFile: filename) as Data
let pdf = PDFDocument(data: pdata)
return pdf!.string!
}
func selectFile() -> URL? {
let dialog = NSOpenPanel()
dialog.allowedFileTypes = ["pdf"]
guard dialog.runModal() == .OK else { return nil }
return dialog.url
}
func getSaveLocation() -> URL? {
let sa = NSSavePanel()
sa.nameFieldStringValue = "Untitled.txt"
sa.canCreateDirectories = true
sa.allowedFileTypes = ["txt"]
guard sa.runModal() == .OK else { return nil }
return sa.url
}
let file = selectFile()?.path ?? ""
print("where to save?")
let dest = getSaveLocation()!
try! helperReadPDF(file).write(to: dest, atomically: true, encoding: .utf8)
(I know, there are lots of unidiomatic things in here, like all the forced unwrapping and pointlessly converting URLs to paths. I have obscure reasons...)
So this code mostly works: when I run it from a terminal window with swift guitest.swift it'll pop up a file picker window, let me select a pdf file, and then pop up a save dialogue and let me choose the directory, and then save the extracted text from the pdf into that directory.
But it won't let me change the filename. I can highlight the "Untitled.txt" provided by default, I can even get a cursor into the field... but it doesn't respond to keyboard input.
In this previous SO, someone suggested adding a nameFieldStringValue to make it editable, but, as you can see from the above code, I did that, and it doesn't work.
I see from this very old SO that at least in Objective-C-land, you have to initiate some kind of application object in order to accept keyboard input. Is that true today in Swift-land as well?
(Even though for some weird reason you can accept mouse input without doing any of that?!) If so, how do I do that here?
Edit: I get from the comments to that last prior SO I linked that this is probably a terrible idea, and that if I want to learn Mac GUI programming I should do it the heavy way with XCode and storyboards and all the rest. But could you indulge my doing it the stupid way in an effort to try to learn one thing at a time? (I.e., learn the GUI APIs on offer without also trying to learn XCode and Apple's preferred style of architecture at the same time.)
Thanks!
(Swift 4.2 on latest version of OSX. Not using XCode at all.)
Setting the application's ActivationPolicy will make it work.
// Import statements... (import Quartz)
NSApplication.shared.setActivationPolicy(.accessory)
// Functions and so on... (func helper..)

Wait for WKWebView evaluateJavaScript to finish

I have a problem that's been irritating me for a couple of days now. I'm trying to work with a WKWebView in my Swift project. Below is my code:
var items: [String] = []
for i in 0...8 {
web_view.evaluateJavaScript("document.getElementsByClassName('channels-content-item yt-shelf-grid-item')[\(i)].innerHTML") { (result, error) in
//print(result ?? "nil")
//print(error ?? "nil")
let string_result: String = String(describing: result!)
items.append(string_result)
num += 1
//print(string_result)
}
}
print(items)
My problem is the fact that the program won't wait for the evaluateJavaScript functions to finish before going to the print(items), even though the items array gets populated via these functions. Any way I can get this to work?
Any help would be greatly appreciated, thanks in advance
What is inside of your completion handler I think runs on a different thread, meaning it is running AFTER your print happens. Since injecting javascript takes longer, your print is most likely always going to happen first.

How Can I Run JXA from Swift?

If I have a string variable with JXA source code, is there a way to run that from swift? It seems NSAppleScript only works with AppleScript source.
Here is an example showing how to use OSAKit from Swift to run a JavaScript for Automation script stored in a string:
import OSAKit
let scriptString = "Math.PI";
let script = OSAScript.init(source: scriptString, language: OSALanguage.init(forName: "JavaScript"));
var compileError : NSDictionary?
script.compileAndReturnError(&compileError)
if let compileError = compileError {
print(compileError);
return;
}
var scriptError : NSDictionary?
let result = script.executeAndReturnError(&scriptError)
if let scriptError = scriptError {
print(scriptError);
}
else if let result = result?.stringValue {
print(result)
}
This Swift code was adapted from the Hammerspoon source code (Objective-C).
Why? JXA is a dog in every respect. If you just want to run JS code, the JavaScriptCore Obj-C API is far cleaner and easier. If you want to control "AppleScriptable" applications then use AppleScript—it's the only officially supported† option that works right.
(† There is SwiftAutomation, but Apple haven't bitten and I'm not inclined to support it myself given Apple's chronic mismanagement of Mac Automation. We'll see what happens with WWDC17.)

How to determine whether a Swift code is running inside XCode Playground

I'm writing a simple application reading CSV file in Swift and I would like to be able to use the same code in Playground and as an input file to the swift command.
To read a file in Playground I have to use this code
let filePath = XCPlaygroundSharedDataDirectoryURL.URLByAppendingPathComponent("data.csv")
I would like to achieve something like:
#if PLAYGROUND
import XCPlayground
let filePath = XCPlaygroundSharedDataDirectoryURL.URLByAppendingPathComponent("data.csv")
#else
let filePath = NSURL.fileURLWithPath("data.csv")
#endif
The test is quite simple:
let bundleId = NSBundle.mainBundle().bundleIdentifier ?? ""
if bundleId.hasPrefix("com.apple.dt"){
//... Your code
}
But I think you have already seen the problem once you've done that... the import will stop the build elsewhere. I suspect you are trying to build a playground for a framework you have built (if not, I'm not quite sure how the code is being shared)... The way I solved it in the framework was to provide an optional call back hook for the value I wanted to get... so for example
In Framework
public defaultUrlHook : (()->NSURL)? = nil
internal var defaultUrl : NSURL {
return defaultUrlHook?() ?? NSURL.fileURLWithPath("data.csv")
}
In playground
import XCPlayground
import YourFramework
defaultUrlHook = { ()->NSURL in
return XCPlaygroundSharedDataDirectoryURL.URLByAppendingPathComponent("data.csv")
}
//Do your thing....