TOP | UP: Interface | NEXT: Pictures |
An agent is a script that Frontier calls automatically from time to time in the background. A hook is a script that Frontier calls automatically when a certain high-level event takes place. Together, they let your scripts respond to what's happening around them.
Agents run in a thread of their own; the agent thread number is maintained at system.compiler.threads.agents. The agent thread is present even if Frontier is otherwise idle. See Chapter 21, Yielding, Pausing, Threads, and Semaphores.
Agent scripts live at system.agents . The database, as shipped, already includes several of them, which you are free to examine. Agents have no eponymous handler; Frontier calls them without parameters.
An agent script is called by Frontier repeatedly, at intervals. The default interval is one second after the agent script finishes its previous execution; or the agent script itself may tell Frontier to wait longer before calling it again, by calling clock.sleepFor() , which takes as its parameter the number of seconds to wait before calling again.
The call to clock.sleepFor() does not have to occur in the agent script itself; but it must occur either in the agent script or in a script called by the agent script. For example, the script at system.agents.schedulerMonitor calls scheduler.monitor(), which calls clock.sleepFor(). The call to clock.sleepFor() need not come last in its script, and it does not immediately return control to Frontier.
To create an agent, you can just make a new script in system.agents, or choose New Script from the Main menu and set the dialog popup to Agent - this creates the script in system.agents for you and supplies a call to clock.sleepFor().
Testing an agent script requires some caution. You probably don't want Frontier to call the agent script while you're working on it. If you press the Compile button (or close the script edit window), Frontier will call the agent script one second later - so don't press it! You can, however, press the Run button to get implicit compilation; this lets you check that the script compiles and behaves as expected, but does not cause Frontier to call the script automatically as an agent. (See Chapter 13, Running and Debugging Scripts.)
You can't test an agent script by pressing its edit window's Run button if the script contains a call to clock.sleepFor(). That's because the script will run in a new thread, which is not the agent thread; and clock.sleepFor() is illegal outside the agent thread. The usual strategy is to comment out the clock.sleepFor() line, test the script with the Run button, and then, when you're done testing, uncomment the clock.sleepFor() line, and press the Compile button. Your agent is now live.
It's a good idea to supply a fairly large clock.sleepFor() value at first, so that there is time for you to stop the agent between calls to it if something goes wrong. I once wrote an agent into which I had put a call to dialog.notify() for debugging purposes. I then made the mistake of pressing the Compile button, and discovered that it was not easy to stop the agent; the dialog would appear, I would click its OK button, and the dialog would immediately appear again! I had a devil of a time getting things to pause. I would have had time to stop the agent if I had said clock.sleepFor(15) within the script.
How do you stop an agent? One way is to have the system.agents table ready to hand; selecting and cutting your agent script stops it from running. Another option is to select all the lines of your agent's script, turn them to a comment, and press the Compile button. Finally, choosing Disable Agents from the Main menu stops all agents; its name then changes to Enable Agents, and you can choose it again to start the agents up once more. This menu item relies on a call to frontier.enableAgents().
A question arises as to how agents are to share the message area of the Main Window. If five agents are running and they all include calls to msg(), is the Main Window going to be bombarded with a constant stream of messages, none of which will remain long enough to be legible? And what about regular scripts that call msg()?
The answer is as follows. At the left end of the Main Window is a popup menu. Choosing from this popup does two things. First, it actually calls the chosen agent, whether it was time to call it automatically or not. Second, it turns control of the Main Window's message area over to the chosen agent. Only one agent at a time can write to the Main Window with msg(), and it is the one most recently chosen with this popup.1
When a non-agent script calls msg() , agents stop writing to the message area of the Main Window. If a non-agent script calls msg("") with the empty string as its parameter, or the user clicks the top half of the Main Window, the Main Window goes back to displaying messages from the agent last chosen in the popup menu.
Choosing from the Main Window popup while holding the Option key opens the agent script's edit window - a handy shortcut.
Here are the agents supplied by UserLand, and what they do.
This is the agent whose messages I usually like to have showing; it tells the number of threads and the amount of Frontier's free memory. It also has another job, a very important one: it calls modes.monitor() , which is responsible for managing modal menus (see Chapter 26, Menus and Suites).
Watches to see whether Microsoft Internet Explorer or Netscape Navigator is active, so that one can drive the browser by calling suites.webBrowser verbs, regardless of which browser it is. It also reconciles the shared menus belonging the two browsers (see Chapter 26).
Installed by the scheduler suite to check once a minute whether any tasks are scheduled to be run; see Chapter 37, Scheduler.
Provides a harmless bit of trivia.
Isn't really an agent at all, in a sense. When you choose it in the popup menu, it displays the pathname of Frontier on your disk, and then calls clock.sleepFor(infinity), which is as good as saying it is never to be called automatically. This is a useful trick: you can list in the agents popup menu, just because it is a convenient location, a utility without agent-like functionality.
A hook is an internal Frontier mechanism that calls a script in response to a specific high-level event. Hooks are managed by associating particular areas in the database with particular events: scripts located in one of these areas are called when the corresponding events occur. Unless otherwise specified, such scripts take no parameters.
As a database is opened,2 all the scripts in its system.startup table are called. Startup scripts are triggered, each in its own thread, and before any agents are called.
If a suite has private values that must be initialized every time Frontier starts up, it will typically install a startup script, using its installSuite() to do so (see Chapter 26). For example, scheduler.installSuite() installs a startup script, system.startup.["Scheduler Startup"], which sets a global value, scheduler.startingUp, to true. This way, when the scheduler monitor agent is first called, it knows that it is being called for the first time, and can take certain actions accordingly. See Chapter 37.
As a database is closed,3 all the scripts in its system.shutdown table are called. This happens after the closeWindow hook has run, and after the "Save changes?" dialog has appeared if it needs to. It is impossible now to stop the database from closing, but it is possible to save it again (using fileMenu.save()); you'll need to do so if your shutdown script makes changes in the database.
If Frontier is frontmost, then when the user or a script brings another application to the front, all the scripts in the frontmost database's system.suspend table are called while Frontier is still frontmost.
A suspend script cannot prevent switching away from Frontier, but it can be used to bring Frontier back to the front again immediately. The following script, if placed in system.suspend, would effectively prevent the user or a script from being able to make any application frontmost other than Frontier:
while sys.frontmostapp() == "UserLand Frontier?"
clock.waitsixtieths(1)
sys.bringapptofront("UserLand Frontier?")
dialog.alert("Don't leave me!")
If Frontier is not frontmost, then when the user or a script brings Frontier to the front, all the scripts in the frontmost database's system.resume table are called once Frontier is frontmost.
For the closeWindow hook, see Chapter 24, Windows.
When the user double-clicks text while holding the Control key, the Option key, or the Command key, the script at system.misc.control2click , system.misc.option2click, or system.misc.cmd2click, respectively, is called with a single parameter, the double-clicked text. Since more than one modifier key can be down simultaneously, an order of precedence is defined: Control key, Command key, Option key (that is, if the Control key is down, system.misc.control2click() is called regardless of what other modifiers are down, and so on).
There are already scripts at system.misc.control2click and system.misc. cmd2click. The former switches to the double-clicked verb's DocServer documentation (or, if the Option key is also down, inserts the parameter information from DocServer into the script or outline where the verb was double-clicked). The latter jumps to the database object whose name was double-clicked (additionally closing the frontmost window if the Option key is also down). See Chapter 13, Running and Debugging Scripts.
A hook script may interrupt a script in progress; the script in progress cannot proceed until the hook script has finished executing. And you can't be exactly certain as to just when the interruption will take place.
For instance, if you have a script that says:
sys.bringapptofront("finder")
clock.waitseconds(5)
sys.bringapptofront("UserLand Frontier?")
and a hook script in system.suspend that says:
dialog.alert("Hold everything.")
then running the first script will switch away from Frontier but won't switch back; the first line has triggered the hook script, which is now stopped, waiting for a response to the dialog, and the first script has been interrupted, so it too is stopped.
Similarly, if you have a script that says:
sys.bringapptofront("finder")
clock.waitseconds(5)
sys.bringapptofront('LAND')
for x = 1 to 10
msg(x)
and a hook script in system.resume that says:
dialog.alert("Welcome back.")
then if you run the first script, it's hard to know how far through its counting x will actually get before the counting is interrupted by the dialog.
When a database is opened, a call is made to (if it exists) people.[user.ini-tials].customStartup(): it isn't a hook, properly speaking; it is triggered by the "startup" hook, though, because the startup hook calls the scripts in system.startup, and one of these is system.startup.startupScript(), which calls user.login(), which calls people.[user.initials].custom Startup().
When you choose Backup from the Main menu, then after the backup has been performed, a call is made to people.[user.initials].customBackup() if it exists. This isn't a hook at all; it's part of the functionality of the menu item script for Backup.
These are "pseudo-hooks" in the sense that they aren't the result of high-level events; they're just scripts called by other scripts in the usual way. They are worthy to be considered along with real hooks, though, because their purpose is similar: they allow custom automatic actions to be triggered. You could use the same technique to introduce your own pseudo-hooks.
Because these pseudo-hooks live in the people.[user.initials] table, they may be thought of as associated with a particular user rather than the database as a whole. Frontier presently has a simple system of differentiation between users - a call to user.switchUser() brings up a dialog to let the user enter a new user name, organization, and initials. A different people.[user.initials] table is then in force.
2. From the File menu, or with filemenu.open(); opening a database "invisibly" with db.open() does not trigger this hook. See Chapter 30, Multiple Databases.
TOP | UP: Interface | NEXT: Pictures |