NSUserUnixTask ignores script quietly - swift

I'm writing a sandboxed app in Swift 4 on macOS High Sierra using Xcode 9. My app has a shell script file (say, myscript) copied to the Bundle.main.resourceURL directory when installing. I want to run this shell script using NSUserUnixTask as follows:
if let scriptUrl = URL(string: "myscript", relativeTo: Bundle.main.resourceURL) {
do {
let task = try NSUserUnixTask(url: scriptUrl)
task.execute(withArguments: [ "hello" ])
debugPrint("OK")
} catch let error {
debugPrint(error)
}
} else {
debugPrint("Script not found")
}
// output: "OK"
This gives no error messages and "OK" is correctly displayed. But the myscript script seems to be completely ignored. When I run myscript from Terminal instead, it runs as intended.
The test myscript file looks like:
#!/bin/sh
/usr/bin/osascript -e "tell app \"System Events\" to display dialog \"Message: $1\""
(The is only a test script; the real one calls emacsclient.) The shell script file is set executable (permission 755). Not only this shell script but also any other shell script seems ignored. For example, if the second line is replaced with /usr/bin/printf "\a", it does not beep. I tried many other shell scripts but nothing seems to work.
How can I solve this issue?
EDIT:
I was hasty. The class document says "If the application is sandboxed, then the script must be in the applicationScriptsDirectory folder." Now I want to know how to copy the script file upon installation.
EDIT 2:
I manually copied myscript to ~/Library/Applications Scripts/com.mydomain.myApp/ and then changed the first line of the above Swift codes (if let scriptUrl ...) to
let scriptFolderUrl = try! FileManager.default.url(for: .applicationScriptsDirectory,
in: .userDomainMask, appropriateFor: nil, create: true)
if let scriptUrl = URL(string: "myscript", relativeTo: scriptFolderURL) {
https://tutel.me/c/programming/questions/43741325/swift+3++sandbox++how+to+create+applicationscriptsdirectory+if+it+doesn39t+exist
The script runs. Now I need to figure out how to copy the script to the scripts folder when installing or running the app the first time.

pkamp requested a proper answer. :)
As the class document says, the script must be in the applicationScriptsDirectory folder. This folder is not writable by a sandboxed application, as vadian pointed out. The script (myscript) must be copied manually to the applicationScriptDirectory folder (e.g., ~/Library/Applications Scripts/com.mydomain.myApp/).
After this done, change the first line to the following:
// --------------------------------------------------------
let scriptFolderUrl = try! FileManager.default.url(for: .applicationScriptsDirectory,
in: .userDomainMask, appropriateFor: nil, create: true)
if let scriptUrl = URL(string: "myscript", relativeTo: scriptFolderURL) {
// --------------------------------------------------------
// same below
do {
let task = try NSUserUnixTask(url: scriptUrl)
task.execute(withArguments: [ "hello" ])
debugPrint("OK")
} catch let error {
debugPrint(error)
}
} else {
debugPrint("Script not found")
}
I benefited from this link for the first line.

Related

How to access "Standard Output" when running macOS executable?

Problem:
I'm trying to hook up the standard input/standard output from a unix executable file to the user interface in a MacOS application. But, I can't seem to access the values - nothing shows up.
Background:
I've implemented every solution that I could find, but none of them have worked [1][2][3][4][5][6][7][8][9]. I've completed a Python 3 course [1], so that I could customize the standard output in the python script of the executable file [1]. And, I've reviewed and implemented several working MacOS repositories that update their user interface with data from standard output [1][2][3].
Code: Full
func runExecutable() {
let desktop = fileManager.urls(for: .desktopDirectory, in: .userDomainMask)[0]
let url = Bundle.main.url(forResource: "youtube_dl_custom", withExtension: "")
let arguments = [
"--format",
"bestvideo[ext=mp4]+bestaudio[ext=m4a]",
"--output",
"\(desktop)/%(title)s.%(ext)s",
"\(videoUrlTextField.stringValue)"
]
process.arguments = arguments
process.executableURL = url
process.standardInput = inputPipe
process.standardOutput = outputPipe
openConsolePipe()
inputPipe.fileHandleForReading.readabilityHandler = {
[weak self] fileHandle in
let data = fileHandle.availableData
self?.buffer.append(data)
if let buffer = self?.buffer,
let string = String(data: buffer, encoding: .utf8),
string.last?.isNewline == true {
self?.buffer.removeAll()
print("## \(string)")
self?.standardOutputTextView.string += string + "\n"
self?.outputPipe.fileHandleForWriting.write(data)
}
}
try? process.run()
closeConsolePipe()
}
func openConsolePipe() {
dup2(STDOUT_FILENO, outputPipe.fileHandleForWriting.fileDescriptor)
dup2(inputPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
dup2(inputPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)
}
func closeConsolePipe() {
freopen("/dev/stdout", "a", stdout)
}
Results:
The standard output appears to automatically print to the console, but I can't seem to access the values.
Misc:
Used the youtube-dlrepository to download videos [1].
Used a custom python script for youtube-dl [1].
Converted youtube-dl to an executable file using pyinstaller [1].
Posted the project to GitHub for troubleshooting [1].

How do I write to a local file in Swift/XCTest?

My ultimate question is about saving a screenshot from an AppleTV application using XCTest and Swift4 (running on a MacBook paired to the TV device), but I'm having trouble even writing a simple text string to a local file. If I can get this simple file-save working, I'm hoping I can resolve the screenshot issue. (Apologies for making this look like two questions but they appear to be related and resulted from my troubleshooting efforts.)
First, here's what I'm trying to do with a screenshot, based on sample code I found somewhere online:
let appshot = XCUIApplication().windows.firstMatch.screenshot()
let shotpath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0].appendingPathComponent("appshot.png")
let shotpathUrl = URL(string: "file://\(shotpath)")
print("Saving to: \(shotpath)")
do {
try appshot.pngRepresentation.write(to: shotpathUrl!)
} catch {
print("Failed saving screenshot due to \(error)")
}
This gives me the following output:
Saving to: file:///var/mobile/Containers/Data/Application/77D52C66-353B-4029-97D5-48E6BAE35C92/Downloads/appshot.png
Failed saving screenshot due to Error Domain=NSCocoaErrorDomain Code=4 "The file “appshot.png” doesn’t exist." UserInfo={NSFilePath=///var/mobile/Containers/Data/Application/77D52C66-353B-4029-97D5-48E6BAE35C92/Downloads/appshot.png, NSUnderlyingError=0x1c405bc60 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
Of course, the file doesn't exist because that's the file I'm trying to create. But /var/mobile doesn't exist on my laptop either -- it looks like the path FileManager is building may exist on the AppleTV device, but I want it on my laptop where my test script is executing.
So I backed out to a much more simple case, and even this is giving me problems:
let str = "This is a test"
let path = "file:///Users/haljor/foo.txt"
let pathUrl = URL(string: path)!
print("Path: \(path)")
print("URL: \(pathUrl)")
do {
try str.write(to: pathUrl, atomically: true, encoding: .utf8)
} catch {
print("Caught error writing to \(pathUrl): \(error)")
}
And here's the output:
Path: file:///Users/haljor/foo.txt
URL: file:///Users/haljor/foo.txt
Caught error writing to file:///Users/haljor/foo.txt: Error Domain=NSCocoaErrorDomain Code=4 "The folder “foo.txt” doesn’t exist." UserInfo={NSURL=file:///Users/haljor/foo.txt, NSUserStringVariant=Folder, NSUnderlyingError=0x1c40553f0 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
Here, it looks like it's trying to write to a folder at the path I specified, not a file. Clearly there's something I'm not understanding in each of these cases.
I don't really have a preference for whether I use a fully-specified path or something using FileManager -- it just needs to land somewhere on my laptop (not the TV device). What am I missing?
You can add an attachment to the test case and save it to disk too. The problem was that Downloads folder may not exist in the container yet. The best way to handle this is via init-once property:
var downloadsFolder: URL = {
let fm = FileManager.default
let folder = fm.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
var isDirectory: ObjCBool = false
if !(fm.fileExists(atPath: folder.path, isDirectory: &isDirectory) && isDirectory.boolValue) {
try! fm.createDirectory(at: folder, withIntermediateDirectories: false, attributes: nil)
}
return folder
}()
func test() {
let appshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: appshot)
attachment.lifetime = .keepAlways
self.add(attachment)
// Save to container
let url = downloadsFolder.appendingPathComponent("appshot.png")
try! appshot.pngRepresentation.write(to: url)
}
If you want to view the attachment, right-click on the test case, select Jump to Report and expand the tree. You will see the screenshot eventually:

xQuartz display not for shell script launched from Swift Process

I have a simple shell script which launches an X11 app. When I execute this shell script form my login shell / terminal xQuartz starts and I get a display. However the process doesn't get a display for xQuartz when running the script from within swift. Any idea how I can get the display?
Also what is the best way to detect if xQuartz is installed? Checking if xterm exists?
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/sh")
let startScriptURL = Bundle.main.url(forResource: "run", withExtension: "sh")
guard let startScriptPath = startScriptURL?.path else {
return
}
process.arguments = [startScriptPath]
do {
try process.run()
} catch let error {
print(error)
}
run.sh:
#!/bin/sh
/opt/X11/bin/xeyes
I figured our how to pass the DISPLAY environment or any environmental variable to Process.
The current environment can be obtained by:
ProcessInfo().environment
So I use now this:
let process = Process()
guard let envDisplay = ProcessInfo().environment["DISPLAY"] else {
print("Please install xQuartz")
return
}
process.environment = ["DISPLAY": envDisplay]
I got the idea from here: Issue launching X11 app via NSTask

How to have access to files via shell commands in mac os x application?

Im new to developing mac applications. (but I have work a lot for ios)
For start I want to have a window on which there is a button. When user click on the button, it should trigger an action that will run some shell commands inside the folder that the user dragged & dropped inside the app before.
For example
User will drag and drop some folder inside my app and the app should create gemfile inside this folder.
Here is my current version where I run the shell and the first command is cd <pathToFolder> but the output is /usr/bin/cd: line 4: cd: file:///Users/klemen/Downloads/ABCAcura/: No such file or directory
Here is my code:
#IBAction func handleCreateGemfileButton(_ sender: NSButton) {
let pipe = Pipe()
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["cd", projectURL.absoluteString]
process.standardOutput = pipe
let file = pipe.fileHandleForReading
process.launch()
if let result = NSString(data: file.readDataToEndOfFile(), encoding: String.Encoding.utf8.rawValue) {
print(result as String)
}
else {
print("error")
}
}
Is there some restrictions. How is it possible to request from user that my app will have access to some of his files?
You should use ~/Downloads/ABCAcura instead of file:///..., file:/// is a protocol (URL) not a path to file.

Using Xcode 7/Swift 2 writeToPath to Resources file the call does not fail but no data is written

I am using Xcode 7.3.1 and Swift 2.0. I am using the following code sample:
func writeToResourcesDataDir() {
if let path = NSBundle.mainBundle().pathForResource("TestData", ofType: ".json") {
let str = "Test String"
do {
try str.writeToFile(path, atomically: false, encoding: NSUTF8StringEncoding)
print("writeToFile successful")
} catch {
print("writeToFile failed")
}
} else {
print("Path does not exist")
}
}
Running under Xcode in the see the "writeToFile successful" message.But, also using the simulator, I can display the TestData in the Resources directory and the file does not have the string.I also used a terminal window in Mac to look at the files in the Resources directory and the TestData file is empty (0 bytes).I know I am in the correct Resources directory because there is another file in the directory that has correct data that is used for running the other parts of the program.
I have spent several days now looking at other google entries about data from writeToFile not working and I have tried out every fix or things to try I have found.
Can anyone help?
I added code to accept the boolean return from the call to writeToFile and it returns a false. I'm not sure why a false is returned but the catch isn't invoked.I am not sure how to get the error code that goes with this writeToFile in Swift 2.0.
I am also wondering if this is a write permissions problem.Should I be using the Documents directory instead of the Data directory?
Try something like this. This is swift 2.3 and xcode 8.
let filename = "yourjsonfile"
let documentDirectoryURL = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
let filePath = documentDirectoryURL.URLByAppendingPathComponent(filename)
let fileExist = filePath?.checkResourceIsReachableAndReturnError(nil)
if (fileExist == true) {
print("Found file")
} else {
print("File not found")
}