"on idle" only gets called once -- FYI

Hi all,

This is an answer for a question that hasn’t been posted here yet.

I had found my ‘on idle’ handler was only getting called once, so I made a very simple script to test it:

on idle
     log "idle"
end idle

I had hooked up the File Owner’s ‘on idle’ routine to this script but for some reason it wasn’t getting called more than once.

It turns out the ‘on idle’ handler (or at least MY on idle handler) needs to return a value to be called again. It doesn’t matter if it returns true or false, just as long as it returns something, it will be called again.

on idle
     log idle
     return true
end idle

Weird, eh?

Just fyi,
Colin.

There’s actually a bit more to it than that.

If you return an integer, the integer is assigned as the idle interval for the next time the handler is called. So, a construct like this:

on idle

return 10
end

… means that the idle handler will be called every 10 seconds.
Of course, this is also true for vanilla scripts, outside of AStudio.

This is an extremely old post but I would like to pose a question…

I’m use a variable from a text field to set the return time. If I update that text form from 30 minutes to 30 seconds, I have to wait 30 minutes for it to re-read the variable unless I quit/restart the program.
The question is, how can I UPDATE the return varible so I do not have to wait?

I’ve seen this question posted many times and as far as I know no one has found a solution. It seems we have no choice but to wait for the idle handle to finish it’s current wait time. I’ve tried manually calling the idle handler with something like

property idleReturnTime : 30

on clicked theObject
            if name of theObject is  "name of something triggering a shorter idle interval" then
                    set idleReturnTime to 5
                    idle()
            end if
end clicked



on idle()

            doSomething()

            return idleReturnTime
end idle

Unfortunately it seems calling the idle handler manually, although will run through it that time and the doSomething will get called after having called the idle handler manually but it will break the idle handler and that will be the last time the idle handler will get called.

If anyone does find a solution for this I’d love to see it. Sucks having to wait for a long idle interval.

I do not know of a way to get the “driver” (script applet or AppleScript Studio application) to invoke an idle hander earlier than the previous return value indicated, but there is a workaround that might be acceptable in many situations.

The idea is to have the actual idle handler always run fairly often (like say, the minimum idle period you expect to need), but only do main work every once in a while.

So, if you normally want the idle handler to be invoked every 30 minutes, but sometimes you want to run it every 30 seconds for a little while, always run the idle handler every 30 seconds and just check each time if you actually want to do the main work right then, or wait until later and just increment a counter for now. When the handler goes off, if the counter is 60 or higher, do the main work. To start doing the main work every 30 seconds, just change the value that is being tested against from 60 to 1. Then when you are tired of having the code run every 30 seconds, change the test value back to 60 to go back to 30 minutes.

Here is some generic code that implements and demonstrates this idea:

to makeAdjustableIdler(_actualPeriod, _effectivePeriod, _idleWorker)
	(* Make a script object with the following characteristics:
		has a doIdle() handler that
			always returns _actualPeriod.
			invokes the doIdleWork(period) handler of _idleWorker when the current effective period has elapsed.
				The effective period is counted in terms of invocations of doIdle(). It is assumed that the invocations have _actualPeriod seconds of delay between them (as provided by a normal idle handler "loop").
			is therefore suitable as the only code in the body of a real idle handler.
		has an setEffectivePeriod(newPeriod) handler that
			changes the current effective period for the _idleWorker doIdleWork(period) invocations
				If changing to a smaller period that would already have elapsed if it was the effective period after the previous call to _idleWorker's doIdleWork(period) handler, then _idleWorker's doIdleWork(period) will instead be called the next time doIdle() is invoked.
	This means you can put your idle code in a doIdleWork(period) handler of a script object and use this handler to create a proxy-like script object that can be used to provide an asynchronously adjustable idle "loop" with a minimum response time of _actualPeriod.
	*)
	script newIdler
		global actualPeriodsDone, actualPeriodsPerEffectivePeriod
		
		on run
			set actualPeriodsDone to 0 -- Need some value here because setEffectivePeriod() accesses the variable for its return value.
			setEffectivePeriod(_effectivePeriod)
			set actualPeriodsDone to actualPeriodsPerEffectivePeriod -- Set to this instead of 0 so that the _idleWorker's handler is triggered once on the very first invocation (just like a normal idle handler).
		end run
		
		to getEffectivePeriod()
			return _actualPeriod * actualPeriodsPerEffectivePeriod
		end getEffectivePeriod
		
		to setEffectivePeriod(newPeriod)
			set actualPeriodsPerEffectivePeriod to newPeriod div _actualPeriod
			actualPeriodsDone
			return {actualPeriodsPerEffectivePeriod:actualPeriodsPerEffectivePeriod, actualPeriodsDone:actualPeriodsDone}
		end setEffectivePeriod
		
		to doIdle()
			try
				if actualPeriodsDone is greater than or equal to actualPeriodsPerEffectivePeriod then
					getEffectivePeriod()
					tell _idleWorker to doIdleWork(result)
					setEffectivePeriod(result)
					set actualPeriodsDone to 0
				end if
				set actualPeriodsDone to actualPeriodsDone + 1
			on error msg
				log "Error: " & msg
			end try
			return _actualPeriod
		end doIdle
	end script
	run newIdler -- Initialize the object
	newIdler
end makeAdjustableIdler

-- Demonstration code start here

script IdleWorker
	global timeOfLastEvent
	to doIdleWork(currentPeriod)
		set now to current date
		try
			set delta to now - timeOfLastEvent
			set err to delta - currentPeriod
		on error
			set delta to 0
			set err to 0
		end try
		log "Doing some idle work... (delta since start/previous = " & delta & "; overage = " & err & ")"
		set timeOfLastEvent to now
		return currentPeriod
	end doIdleWork
end script

global idler

on run
	-- Do the actual idle handler every 2 seconds, but only do the main work every 20 seconds (to start with, it is adjustable later on).
	set idler to makeAdjustableIdler(2, 20, IdleWorker)
	
	-- Demonstrate the adjustable period by simulating a normal idle "loop" and adjusting the idle period in the middle of the loop. Read/watch the event log to see what happens and at what times it happens.
	
	-- Get current date so it shows up in event log 
	current date
	-- Run the idle handler once, right away, just like the applet driver program would.
	idle
	delay result
	-- Then run a fixed number of iterations, just for demonstration purposes.
	repeat with i from 1 to 34
		if i is 15 then
			log "Setting period to 6"
			tell idler to setEffectivePeriod(6)
		end if
		if i is 23 then
			log "Setting period to 12"
			tell idler to setEffectivePeriod(12)
		end if
		idle
		delay result
	end repeat
	-- Get current date so it shows up in event log 
	current date
end run

on idle
	tell idler to doIdle()
end idle

The code is normal AppleScript, not AppleScript Studio, but hopefully you should be able to adapt it to other situations. That first handler (makeAdjustableIdler) could be used as-is. The IdleWorker script object is where you put the code for the main work you want to do when the idle handler goes off. To set it up, you would set idler to makeAdjustableIdler(30, 30*minutes, IdleWorker) and use the same idle handler as is at the bottom of the example code. Then just tell idler to setEffectivePeriod(30) when you want 30 second periods, and tell idler to setEffectivePeriod(30*minutes) when changing back to 30 minutes. When changing to a smaller period, the code in IdleWorker’s doIdleWork() handler will be invoked within 30 seconds (since that is the actual period, as specified in the first parameter in the call to makeAdjustableIdler(). There you go, no waiting for the remainder of the previous 30 minute period just to switch to a 30 second interval. It costs some extra CPU time (since the idle handler is being invoked much more often), but hopefully the ability to quickly change the effective idle handler period will be worth it.

Model: iBook G4 933
AppleScript: 1.10.7
Browser: Safari 419.3
Operating System: Mac OS X (10.4)

Hi Chrys,

Your script would work with pure AppleScript.

With AS Studio, it has to be reworked in. Big problem would be that the “on run/end run” handler does not work with AS Studio. I just read a thread on this forum a couple of days ago saying this and I also confirmed that the “on run/end run” doesn’t work.

I experimented a little bit on this “recalcitrant” idler by creating a button that would invoke the idler to change the return value. However, I could not make it to work either.

I haven’t tried this yet but perhaps a countdown type idling might. Here, one would start a countdown from the current date (date and time) and would stop, say 30 minutes after invoking the idler. In other words, the idler stopper is an integer value from current date to the stop date. Then give the idler a return value of 1 corresponding to 1 second of the internal clock.

If one creates a button that allows supplying a new stop date-time to the idler and then tell the idler to stop when the current time is greater than that new time, might that work?

I might try this out sometime but anyone’s welcome to try it out. Just a thought.

archseed :slight_smile:

Hi,

Well, what dya know?

The countdown method does work. At least it stopped the idler using the new value provided to it. So, one could actually stop the idler if necessary.

Here are the codes that I used to test this.


property countdown : false
property originalStop : missing value
property newStop : missing value
property oldMinutes : 10 --original time in minutes to stop the idler
property newMinutes : 1 --new time in minutes to stop the idler in the middle of idling the oldMinutes

global currDate

on awake from nib theObject
	tell me to activate
	set currDate to current date
	--get the date-time
	set originalStop to (currDate + (oldMinutes * 60))
	--display dialog "This is the original stop time: " & originalStop >>if you just want to check the date
end awake from nib

on clicked theObject
	set objectname to name of theObject
	
	if objectname is "startIdling" then tell window "main" to set countdown to true
	
	--if button to change the idling time is pressed
	if objectname is "stopIdling" then
		tell window "main"
			set currDate to current date
			set newStop to (currDate + (newMinutes * 60))
			--display dialog "This is the new stop time: " & newStop >>if you want to check the date
		end tell
	end if
end clicked

on idle theObject
	if countdown is true then
		set currDate to current date
		set content of text field "timefield" of window "main" to currDate
		if currDate is greater than originalStop and newStop is missing value then
			beep
			display dialog "The countdown using the variable oldMinutes is finished!"
			set countdown to false
		else if newStop is not missing value then
			if currDate is greater than newStop then
				beep
				display dialog "The countdown using variable newMinutes is done!"
				set countdown to false
			end if
		end if
	end if
	return 1
end idle


In AS Studio, create a simple window with 2 buttons, one to start the countdown (“startIdling”) and one to change the countdown value (“stopIdling”). I used “Start” and “Stop” for button titles in the main window. A third object in the window is a text field (“timefield”) formatted to contain the current date (given in “hour:minutes:seconds” format). Link the objects to the script and then begin the test by pressing “Start”.

The idler will use the oldMinutes value of 10 minutes. Once the time changes in the text field, press “Stop” to change the value to the newMinute (set in the property section to 1). Then wait till the idler stops and tell you which setting was used.

Good luck!

archseed :o

Well, actually, the large on run handler was just a simulation of the normal idle handler processing. It would not be used in a real situation, so it is irrelevant any adaptation to AppleScript Studio. Unless you mean the on run handler in the script object. Surely that still works, since it is only invoked manually anyway (as such, it doesn’t even really have to be on run it could have been to doMyInitialization()).

Anyway, your approach and mine are similar in nature (actually idling at a higher frequency than ordinarily necessary to adapt requested changes in frequency). The biggest difference is that yours checks real time (well, clock time), where mine checks only the number of iterations (sort of like run time). I am not sure which one is more faithful to a system-driven on idle handler that requests a long delay interval.

My big question would be: what happens when a computer goes to sleep during an idle interval? Say an idle handler returns 300 (5 minutes). The computer is then put to sleep (or goes to sleep normally). The computer is left asleep for 5 or more minutes and then awakened. Does the idle handler go off right away (because 5 minutes have passed in the real world), or does it wait nearly 5 more minutes (because 5 minutes of system run time have not yet elapsed)? My solution would wait for about 5 more minutes (since it is based on a number of intervals). Your solution would go off right away (since it is based on a specific date). The difference is akin to periodic events versus scheduled events. Probably for most applications this is not important, but it is a consideration to keep in mind when employing this technique.

property idleDate : null

on idle theObject
	if (idleDate is not null) then
		if (idleDate < (current date)) then
			set idleDate to null
			log "Fire"
			--> Do something
		else
			log (("Will fire in: " & (idleDate - (current date)) & " seconds") as string)
		end if
	end if
	return 1
end idle

on clicked theObject
	if name of theObject is "setIdleTime" then
		set idleInterval to (content of text field "idleTime" of (window of theObject)) as integer -- Get the number of seconds
		set idleDate to dateForInterval(idleInterval)
	else if name of theObject is "cancelIdleTime" then
		set idleDate to null
	end if
end clicked

on dateForInterval(tmpSeconds)
	return ((current date) + tmpSeconds) as date
end dateForInterval