It's impossible for me to run an Apple Script from XCode. Here is how I try to make it work :
let appleScript = NSAppleScript(source: script)
var error: NSDictionary?
if let outputString = appleScript?.executeAndReturnError(&error).stringValue {
print(outputString)
} else if (error != nil) {
print("error: \(error!)")
}`
I got this error in output :
error : {
NSAppleScriptErrorAppName = Finder;
NSAppleScriptErrorBriefMessage = "Not authorized to send Apple events to Finder.";
NSAppleScriptErrorMessage = "Not authorized to send Apple events to Finder.";
NSAppleScriptErrorNumber = "-1743"
NSAppleScriptErrorRange = "NSRange: {63,1O}"
I've done everything described here and check every permission on my Entitlement file. "Apple Events" are set to "YES", but that is still not working.
I'm really stuck here. Any suggestion ? I'm on Monterey.
I found a solution to this : putting the applescript.scpt file in the ressources of my app and call it with :
AppKit.NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { event in
let task = Process()
if let path = Bundle.main.path(forResource: "getpathscript.scpt", ofType: nil) {
task.launchPath = "/usr/bin/osascript"
task.arguments = ["\(path)"]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
filepath = (String(data: data, encoding: .utf8)?.trimmingCharacters(in: .newlines))!
}
}
Hope this helps.
Related
I am trying to rename a file on the local hard disk from a swift MacOS application.
Basically I am bringing up the open panel to select the folder where the files are. then I enumerate the files and rename them to the modification date.
Here is the relevant code:
let openPanel = NSOpenPanel()
openPanel.canChooseDirectories = true
openPanel.canChooseFiles = false
openPanel.canCreateDirectories = false
openPanel.allowsMultipleSelection = false
var mtsVideosFolderPathString : String! = ""
if openPanel.runModal() == NSApplication.ModalResponse.OK
{
mtsVideosFolder = openPanel.urls[0] as URL
mtsVideosFolderPathString = mtsVideosFolder?.path
let fileManager = FileManager.default
let enumerator = fileManager.enumerator(atPath: mtsVideosFolderPathString)
while let element = enumerator?.nextObject() as? String
{
if element.hasSuffix("MTS")
{
let filePath = "\(mtsVideosFolderPathString!)/\(element)"
let fileModDate = self.fileModificationDate(url: URL(fileURLWithPath: filePath))
let format = DateFormatter()
format.timeZone = .current
format.dateFormat = "yyyy.MM.dd HH.mm.ss"
let dateString = format.string(from: fileModDate!)
let fromFilename = "\(mtsVideosFolderPathString!)/\(element)"
let toFilename = "\(mtsVideosFolderPathString!)/\(dateString).mts"
print("Rename \(fromFilename) to \(toFilename)")
do {
try fileManager.moveItem(at: URL(fileURLWithPath: fromFilename), to: URL(fileURLWithPath: toFilename))
}
catch let error as NSError
{
print("Ooops! Something went wrong: \(error)")
}
}
}
}
When I run the app its fails at moveItem in the try/catch with the following error:
Rename /Volumes/BigNFast/test/00000.mts to /Volumes/BigNFast/test/2013.08.05 20.09.50.mts
Ooops! Something went wrong: Error Domain=NSCocoaErrorDomain Code=513 "“00000.mp4” couldn’t be moved because you don’t have permission to access “ test”." UserInfo={NSSourceFilePathErrorKey=/Volumes/BigNFast/test/00000.mp4, NSUserStringVariant=(
Move
), NSDestinationFilePath=/Volumes/BigNFast/test/2013.08.05 20.09.50.mp4, NSFilePath=/Volumes/BigNFast/test/00000.mp4, NSUnderlyingError=0x6000002bb450 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}
So, the question is, how do I set the permissions? How to I rename a file on disk?
Thank you for any help
Signing and Capabilities -> File Access -> User Selected File
Change it to read and write with the selector.
I want to execute a Terminal command in my Application and redirect the Terminal output of this command to a TextView (content_scroller). If I run the Application with Apple+R from within Xcode the Progress of this Terminal command is refreshed as it should. But ... If I started the Application the normal way only the first line of terminal output is shown but there is no refresh/new lines anymore. But why? Is there a way to loop the request of the actual output? Here is mit Swift 5 Code:
func syncShellExec(path: String, args: [String] = []) {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = [path] + args
let outputPipe = Pipe()
let filelHandler = outputPipe.fileHandleForReading
process.standardOutput = outputPipe
process.launch()
filelHandler.readabilityHandler = { pipe in
let data = pipe.availableData
if let line = String(data: data, encoding: .utf8) {
DispatchQueue.main.sync {
self.content_scroller.string += line
self.content_scroller.scrollToEndOfDocument(nil)
}
}
process.waitUntilExit()
filelHandler.readabilityHandler = nil
}
Should be able to direct output straight to text view if I understand your question correctly. Something like the following outputs an error (I didn't test it.)
import Cocoa
func syncShellExec(path: String, args: [String] = []) {
var status : Int32
var dataRead : Data
var stringRead :String?
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = [path] + args
let outputPipe = Pipe()
let txtView = NSTextView()
let fileHandler = outputPipe.fileHandleForReading
process.standardOutput = outputPipe
process.launch()
process.waitUntilExit()
status = process.terminationStatus
dataRead = fileHandler.readDataToEndOfFile()
stringRead = String.init(data: dataRead, encoding: String.Encoding.utf8)
if (status != 0) {
txtView.string.append("Terminated with error.\n")
txtView.string.append(stringRead!)
}
}
I have a java program running in the background of my swift program. The java program can read user input in a command line. How can I pass "commands", or text line into the jar after it has been launched?
Just like the way the program down below read from the java program, how can I then "reply" to it?
let b = Bundle.main
let path = b.path(forResource: "myjar", ofType: "jar")!
NSLog("%#", "jar path : \(path)")
task.launchPath = "/usr/bin/java"
task.arguments = ["-jar", path]
let pipe = Pipe()
task.standardOutput = pipe
let errorPipe = Pipe()
task.standardError = errorPipe
task.launch()
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var progressObserver : NSObjectProtocol!
progressObserver = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: outHandle, queue: nil){
notification -> Void in
let data = outHandle.availableData
if data.count > 0 {
if let str = String(data: data, encoding: String.Encoding.utf8) {
NSLog("%#", str)
}
outHandle.waitForDataInBackgroundAndNotify()
} else { NotificationCenter.default.removeObserver(progressObserver)
}
}
I then tried the following without luck:
pipe.fileHandleForWriting.write("text to send to java program".data(using: String.Encoding.utf8)
You're pretty close. If you just added the code pipe.fileHandleForWriting.write("text to send to java program"... that's not going to work because that pipe is the one you assigned to standardOutput.
You need to create yet another Pipe object, assign it to standardInput, and write to that:
...
>> let inputPipe = Pipe()
>> task.standardInput = inputPipe
task.launch()
>> inputPipe.fileHandleForWriting.write("text to send to java program".data(using: String.Encoding.utf8)
...
I'm using an NSTask to run rsync, and I'd like the status to show up in the text view of a scroll view inside a window. Right now I have this:
let pipe = NSPipe()
task2.standardOutput = pipe
task2.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: NSASCIIStringEncoding)! as String
textView.string = output
And that get's me the some of the statistics about the transfer, but I'd like to get the output in real time, like what get's printed out when I run the app in Xcode, and put it into the text view. Is there a way to do this?
Since macOS 10.7, there's also the readabilityHandler property on NSPipe which you can use to set a callback for when new data is available:
let task = NSTask()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]
let pipe = NSPipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.readabilityHandler = { pipe in
if let line = String(data: pipe.availableData, encoding: NSUTF8StringEncoding) {
// Update your view with the new text here
print("New ouput: \(line)")
} else {
print("Error decoding data: \(pipe.availableData)")
}
}
task.launch()
I'm surprised nobody mentioned this, as it's a lot simpler.
(See Patrick F.'s answer for an update to Swift 3/4.)
You can read asynchronously from a pipe, using notifications.
Here is a simple example demonstrating how it works, hopefully that
helps you to get started:
let task = NSTask()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]
let pipe = NSPipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var obs1 : NSObjectProtocol!
obs1 = NSNotificationCenter.defaultCenter().addObserverForName(NSFileHandleDataAvailableNotification,
object: outHandle, queue: nil) { notification -> Void in
let data = outHandle.availableData
if data.length > 0 {
if let str = NSString(data: data, encoding: NSUTF8StringEncoding) {
print("got output: \(str)")
}
outHandle.waitForDataInBackgroundAndNotify()
} else {
print("EOF on stdout from process")
NSNotificationCenter.defaultCenter().removeObserver(obs1)
}
}
var obs2 : NSObjectProtocol!
obs2 = NSNotificationCenter.defaultCenter().addObserverForName(NSTaskDidTerminateNotification,
object: task, queue: nil) { notification -> Void in
print("terminated")
NSNotificationCenter.defaultCenter().removeObserver(obs2)
}
task.launch()
Instead of print("got output: \(str)") you can append the received
string to your text view.
The above code assumes that a runloop is active (which is the case
in a default Cocoa application).
This is the update version of Martin's answer above for the latest version of Swift.
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]
let pipe = Pipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var obs1 : NSObjectProtocol!
obs1 = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
object: outHandle, queue: nil) { notification -> Void in
let data = outHandle.availableData
if data.count > 0 {
if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
print("got output: \(str)")
}
outHandle.waitForDataInBackgroundAndNotify()
} else {
print("EOF on stdout from process")
NotificationCenter.default.removeObserver(obs1)
}
}
var obs2 : NSObjectProtocol!
obs2 = NotificationCenter.default.addObserver(forName: Process.didTerminateNotification,
object: task, queue: nil) { notification -> Void in
print("terminated")
NotificationCenter.default.removeObserver(obs2)
}
task.launch()
I have an answer which I believe is more clean than the notification approach, based on a readabilityHandler. Here it is, in Swift 5:
class ProcessViewController: NSViewController {
var executeCommandProcess: Process!
func executeProcess() {
DispatchQueue.global().async {
self.executeCommandProcess = Process()
let pipe = Pipe()
self.executeCommandProcess.standardOutput = pipe
self.executeCommandProcess.launchPath = ""
self.executeCommandProcess.arguments = []
var bigOutputString: String = ""
pipe.fileHandleForReading.readabilityHandler = { (fileHandle) -> Void in
let availableData = fileHandle.availableData
let newOutput = String.init(data: availableData, encoding: .utf8)
bigOutputString.append(newOutput!)
print("\(newOutput!)")
// Display the new output appropriately in a NSTextView for example
}
self.executeCommandProcess.launch()
self.executeCommandProcess.waitUntilExit()
DispatchQueue.main.async {
// End of the Process, give feedback to the user.
}
}
}
}
Please note that the Process has to be a property, because in the above example, given that the command is executed in background, the process would be deallocated immediately if it was a local variable. Thanks for your attention.
I've written the function that launches Tor process. It's a process that won't stop until SIGTERM is sent to it, so, to avoid app freezing, I run this process in a background queue (Tor needs to be launched when the application is started and finished when application is terminated, and user needs to do some other things meanwhile). That's my code for Tor launching:
func launchTor(hashedPassword hash : String) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
let task = NSTask()
task.launchPath = "/bin/bash"
print("Hashed password : \(hash)")
task.arguments = (["-c", "/usr/local/bin/tor HashedControlPassword \(hash)"])
let pipe = NSPipe()
task.standardOutput = pipe
let handle = pipe.fileHandleForReading
handle.waitForDataInBackgroundAndNotify()
let errPipe = NSPipe()
task.standardError = errPipe
let errHandle = errPipe.fileHandleForReading
errHandle.waitForDataInBackgroundAndNotify()
var startObserver : NSObjectProtocol!
startObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSFileHandleDataAvailableNotification, object: nil, queue: nil) { notification -> Void in
let data = handle.availableData
if data.length > 0 {
if let output = String(data: data, encoding: NSUTF8StringEncoding) {
print("Output : \(output)")
}
}
else {
print("EOF on stdout")
NSNotificationCenter.defaultCenter().removeObserver(startObserver)
}
}
var endObserver : NSObjectProtocol!
endObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSTaskDidTerminateNotification, object: nil, queue: nil) {
notification -> Void in
print("Task terminated with code \(task.terminationStatus)")
NSNotificationCenter.defaultCenter().removeObserver(endObserver)
}
var errObserver : NSObjectProtocol!
errObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSTaskDidTerminateNotification, object: nil, queue: nil) {
notification -> Void in
let data = errHandle.availableData
if (data.length > 0) {
if let output = String(data: data, encoding: NSUTF8StringEncoding) {
print("Error : \(output)")
NSNotificationCenter.defaultCenter().removeObserver(errObserver)
}
}
}
task.launch()
_ = NSNotificationCenter.defaultCenter().addObserverForName("AppTerminates", object: nil, queue: nil) {
notification -> Void in
task.terminate()
}
task.waitUntilExit()
}
}
When it's launched, everything is OK, but then the whole app freezes. When I stop an app, I always see that let data = handle.availableData line is running. How to fix this issue?