Scriptable stay-open application freezes after nth command sent to it

I have created a scriptable stay-open ASObjC application called “Utility Handlers” that serves as a general purpose library of utility handlers that scripts can utilize. The problem is that the application freezes after a certain number of times commands are sent to the application. Before going into the problem in more detail, I will start with a description of the application.

Each of the app’s handlers takes its own distinct set of input parameters. Because it is a general purpose library, it must be able to accommodate input parameters and returned values of any arbitrary class. For simplicity of coding, rather than create a separate NSScriptCommand file for each handler (and thus a separate command that would be sent to the application), the application responds to a single command, namely xeq, with a single direct object argument in the form of an Applescript record. The record has a handlerName property, which is the name of the handler to execute, along with whatever other properties that particular handler requires as input arguments. Each handler is responsible for parsing the input record for its required parameters. By way of example, if the handler is getSumOfTwoNumbers, then the calling program might issue the following command:

tell application "Utility Handlers" to set theSum to xeq {handlerName:"getSumOfTwoNumbers", firstNumber:[...first number goes here...], secondNumber:[...second number goes here...]}

The getSumOfTwoNumbers handler would expect to find the firstNumber and secondNumber properties in the input record and would return the sum of those two numbers.

The NSScriptCommand file for the application is named ExecuteHandler.applescript. The direct object input parameter is expected to be an Applescript record, which is enabled by setting the type to type=“any” in the sdef file and which the NSScriptCommand file accesses with the construct

my directParameter() as record

The value returned by NSScriptCommand file is also an Applescript record. This is enabled by specifying type=“any” in the sdef file and is achieved by converting the Applescript record to be returned to an NSAppleEventDescriptor (the only way I could discern to return a list or record to the calling program; perhaps there are other ways??) Not knowing of a straightforward way to convert an Applescript record to an NSAppleEventDescriptor, I ended up using an ugly hack, namely getting a text version of the results record using the forced error message trick

|| of {...record...}

then setting that text as the source for an NSAppleScript, whose return result is always an NSAppleEventDescriptor. Ugly, but it works and can accommodate Applescript values of virtually any class. Here is the code for the NSScriptCommand file with two utility handlers, namely getSumOfTwoNumbers and replicateString (error checking and other niceties removed for the sake of brevity):

script ExecuteHandler

	property parent : class "NSScriptCommand"
	property NSAppleScript : class "NSAppleScript"
	property inputRecord : missing value
	
	on performDefaultImplementation()
		set my inputRecord to my directParameter() as record
		set handlerName to my inputRecord's handlerName
		if handlerName = "getSumOfTwoNumbers" then
			set handlerResult to my getSumOfTwoNumbers()
		else if handlerName = "replicateString" then
			set handlerResult to my replicateString()
		else if handlerName = "someOtherHandler" then
			--	etc
		end if
		set resultsRecord to {theResult:handlerResult}
		set resultsRecordAsText to my getValueAsText(resultsRecord)
		set resultsRecordAsNSAppleEventDescriptor to NSAppleScript's alloc()'s initWithSource_(resultsRecordAsText)'s executeAndReturnError_(missing value)
		return resultsRecordAsNSAppleEventDescriptor
	end performDefaultImplementation
	
	on getValueAsText(theValue)
		try
			|| of theValue
		on error m
			set o to offset of "|| of " in m
			set theValueAsText to m's text (o + 6) thru -2
		end try
		return theValueAsText
	end getValueAsText

	on getSumOfTwoNumbers()
		-- takes two input parameters: firstNumber, secondNumber
		tell my inputRecord to return (its firstNumber) + (its secondNumber)
	end getSumOfTwoNumbers
	
	on replicateString()
		-- takes two input parameters: replicationFactor, theString
		set replicatedString to ""
		repeat inputRecord's replicationFactor times
			set replicatedString to replicatedString & inputRecord's theString
		end repeat
		return replicatedString
	end replicateString
	
end script

The sdef file is coded with type=“any” for both the direct object input parameter and the return value so that they may be coded as Applescript records with properties whose values may be of any arbitrary class in the calling script. Here is the sdef file:

<?xml version="1.0" encoding="UTF-8"?>
<suite name="Standard Suite" code="^sts" description="Common classes and commands for all applications.">
	<class name="application" code="capp" description="The application's top-level scripting object.">
		<cocoa class="NSApplication"/>
		<property name="name" code="pnam" type="text" access="r" description="The name of the application."/>
		<property name="frontmost" code="pisf" type="boolean" access="r" description="Is this the active application?">
			<cocoa key="isActive"/>
		</property>
		<property name="version" code="vers" type="text" access="r" description="The version number of the application."/>
	</class>
</suite>

<suite name="Utility Handler Suite" code="^UtH" description="Utility handlers.">
	<command name="xeq" code="^UtH^xeq" description="Execute a utility handler.">
		<cocoa class="ExecuteHandler" />
		<direct-parameter description="A record whose handlerName property is the name of the handler to be executed and whose remaining properties are the handler's arguments.">
			<type type="any"/>
		</direct-parameter>
		<result description="The handler return value.">
			<type type="any" />
		</result>
	</command>
</suite>

The application works as intended. The problem is that after a certain number of handler calls (“xeq” commands), the application freezes and must be quit and re-launched in order to become responsive again. The number of times a given handler call must be issued before the application freezes seems to be inversely related to the complexity of the task that handler performs. Some very simple handlers may run a few hundred times before the application reproducibly freezes at the exact same number of times the command is sent to it, whereas some particularly complex handlers may run only once then reproducibly freeze on the second try. (Thus far, at least, even the most complex handlers execute at least once successfully.) The striking finding is the constancy of the number of handler calls before freezing ensues for a given handler and set of input parameters. That constancy makes me wonder if the underlying problem is some kind of memory overflow or object leak, but these are only guesses from a relatively inexperienced OO programmer.

Examples (note the last example, which freezes the 2nd time the handler call is made):

tell application "Utility Handlers"
xeq {handlerName:"getSumOfTwoNumbers", firstNumber:3, secondNumber:7}
-- > {theResult:10}; freezes the 222nd time the command is sent
xeq {handlerName:"replicateString", replicationFactor:2, theString:"x"}
--> {theResult:"xx"}; freezes the 207th time the command is sent
xeq {handlerName:"replicateString", replicationFactor:2, theString:"With malice toward none, with charity for all, with firmness in the right as God gives us to see the right, let us strive on to finish the work we are in, to bind up the nation's wounds, to care for him who shall have borne the battle and for his widow and his orphan, to do all which may achieve and cherish a just and lasting peace among ourselves and with all nations."}
--> {theResult:...the string replicated twice...}; freezes the 47th time the command is sent
xeq {handlerName:"replicateString", replicationFactor:25, theString:"With malice toward none, with charity for all, with firmness in the right as God gives us to see the right, let us strive on to finish the work we are in, to bind up the nation's wounds, to care for him who shall have borne the battle and for his widow and his orphan, to do all which may achieve and cherish a just and lasting peace among ourselves and with all nations."}
--> {theResult:...the string replicated 25 times...}; freezes the 2nd time the command is sent
end tell

I would be most grateful for help in understanding the cause of the application freeze. Also, as an ancillary question, I would like to ask (1) if there is a better way of returning an arbitrary Applescript value from the stay-open application other than the type=“any”/Applescript record as NSAppleEventDescriptor technique I am using, and (2) if there is a better way of converting an arbitrary Applescript value to an NSAppleEventDescriptor other than the NSAppleScript technique I am using.

Model: MacBook Pro
Browser: Safari 536.30.1
Operating System: Mac OS X (10.8)

Hi,

for returning a record (NSDictionary) it’s not necessary to create an explicit NSAppleEventDescriptor.
Cocoa Scripting can handle all standard AppleScript types without coercing.

I recommend to set the direct parameter and return type to record instead of any
and return an NSDictionary object.

I recommend also to implement a robust error handling, for example if the direct parameter couldn’t be coerced to record

Thank you for the prompt reply, and apologies for not responding sooner (I was called away from scripting for a couple of days.)

I changed type=“any” to type=“record” for the direct input parameter in the sdef file and issued the following command:

tell application "Utility Handlers" to xeq {handlerName:"getSumOfTwoNumbers", firstNumber:3, secondNumber:7}

The expected return value was:

{theResult:10}

Instead, the command failed with the following error:

Applescript Execution Error:
Utility Handlers got an error: {handlerName:“getSumOfTwoNumbers”, firstNumber:3, secondNumber:7} doesn’t understand the xeq message.

Alternatively, leaving type=“any” for the input, I changed type=“any” to type=“record” for the return value in the sdef file and modified the NSScriptCommand file as follows:

		set resultsRecordAsNSDictionary to current application's class "NSDictionary"'s dictionaryWithObject_forKey_(handlerResult, "theResult")
		return resultsRecordAsNSDictionary

I confirmed that an NSDictionary with the correct key and object was created by logging. The same command as above failed with the following error:

Error while returning the result of a script command: the result object…
{
theResult = 10;
}
…could not be converted to an Apple event descriptor of type ‘record’. This instance of the class ‘__NSDictionaryI’ doesn’t respond to -scriptingRecordDescriptor messages.

I also tried changing both input and output type to “record” without success. So for some reason, it seems that I am unable to get type=“record” to work. Frustrating!

(Also, thank you for the reminder concerning robust error handling, I do have extensive error checking in my code but left it out for the sake of brevity in this thread.)

Alternatively you can create the event descriptor with

set recordDescriptor to current application's NSAppleEventDescriptor's recordDescriptor()
recordDescriptor's setDescriptor_forKeyword_(current application's NSAppleEventDescriptor's descriptorWithInt32_(7), 'wxyz')

but I don’t know to “translate” the 4-letter codes into ASOC

Anyway If you want to pass a record as (direct) parameter you have to create a record-type in the sdef file
to specify the property names and 4-letter codes

Thank you for the suggestions.

I couldn’t get setDescriptor_forKeyword_ to create the desired NSAppleEventDescriptor, namely one for an Applescript record with a property whose value may be of any arbitrary Applescript class. It seems that that and other descriptor initializers require an explicit specification of value class, along the lines of your suggestion for the sdef file.

I fooled around with descriptorWithDescriptorType_data_ thinking it might be more generic. I was able to convert an arbitrary Applescript record to an NSData with the following:

set handlerResult to 1 -- or 2.2, or "three", or true, or {1, 2, 3}, or any arbitrary class
set resultsRecordAsNSDictionary to current application's class "NSDictionary"'s dictionaryWithObject_forKey_(handlerResult, "theResult") --> creates an NSDictionary corresponding to the Applescript record {theResult:handlerResult}
set resultsRecordAsNSData to current application's class "NSKeyedArchiver"'s archivedDataWithRootObject_(resultsRecordAsNSDictionary)

However, I was unsuccessful in converting the NSData to an NSAppleEventDescriptor because I could not figure out what to use for the descriptor type (if it is even possible):

set resultsRecordAsNSAppleEventDescriptor to current application's class "NSAppleEventDescriptor"'s descriptorWithDescriptorType_data_([...descriptor type...], resultsRecordAsNSData)

I tried “reco” and current application’s AERecord for the descriptor type, but neither worked.

The four-letter codes are really unsigned longs – if you can do the conversion, you might be able to pass the number. (Eg, ‘type’ is 1954115685.)

Based on your suggestion, I tried setting the descriptor type to 1919247215 (“reco”):

set handlerResult to 1 -- or 2.2, or "three", or true, or {1, 2, 3}, or any arbitrary class
set resultsRecordAsNSDictionary to current application's class "NSDictionary"'s dictionaryWithObject_forKey_(handlerResult, "theResult") --> creates an NSDictionary corresponding to the Applescript record {theResult:handlerResult}
set resultsRecordAsNSData to current application's class "NSKeyedArchiver"'s archivedDataWithRootObject_(resultsRecordAsNSDictionary)
set resultsRecordAsNSAppleEventDescriptor to current application's class "NSAppleEventDescriptor"'s descriptorWithDescriptorType_data_(1919247215, resultsRecordAsNSData)

With this, an NSAppleEventDescriptor was indeed created based on logging resultsRecordAsNSAppleEventDescriptor’s class. Unfortunately, the desired result (the Applescript record {theResult:1}) was not returned by the application to the calling program. I also tried setting the descriptor type to 1868720672 ("obj "), and a raw data value was returned in the form «data obj 6270…» but still not the desired result.

You really have two choices with command scripting: specify one of the types Cocoa scripting automatically converts to/from Cocoa, or use type any and do the conversion yourself. The problem is that most of the conversions can’t really be done in ASObjC. In the case of records, you need to use C-based Carbon functions. NSKeyedArchiver isn’t going to get you anywhere, I’m afraid.

Thanks, Stefan and Shane, for the valuable suggestions and information about processing input and output for a scriptable application. I’ve progressed along the ASObjC learning curve thanks to these efforts.

May I ask about the original question of this post? Any thoughts about why the application freezes after a fixed number of times an “xeq” command is sent to it (the details of which are described at the end of the original post)?

I wish I knew. There does seem to be a problem when one creates a significant number of AS-based classes – I’ve seen problems when using collections of AS class objects for tables. I suspect it’s something to do with AppleScript limitations on how much stuff it can store, or a problem with its own garbage collection. But I’m only guessing, wildly.

Have you thought about a different approach: rather than creating a new NSScriptCommand subclass object each time, you can have a method in your app delegate that gets called. That way you’re not replicating AS class objects.

You describe in your excellent “AppleScriptObjC Explored” two ways of processing a command in a scriptable application: (1) in a newly instantiated NSScriptCommand object, as I’ve done in the current application, and (2) in the application itself. When you suggest calling a method in the app delegate, are you referring to the latter approach to processing a command? If so, can you kindly give me guidance on how I would modify the sdef file to direct the command to the application rather than to an NSScriptCommand object? Would it entail the use of an application_delegateHandlesKey_ handler?

That’s the approach I was suggesting, but I just noticed the docs for application_delegateHandlesKey_ say it can only be used for set/get commands – that probably rules it out for this case.

I’m afraid I’m out of ideas, short of moving the scriptability to Objective-C. Well, I have one suggestion: it sounds like you have a good example of repeatable freezes, so please logging the problem with bug reporter. I suspect the is problem lingers in part because it’s hard to produce a good repeatable example for the Apple engineers to work from.

I will definitely submit the reproducible freeze problem to Apple. Thanks again for your efforts with these probems.

I just had one of those duhhhhhhhhhhhhh moments this morning.

I had been running the application on OS X 10.8 Mountain Lion with Xcode’s ARC (Automatic Reference Counting) turned on and garbage collection turned off. When I reversed the settings (ARC off, GCC_ENABLE_OBJC_GC set to “required”), my freeze problem disappeared. So the problem was indeed one of memory management. It does however seem to point out a problem with Xcode’s Automatic Reference Counting, does it not?

Not exactly. Before 10.8, ASObjC and the scripting bridge did no memory management, leaving it to garbage collection at the application level. As of 10.8, they do their own (presumably manual) memory management, so in theory it doesn’t matter if the app is using GC or ARC. So it suggests more a memory management problem in ASObjC or the scripting bridge – one that’s probably being shown up by stricter ARC memory management.

I did submit this as a bug report to Apple, and modified the submission with this new information. I’ll report back if (hopefully when) I hear back from Apple.

Just to close the loop, Apple acknowledged the application freeze problem reported in my bug report, and the problem is now fixed in OS 10.9.4 Mavericks. The application no longer freezes with Automatic Reference Counting turned on.

Thanks for reporting back. And hopefully others reading will realise that reporting bugs is not a waste of time. I’ve been getting lucky lately, too :wink: