AppleScript/OSAKit/Cocoa Inter-Application Communication Memory Leak - objective-c

I have an app that supports various AppleScript commands and I'm running into a memory leak issue when passing a list (array) object from a script into the app. There isn't a ton a 100% clear and helpful documentation on adding AppleScript support to your cocoa app, so I'm hoping this is just a simple oversight/error on my part and someone can help me find it.
My end goal is to have another Cocoa app compile and run the script via OSAKit but I've also tested using macOS's Script Editor.app to pass in a list and my main Cocoa app still leaks when doing it that way too:
-- This AppleScript command causes leaks
tell application "MyApp"
update database with things {"thing1", "thing2", "thing3"}
end tell
In my other Cocoa app (not the main Cocoa app), I run the above via OSAKit but it leaks there as well:
#import <OSAKit/OSAKit.h>
OSAScript *script= [[OSAScript alloc] initWithSource:[NSString stringWithFormat:#"tell application \"MyApp\"\nupdate database with things %#", listOfThings]];
NSDictionary * errorDict = nil;
NSAppleEventDescriptor *returnDescriptor = [script executeAndReturnError: &errorDict];
On the receiving end, my main Cocoa app has an NSScriptCommand class set up and can receive and respond to AppleScript commands without any other known issues. Here's what it looks like just for the list part:
MyScriptHandler.h
#import "AppDelegate.h"
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#interface MyScriptHandler : NSScriptCommand
- (NSAppleEventDescriptor *) performDefaultImplementation;
#end
**MyScriptHandler.m**
#import "MyScriptHandler.h"
#implementation MyScriptHandler
- (NSAppleEventDescriptor *) performDefaultImplementation
{
NSString* cmdName = [[self commandDescription] commandName];
if ([cmdName isEqualToString: #"update database"])
{
// GET THE LIST AND PASS IT OFF TO APP DELEGATE FOR PROCESSING
NSMutableArray *things = [[self evaluatedArguments] valueForKey:#"things"];
[(AppDelegate *)[[NSApplication sharedApplication] delegate] updateDatabaseFromAppleScript:options];
}
return [NSAppleEventDescriptor descriptorWithBoolean:YES];
}
Not sure this is helpful, but my main Cocoa app's AppleScript dictionary (.sdef) looks like this for the command:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="MyApp Terminology">
<suite name="MyApp Suite" code="MyAp" description="Commands for MyApp">
<command name="update database" code="MyApUpDB" description="Update the database">
<parameter name="with things" code="optn" type="list of text" optional="yes" description="">
<cocoa key="things"/>
</parameter>
<cocoa class="MyScriptHandler"/>
</command>
</suite>
</dictionary>
Thank you in advance for any help!

I believe I have fixed the memory leak issue by making the following changes...
In my .sdef file, I changed the parameter type for the command to be type="record" instead of type="list of text". This means I have to pass some useless/duplicate information (the key name) into my main app, but that's acceptable since it's now not leaking and doesn't cost any extra CPU as far as I can tell.
<command name="update database" code="MyApUpDB" description="Update the database">
<parameter name="with things" code="optn" type="record" optional="yes" description="">
<cocoa key="things"/>
</parameter>
<cocoa class="MyScriptHandler"/>
</command>
Then, I updated the AppleScript to pass a record instead of a list:
-- This AppleScript command does NOT cause leaks
tell application "MyApp"
update database with things {thing1:"thing1", thing2:"thing2", thing3:"thing3"}
end tell

Related

Applescript command in document class

I am trying to create an applescript command in my document class. I know I am doing something wrong, but I am not sure how to do this.
From what I understand (which may be incorrect), when I create a new command
I need to specify a new class for that command. But from that new class, lets call it
ScriptResetCommand, how do I access the document object from the performDefaultImplementation method? The applescript call is something like
tell document 1 of application "DocScript" to simple reset
Here is my current code:
ScriptResetCommand.m file:
#implementation ScriptResetCommand
- (id)performDefaultImplementation {
// Somehow I need to access the correct document class and
// perform my reset.
NSLog(#"ScriptResetCommand performDefaultImplementation");
return #"Reset Stuff";
}
ScriptResetCommand.h file:
#interface ScriptResetCommand : NSScriptCommand
- (id)performDefaultImplementation;
.sdef file:
<command name="simple reset" code="jDsgSrst" description="run a simple reset">
<cocoa class="ScriptResetCommand"/>
<result type="text" description="returns the result"/>
</command>
So with this code, I can successfully call the performDefaultImplementation method in the ScriptResetCommand class, but how do I then access the Document object which has the desired reset command in it?
Thanks in advance.
You should be able to access the document object using the method -[NSScriptCommand evaluatedReceivers]. Try something like this:
- (id)performDefaultImplementation {
NSDocument *document = [self.evaluatedReceivers firstObject];
if (![document isKindOfClass:[NSDocument class]]) {
// I'm just guessing how this error should be handled; untested (for example, you'll want to make sure your app returns an error if you run "reset stuff" without specifying a document)
[self setScriptErrorExpectedTypeDescriptor:[NSAppleEventDescriptor descriptorWithTypeCode:cDocument]];
return nil;
}
NSLog(#"Got document: %#", document);
return #"Reset Stuff";
}
I'm not sure what
tell document 1 of application "DocScript" to simple reset
would do, but to process it, break it down into a verb (simple reset) and a target (document 1 of application "DocScript"). When the command is executed, a reference to the target will be provided in [command directParameter], from which you can get an objectSpecifier for your document.
Here's how I implemented text selection in my scriptable text editor, Ted. The verb (command) is "select" and the target is "paragraph 5 of document 1".
tell application "Ted"
select paragraph 5 of document 1
end tell
SDEF: The command is defined in the Text Suite objects:
<suite name="Text Suite" code="????">
...
<class name="paragraph" code="cpar" inherits="item" >
...
<responds-to command="select">
<cocoa method="handleSelectCommand:"/>
</responds-to>
</class>
...
</suite>
The "select" command actually is included for EVERY class in the text suite that can be selected: e.g. paragraph, line, word, character.
Implementation:
Since all the various scriptable text objects are represented as NSTextStorage classes, I just use a category on NSTextStorage to implement the command.
#implementation NSTextStorage (Scriptability)
- (void) handleSelectCommand:(NSScriptCommand *)command
{
NSScriptObjectSpecifier *directParameter =[command directParameter];
NSScriptObjectSpecifier *container = [directParameter containerSpecifier];
TedDocument *document = [self extractDocument:container];
NSString *contents = document.textView.string;
NSRange selRange;
[self getRangeForSelectCommand:directParameter contents:contents range:&selRange];
[document.textView setSelectedRange:selRange];
}
#end
A reference to the document is provided by the directParameter, but it is part of a containment hierarchy, in this case 'paragraph 5 of document 1 of application "Ted"', so we need a method to walk that hierarchy to extract the document from it. Here is that function:
// Navigate the containment hierarch using recursion, if necessary.
- (TedDocument *) extractDocument:(NSScriptObjectSpecifier *)container
{
if (container == nil)
{
NSLog(#"%#", #"ERROR: extractDocument received nil container reference");
return nil;
}
FourCharCode containerClass = [[container keyClassDescription] appleEventCode];
if (containerClass != 'docu')
{
return [self extractDocument:[container containerSpecifier]];
}
return [container objectsByEvaluatingSpecifier];
}

NSURLSession or NSURLConnection - iOS 6 support

I need to make a connection to my server to get some JSON data and I have to support both iOS 6 and iOS 7.
Should I create two classes? One with NSURLSession for iOS 7 and one with NSURLConnection for iOS 6? Or should I just use NSURLConnection for both of them?
What benefit would you gain by creating two separate classes that do essentially the same thing? If you can't use NSURLSession because it's only supported in iOS 7, and you can get the same functionality using NSURLConnection, which works in both, then just use NSURLConnection. You will have less code to maintain.
Great question. In fact I had the same question and researched it quite a bit and I think this is a good place to use a protocol (a.k.a. interface in other languages). This is based off of the quote "Program to an interface, not an implementation" from the famous "Gang of Four" Patterns Book. I think it's best to try and code for the future so I never get hammered if they decide to deprecate something (which isn't the case here, but you never know).
Instead of writing classes, write a protocol that defines the methods you want to use and then create 2 different classes that implement those methods. In your app you would make a pointer that can point to any class that implements all of that protocols methods and then each implementing class can use whatever frameworks/libraries/other code they want to make that happen.
As an example, you could create a Server protocol like this:
// Server.h
#protocol Server <NSObject>
#required
- (void)callService:(NSString *)service withData:(NSData *)data;
#end
and then create a RestServer class like this:
// RestServer.h
#import "Server.h"
#interface RestServer : NSObject <Server>
#end
// RestServer.m
#import "RestServer.h"
#implementation RestServer
- (void)callService:(NSString *)service withData:(NSData *)data {
// Code using REST
}
#end
and then create another class like SoapServer:
// SoapServer.h
#import "Server.h"
#interface SoapServer : NSObject <Server>
#end
// SoapServer.m
#import “SoapServer.h"
#implementation SoapServer
- (void)callService:(NSString *)service withData:(NSData *)data {
// Code using SOAP
}
#end
Code your main app to just use a pointer to the interface and now you can swap classes without having to change your main code:
// SomeViewController.m
#import “Server.h”
#import “RestServer.h”
#import “SoapServer.h”
…
- (void)someMethod() {
id<Server> myServer;
if ([self shouldIUseSoap])
myServer = [[SoapServer alloc] init];
else
myServer = [[RestServer alloc] init];
[myServer callService:#"loginUser" withData:[self getData]];
}
Now you can change server classes whenever you want and never have to go hunt down all the places in your code where you make calls to callService:withData:. THIS IS THE BENEFIT OF PROGRAMMING TO INTERFACES!
I used Rest vs Soap because I figured people newer to Objective-C might understand that better, but in your case you’d maybe have a ConnectionServer vs SessionServer or something like that.
Another good read on programming to interfaces/protocols can be found here: https://stackoverflow.com/a/384067/504873
If you have to use NSURLCredentialPersistenceForSession if you have to get into a Windows Authentication network...then using NSURLConnection will create multiple problems for you. I'm going through the pain right now and have come to the conclusion that I need both to support iOS 7. Basically, if you use NSURLConnection and willSendRequestForAuthenticationChallenge, you will find that in iOS 7, your session will end with a mind of it's own (seems like a 30 second mind span). So if you have to persist a credential to access more SOAP or whatever, welcome to the terror dome! I will report back to you with code if I find a smooth solution.
At time of writing, NSURLConnection has been deprecated in OS X 10.11 and iOS 9.0 but my Apps need to support OS X 10.7 and iOS 6.
So now you HAVE to use NSURLSession for ongoing projects BUT also support the now deprecated NSURLConnection class for supported legacy OS releases!
I would these days vote for TenaciousJay solution with Compiler warning suppression around the NSURLConnection class implementation.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
/* NSURLConnection code here */
#pragma GCC diagnostic pop
The benefit you would you gain by creating two separate classes that do essentially the same thing is that you can eventually CUT off the old, deprecated solution when you can finally drop support for legacy OS releases.
My code decision to use one class or the other would not be based upon some class property but on the result of a Macro like:
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
for iOS or for OS X:
NSString *systemVersion = nil;
if ([[NSProcessInfo processInfo] respondsToSelector:NSSelectorFromString(#"operatingSystemVersion")]) {
NSOperatingSystemVersion operatingSystemVersion = [[NSProcessInfo processInfo] operatingSystemVersion];
systemVersion = [NSString stringWithFormat:#"%ld.%ld.%ld", (long)operatingSystemVersion.majorVersion, (long)operatingSystemVersion.minorVersion, (long)operatingSystemVersion.patchVersion];
} else {
SInt32 versionMajor=0, versionMinor=0, versionPatch=0;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
Gestalt(gestaltSystemVersionMajor, &versionMajor);
Gestalt(gestaltSystemVersionMinor, &versionMinor);
Gestalt(gestaltSystemVersionBugFix, &versionPatch);
#pragma GCC diagnostic pop
systemVersion = [NSString stringWithFormat:#"%ld.%ld.%ld", (long)versionMajor, (long)versionMinor, (long)versionPatch];
}
NSLog(#"[Line %d] %s OS X Runtime Version: '%#'", __LINE__, __PRETTY_FUNCTION__, systemVersion);

EXC_BAD_ACCESS when using the same NSFileHandle object in a Cordova plugin

I'm experiencing an access problem in my Cordova plugin: my NSFileHandle "loses" context between Cordova calls, and I end up with either an EXC_BAD_ACCESS, a SIGABRT or an Unrecognized Selector sent to instance error. Debugging and digging inside Obj-C's documentation has given me no lead on this, so I'd appreciate your help here very much!
Here's my code. First - the interface:
#interface MyPlugin : CDVPlugin
...
- (void) startWriting:(CDVInvokedUrlCommand*)command;
- (void) stopWriting:(CDVInvokedUrlCommand*)command;
#end
And the implementation:
....
static NSFileHandle *logFile;
#implementation MyPlugin
- (void) startWriting:(CDVInvokedUrlCommand*)command{
logFile = [NSFileHandle fileHandleForWritingAtPath:#"path_to_my_file"];
NSData nsData = [#"Hello World!" dataUsingEncoding:NSUTF8StringEncoding];
[logFile writeData:nsData];
}
- (void) stopWriting:(CDVInvokedUrlCommand*)command{
NSData nsData = [#"Goodbye World!" dataUsingEncoding:NSUTF8StringEncoding];
[logFile writeData:nsData];
}
I call startWriting and then stopWriting using cordova.exec. The error occurs on the last line of stopWriting. There were a few times that the problem miraculously disappeared, but in most cases I get one of the aforementioned errors.
It appears that my logFile object closes the file seamlessly, but according to iOS documentation, this usually happens when the NSFileHandle object is deallocated, whereas my object is declared as static, and is not supposed to be deallocated as long as my plugin lives (plus, I see in the XCode debugger that it is still allocated).
What, in your opinion, causes my NSFileHandle object to "lose" the actual file?
Imho - logFile is released once function finishes its job. You should change your code to something like
if (logFile==nil) logFile = [NSFileHandle fileHandleForWritingAtPath:#"path_to_my_file"];
or manually retain/release the logFile object.

Developing a non-GUI user agent in Objective-C using NSDistributedNotificationCenter

I would like to create a user agent in Objective-C that listens for notifications from the default NSDistributedNotificationCenter. The agent will not have a GUI. When I create a Cocoa application (I will also be using Distributed Objects, which I think is only in Cocoa) in Xcode, however, Xcode sets the project as a GUI application.
In the main function, I remove the NSApplicationMain(...) function call to remove the GUI elements from the application. However, now I can't get the thread to wait (listen for) notifications coming in from the NSDistributedNotificationCenter. The app just starts and quits immediately.
I looked into using the NSRunLoop from the current NSThread, however, it seems that NSRunLoops only wait on NSPorts. There's no mention of waiting on NSNotifications.
NSDistributedNotificationCenter is Foundation, so you don't need to create a GUI app. You can create a command line template, for example, and run it from terminal. As a very simple example, you could create an example that just prints out every distributed notification it receives below.
To build, copy into an Xcode template for a Foundation command line app, or simply copy into a text file named something like test_note.m and build according to the comments. In this example, the application will never end (CFRunLoopRun() never returns) and you will have to kill it by hitting CTRL+C from the terminal or killing it with something like kill or the activity monitor.
// test_build.m
// to build: clang -o test_build test_build.m -framework foundation
#import <Foundation/Foundation.h>
#interface Observer : NSObject
- (void)observeNotification:(NSNotification*)note;
#end
#implementation Observer
- (void)observeNotification:(NSNotification*)note
{
NSLog(#"Got Notification: %#", note);
}
#end
int main (int argc, char const *argv[])
{
#autoreleasepool {
Observer* myObserver = [[Observer alloc] init];
[[NSDistributedNotificationCenter defaultCenter] addObserver:myObserver selector:#selector(observeNotification:) name:nil object:nil];
CFRunLoopRun();
}
return 0;
}

How do I add Applescript support to my Cocoa application?

I am new to the world of Cocoa programming, and I want to add Applescript support to my application. The examples at Apple's website seem out of date.
How do I add Applescript support to my Cocoa application?
If you want to send AppleScript from your application and need a sandboxed app, you need to create a temporary entitlement
You need to add those two keys in your info.plist
<key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>MyAppName.sdef</string>
...of course you have to change "MyAppName" to your app's name
Create a .sdef file and add it to your project.
The further course now greatly depends on the needs of your application, there are:
Class Elements (create an object from AppleScript)
Command Elements (override NSScriptCommand and execute "verb-like" commands)
Enumeration Elements
Record-Type Elements
Value-Type Elements (KVC)
Cocoa Elements
-
Go here to find a detailed description and many details on their implementation: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ScriptableCocoaApplications/SApps_script_cmds/SAppsScriptCmds.html
I found working with Class and KVC Elements very complicated, as I just wanted to execute a single command, nothing fancy. So in order to help others, here's an example of how to create a new simple command with one argument. In this example it'll "lookup" one string like this:
tell application "MyAppName"
lookup "some string"
end tell
The .sdef file for this command looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="MyAppName">
<suite name="MyAppName Suite" code="MApN" description="MyAppName Scripts">
<command name="lookup" code="lkpstrng" description="Look up a string, searches for an entry">
<cocoa class="MyLookupCommand"/>
<direct-parameter description="The string to lookup">
<type type="text"/>
</direct-parameter>
</command>
</suite>
</dictionary>
Create a subclass of NSScriptCommand and name it MyLookupCommand
The MyLookupCommand.h
#import <Foundation/Foundation.h>
#interface MyLookupCommand : NSScriptCommand
#end
The MyLookupCommand.m
#import "MyLookupCommand.h"
#implementation MyLookupCommand
-(id)performDefaultImplementation {
// get the arguments
NSDictionary *args = [self evaluatedArguments];
NSString *stringToSearch = #"";
if(args.count) {
stringToSearch = [args valueForKey:#""]; // get the direct argument
} else {
// raise error
[self setScriptErrorNumber:-50];
[self setScriptErrorString:#"Parameter Error: A Parameter is expected for the verb 'lookup' (You have to specify _what_ you want to lookup!)."];
}
// Implement your code logic (in this example, I'm just posting an internal notification)
[[NSNotificationCenter defaultCenter] postNotificationName:#"AppShouldLookupStringNotification" object:stringToSearch];
return nil;
}
#end
That's basically it. The secret to this is to subclass NSScriptCommand and override performDefaultImplementation. I hope this helps someone to get it faster...
Modern versions of Cocoa can directly interpret the scripting definition (.sdef) property list, so all you need to do for basic AppleScript support is to create the sdef per the docs, add it to your "copy bundle resources" phase and declare AppleScript support in your Info.plist. To access objects other than NSApp, you define object specifiers, so each object knows its position in the scripting world's hierarchy. That gets you kvc manipulation of object properties, and the ability to use object methods as simple script commands.
A simple example to get you started,
place a script (named dialog) into the documents folder then you can run it from Xcode
NSArray *arrayPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDirectory = [arrayPaths objectAtIndex:0];
NSString *filePath = [docDirectory stringByAppendingString:#"/dialog.scpt"];
NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithContentsOfURL:[NSURL fileURLWithPath:filePath] error:nil];
[scriptObject executeAndReturnError:nil];
The nice thing about keeping the script external is the ability to edit it outside of Xcode.
I would recommend adding the error checking if you did start editing as the applescript may not compile
maybe check with
if(scriptObject.isCompiled){