Swift on MacOS: cannot open a file from my app - swift

I'm writing an app that displays a list of PDF files in a NSTableView that the user should be able to double-click to open in the default application (Preview, Adobe Reader, ...).
I've tried using NSWorkspace.shared.openFile and NSWorkspace.shared.open(_: withAppBundleIdentifier: options: additionalEventParamDescriptor: launchIdentifiers:), but none of them work.
I can't use the new func open(URL, configuration: NSWorkspace.OpenConfiguration, completionHandler: ((NSRunningApplication?, Error?) -> Void)?) as this is targeted at pre-Catalina computers.
Here are the two code snippets:
1.
#objc func doubleClickOnResultRow() {
let clickedRow = resultsTableView.selectedRow
if ( clickedRow > -1 ) {
let myURL = foundItems[clickedRow] as URL
if (!NSWorkspace.shared.openFile(myURL.path)) {
print("Unable to open : ", myURL.path)
}
}
}
This first one does nothing, and I get a "Unable to open : url/to/my/file.pdf" in the console.
2.
#objc func doubleClickOnResultRow() {
let clickedRow = resultsTableView.selectedRow
if ( clickedRow > -1 ) {
let myURL = foundItems[clickedRow] as URL
NSWorkspace.shared.open([myURL], withAppBundleIdentifier: "com.apple.Preview", options: NSWorkspace.LaunchOptions.withErrorPresentation, additionalEventParamDescriptor: nil, launchIdentifiers: nil)
}
}
With this one, however, when I double-click on a file, I get a error window with the Finder icon that says :
"The application “My app” does not have permission to open “myfile.pdf.” Here is the screenshot:
Finder error
I don't understand what I'm doing wrong. Eventually I could build a lightweight PDF viewer inside my app, but I would really like to avoid it if possible.
EDIT 03/31/2020, 16:10
I've tried a third way, by calling the shell command "open" with this (found here):
func shell(_ command: String) -> String {
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
}
and then
let command = "open \""+myURL.path+"\""
print(shell(command))
and I get
LSOpenURLsWithRole() failed with error -54 for the file path/to/my/file.pdf.

You have to construct the URL to file using init(fileURLWithPath:) of URL not the usual init?(string:)
let url = URL(fileURLWithPath: "/path/to/file.pdf")
Then you can open it with default application using NSWorkspace.open instance method
NSWorkspace.shared.open(url)

Related

How to launch terminal and pass it a command using Swift

I want to programmatically open a terminal and paste a command into it like "cd /Users/...".
I can start a terminal using this code, but I don't know how to execute command
guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal") else { return }
let path = "/bin"
let configuration = NSWorkspace.OpenConfiguration()
configuration.arguments = \[path\]
NSWorkspace.shared.openApplication(at: url, configuration: configuration, completionHandler: nil)
It is very important to use the sandbox, the Process command is not suitable.
If all you are trying to achieve is to open a terminal on a specific location all you have to do is to use this code:
let pathToOpen = "/Users/admin/Desktop"
let url = URL(string:"terminal://"+pathToOpen)!
NSWorkspace.shared.open(url)
If you are trying just to run a command in terminal and show the output in your app, here is another useful code snippet:
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
task.standardInput = nil
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
//Usage:
shell("yourCommandHere")
//please note that you are not allowed to use commands that require sudo

Mac Swift Cocoa - hdiutil: attach failed - Device not configured

I am creating a MacOS app that automatically installs .dmg's in user Applications folder (copies .app in a dmg to /Applications). But I need to somehow attach the selected .dmg image. I have tried doing this:
func dragViewDidReceive(fileURLs: [URL]) {
for url in fileURLs {
print(url.path)
print(shell("hdiutil attach \(url.path)"))
}
}
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
But if I select the file (aka call dragViewDidReceive function) the dmg will not attach and the output for bash command will be hdiutil: attach failed - Device not configured
I also tried using ShellOut:
func dragViewDidReceive(fileURLs: [URL]) {
for url in fileURLs {
print(url.path)
try! shellOut(to: "hdiutil attach \(url.path)")
}
}
Which gives the same error. hdiutil: attach failed - Device not configured
Is there any other way how I can attach a .dmg? Or is there any fix for this error using one of these methods?
Thanks

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].

Swift-Problem discovered when creating a helper

When i try to create a helper for an application that retrieve system software and hardware details using system_profiler command i got the following error.
Response from XPC service: HELLO XPC
Response from XPC service: /usr/sbin/system_profiler:
/usr/sbin/system_profiler: cannot execute binary file"
The code is given below.
class CommandHelper: NSObject,CommandHelperProtocol {
func upperCaseString(_ string: String, withReply reply: #escaping (String) -> Void) {
let response = string.uppercased()
reply(response)
}
func loadServerURL(_ string: String, withReply reply: #escaping (String) -> Void) {
let pipe = Pipe()
let process = Process()
process.launchPath = "/bin/sh"
process.arguments = ["system_profiler","SPHardwareDataType"]
process.standardOutput = pipe
process.standardError = pipe
let fileHandle = pipe.fileHandleForReading
process.launch()
let response = String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)
print(response!)
reply(response!)
}
}
When i set launchPath to /usr/sbin/system_profiler i got blank output.
Shells execute scripts, not binaries. The solution is to run the tool directly; there's hardly any reason to launch a shell just to execute a program:
process.launchPath = "/usr/sbin/system_profiler"
process.arguments = ["SPHardwareDataType"]
Also, there's no point in setting the stderr pipe if you're not going to use it:
/* process.standardError = pipe */

Getting success result on shell script executing installer pkg within an NSTask Swift

I am building an installer for an OS X application in Swift 2.0. The installer does little more than take user input, sends it to a server, and builds a desktop shortcut determined by server response. This shortcut (the 'application') opens up another application (FileMaker). As part of the installation process I am installing FileMaker silently using NSTask which runs a shell script executing installer.app on the FileMaker .pkg. This works fine.
I need to determine whether this installation is successful or not in order to progress to the next step in the installer. I can easily get the string response from the terminal, ie "installer: The install was successful," but I dont feel a hard coded string condition is robust enough. Any other possibilities?
n.b. I'm a beginner Swift developer (1 week!) and have only a year of Web Development behind that. ie. I'm blindingly green.
P.S. Ideally I'd be displaying a progress indicator for the FileMaker installation rather than a message, but that'd be overextending myself (further) if it's even possible.
func installFileMaker() {
let fileMakerFileName = "FileMaker Pro 14"
let fileMakerDirectory = "Resources/FileMaker14_MacClient"
// get resource path
let bundle = NSBundle.mainBundle()
let resourcePathResult = bundle.pathForResource(fileMakerFileName, ofType: "pkg", inDirectory: fileMakerDirectory)
if let resourcePath = resourcePathResult {
displayWebViewMessage("Installing Briefcase Now...")
let command = "installer -pkg \"" + resourcePath + "\" -target /"
Utilities.runAsCommandInBackground(command, callback: installUpdateWebviewCallback)
} else {
// error
displayWebViewMessage("Installation error")
}
print("rrr")
}
Runs shell command
static func runAsCommandInBackground(command: String, callback: (((success:Bool, message:String)) -> Void)?) {
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let pipe = NSPipe()
let task = NSTask()
task.launchPath = "/bin/sh"
task.arguments = ["-c", String(format:"%#", command)]
task.standardOutput = pipe
let file = pipe.fileHandleForReading
task.launch()
var result = ""
if let tmpResult = NSString(data: file.readDataToEndOfFile(), encoding: NSUTF8StringEncoding) {
result = tmpResult as String
} else {
// error
}
dispatch_async(dispatch_get_main_queue()) {
print(result)
// Give me a more robust result!!
if let unwrappedCallback = callback {
unwrappedCallback((true, result as String))
}
}
}
}