Monday, November 08, 2010 at 12:05 AM.
system.verbs.builtins.rootUpdates.update
on update (adrInRoot = table.getcursoraddress (), flQuietMode=false, adrCtNewParts=nil) {
<<Changes
<<9/17/10; 10:34:08 AM by DW
<<Call the afterInstallpart callbacks when doing RSS-based updates. This makes it possible for one of my servers to keep listings.opml.org current, automatically.
<<3/2/09; 11:35:37 AM by DW
<<After updating, call menus.buildMenuBar so the menu gets a full refresh after an update. This should eliminate the need to quit and restart the editor to see updates that are reflected in the menus.
<<7/26/08; 12:39:59 PM by DW
<<Subject to a pref, user.rootUpdates.prefs.flBackupParts, backup parts in a sub-folder of "ops" before it's updated.
<<4/23/08; 1:51:49 PM by DW
<<Per Thomas Creedon's suggestion, instead of setting lastupdate to be clock.now, we set it to be the max pubDate taken from the RSS feed. It's more accurate, and takes into account the possibility that new items are added to the feed while the update is taking place (made worse by the possibility that the clocks are out of synch).
<<http://tech.groups.yahoo.com/group/frontierkernel/message/3591
<<3/21/07; 11:00:25 AM by DW
<<Initialize adrCtNewParts^ to 0 for RSS-based updates.
<<3/7/07; 10:08:12 AM by DW
<<The rssUpdate script has to be local to this script because all updating code has to be updatable. The update process goes to great lengths to run a copy of this script, if we did the updating from another script it would have to also run a copy of that one, greatly complicating an already complicated process.
<<2/18/07; 8:08:31 AM by DW
<<Update for codecasting.
<<http://stories.scripting.com/2007/02/18/continuingWithCodecasting.html
<<6/21/05; 12:27:52 PM by DW
<<No longer default to userland.com for the server, instead look to opml.org, port 5337.
<<6/11/05; 8:39:59 AM by DW
<<Updated for OPML editor.
<<Don't open the About window.
<<1/12/04; 12:48:36 AM by JES
<<Changed default updates server from madrid.userland.com to updates.userland.com.
<<8/28/02; 10:48:23 PM by JES
<<Added support for two new callbacks:
<<user.rootUpdates.callbacks.beforeInstallPart -- scripts in this table are called with two parameters: the address of the part, and the change note. If any script returns false, then the part is not installed. No error is generated -- it's up to the callback script to provide feedback to the user.
<<user.rootUpdates.callbacks.afterInstallPart -- scripts in this table are called with two parameters: the address of the part, and the change note. The return value is ignored.
<<4/16/02; 4:34:53 PM by JES
<<Bug fix: Store the new part count in adrCtNewParts^ even if no new parts were loaded.
<<1/21/02; 2:25:42 PM by JES
<<Use a string comparison to determine whether we're getting updates from radioupdates.userland.com, instead of a call to tcp.equalNames, since tcp.equalNames fails for some proxy server users.
<<1/7/02; 9:09:40 PM by PBS
<<If getting Radio updates from radioupdates.userland.com, send your usernum as an additional parameter.
<<8/6/01; 2:22:37 PM by PBS
<<When updating Manila or mainResponder, send the serial number.
<<01/21/01; 6:03:11 PM by JES
<<Added optional parameter, adrCtNewParts. If not nil, the number of updated parts is assigned to adrCtNewParts^.
<<08/20/00; 11:49:35 AM by PBS
<<If the server returns a last update time, use that instead of clock.now (). This fixes problems with people missing updates due to having clocks out-of-synch with the server. This way, the server's time is always used.
<<06/17/00; 11:16:23 PM by PBS
<<Use new root updates timeout pref.
<<Tuesday, November 16, 1999 at 1:43:53 AM by AR
<<Make sure backup folder exists before backing up GDBs.
<<Added flQuietMode parameter, defaulting to false. If it's true, we suppress all server-unfriendly interaction like putting up dialogs or opening the outline of new parts, regardless of the settings in user.rootUpdates.
<<10/7/99; 6:43:29 PM by DW
<<New pref, user.rootupdates.prefs.flShowUpdatesToUser, defaults true, initialized in rootUpdates.init.
<<New pref, user.rootupdates.prefs.adrOutlineForUser, defaults to @user.rootUpdates.updateOutline.
<<if flShowUpdatesToUser is true, we create the outline if it doesn't exist and add a top-level headline with the date and time and the name of the database being updated. Each subordinate entry is the address of an object that's been updated. Underneath the address is the explanation of the change, if available.
on backupPart (adrpart) { //7/26/08 by DW
<<Changes
<<7/26/08; 12:13:45 PM by DW
<<Created.
try {
if user.rootUpdates.prefs.flBackupParts {
if defined (adrpart^) { //not new
local (folder = user.rootUpdates.prefs.backupFolder + file.getDatePath ());
local (parentname = nameof (parentof (adrpart^)^));
local (suffix = frontier.getfilesuffix (typeof (adrpart^), true), f, ct = 1);
loop {
f = folder + parentname + "." + nameof (adrpart^) + "." + ct++ + "." + suffix;
file.surefilepath (f);
if not file.exists (f) {
break}};
export.sendobject (adrpart, f)}}}};
on rssUpdate (url, whenLastCheck, adrCtNewParts) {
<<Changes
<<2/18/07; 8:01:15 AM by DW
<<Because the feed is now written in reverse-chronologic order, we must process the updates starting at the bottom of the feed and work our way up. To see why this is true, consider this case...
<<On Tuesday we update examples.counters.
<<On Wednesday we update examples.counters.test2.
<<If we were to process the second update before the first, we'd wipe out he change to test2 when we update its parent table. But that wasn't the intention of the author. We have to perform the changes in the order the author performed them or else we'll break the app.
<<2/17/07; 4:20:33 AM by DW
<<Created.
adrCtNewParts^ = 0; //3/21/07 by DW
local (xmltext = tcp.httpreadurl (url));
xml.compile (xmltext, @xstruct);
<<scratchpad.xstruct = xstruct
local (adrrss = xml.getaddress (@xstruct, "rss"));
local (adrchannel = xml.getaddress (adrrss, "channel"));
local (adritem, i);
for i = sizeof (adrchannel^) downto 1 {
adritem = @adrchannel^ [i];
if nameof (adritem^) contains "item" {
local (pubdate = date (xml.getvalue (adritem, "pubDate")));
local (title = xml.getvalue (adritem, "title"));
if pubdate > whenLastCheck {
local (adrenclosure = xml.getaddress (adritem, "enclosure"));
local (urlpart = xml.getattribute (adrenclosure, "url")^);
local (bits = tcp.httpreadurl (urlpart), atts);
bits = string.replaceAll (bits, cr + lf, cr);
bits = string.replaceAll (bits, lf, cr);
on partError () {
scriptError ("The part, \"" + title + "\", does not contain an embedded object.")};
if not fatPages.getPageAtts (@bits, @atts) {
partError ()};
if not defined (atts.pageData) {
partError ()};
local (x = atts.objectType, objectType);
local (prefix = "application/x-frontier-");
if not (x beginsWith prefix) {
partError ()};
objectType = string.delete (x, 1, sizeof (prefix));
local (data = binary (base64.decode (atts.pageData)));
setBinaryType (@data, objectType);
table.surePath (string (atts.adrPageData));
backupPart (address (atts.adrPageData)); //7/26/08 by DW
unpack (@data, atts.adrPageData);
bundle { //do the callbacks -- 9/17/10 by DW
try {
local (description = xml.getvalue (adritem, "description"));
rootUpdates.doCallback (atts.adrPageData, description, @user.rootUpdates.callbacks.afterInstallPart)}};
adrCtNewParts^++;
if pubdate > maxpubdate { //4/23/08 by DW
maxpubdate = pubdate}}}}};
bundle { //trial
if defined (userland.trialVersionCheck) {
if userland.trialVersionCheck () {
scriptError ("Can't update the database because the trial period has expired.")}}};
rootUpdates.init ();
local (adrroot = table.getrootaddress (adrInRoot));
local (alist = string.parseaddress (adrroot));
local (f = window.getFile (alist [1]));
local (fname = file.filefrompath (f));
if not string.lower (fname) endswith ".root" {
scripterror ("Can't update the file because its name doesn't end with .root.")};
local (channelname = string.mid (fname, 1, sizeof (fname) - 5));
local (adrtable = @user.rootUpdates.servers.[channelname]);
if not defined (adrtable^) {
new (tabletype, adrtable);
adrtable^.server = "updates.opml.org"; //6/21/05; 12:29:20 PM by DW
adrtable^.port = 5337;
adrtable^.method = "nirvanaServer.subscriptions.update";
adrtable^.dbname = fname;
adrtable^.lastupdate = "Fri, 16 Oct 1998 12:00:00 GMT"; //indicate that it's never been updated
adrtable^.autobackup = false; //if true the backup will be of docServer.root, not Frontier.root
adrtable^.dialogs = true; //set this false if you're updating automatically thru the scheduler
adrtable^.openLog = true; //show the log file of changes
adrtable^.URL = ""}
else {
if string.lower (adrtable^.server) == "madrid.userland.com" {
adrtable^.server = "updates.userland.com"}};
bundle { //if there's a rssUrl in the table, use that method of updating
if defined (adrtable^.rssUrl) {
try {
local (maxpubdate = date (adrtable^.lastupdate)); //4/23/08 by DW
rssUpdate (adrtable^.rssUrl, maxpubdate, adrCtNewParts);
try {menus.buildMenuBar ()}; //3/2/09 by DW
adrtable^.lastupdate = date.netstandardstring (maxpubdate)}
else {
adrtable^.lasterror = tryerror};
return}};
bundle { //the responder name on the server changed
if adrtable^.method == "mainResponder.subscriptions.update" {
adrtable^.method = "nirvanaServer.subscriptions.update"}};
on getUpdate (serverName, adrInRoot = nil) { //an enhanced version of rootupdates.getUpdate
<<10/18/98; 11:33:30 AM by DW
<<Made it guest database-aware.
<<If adrInRoot is specified, it's assumed to point into the guest db that's getting updated
<<Added a field to the prefs record, dbname, which names the guest database that's being shared
<<It's an index into user.mainresponder.databases on the server.
<<The handler for this is at mainresponder.rpchandlers.subscriptions.update.
local (frontierVersion = Frontier.version ());
local (platform = string.mid (sys.os (), 1, 3));
local (ctnewparts = 0);
local (flDialogs = true);
local (flLogOutlineOpened = false, flNewLogOutline = false);
local (adrPrefs = @user.rootUpdates.servers.[serverName]);
local (flguestdb = false, addressprefixstring); //10/18/98 DW
bundle { //gather info about guest database
if adrInRoot != nil and string.lower (channelname) != "frontier" {
addressprefixstring = string (table.getRootAddress (adrInRoot));
flguestdb = true}};
if defined (adrPrefs^.dialogs) and not adrPrefs^.dialogs {
flDialogs = false};
if flQuietMode { //AR 11/15/1999
flDialogs = false};
if not defined (user.prefs.serialNumber) {
user.prefs.serialNumber = ""};
try { //try to get the root update from the server
if flDialogs {
local (prompt = "Connect with " + adrPrefs^.server + " to get latest update");
if flguestdb {
prompt = prompt + " of " + adrprefs^.dbname};
prompt = prompt + "?";
if not dialog.confirm (prompt) {
return (false)}};
bundle { //open the About window, 6/11/05; 8:41:25 AM by DW
local (flopenabout = true);
if defined (system.environment.isOpmlEditor) {
if system.environment.isOpmlEditor {
flopenabout = false}};
if flopenabout {
window.about ()}};
local (sn);
<<if adrPrefs == @user.rootUpdates.servers.Frontier
<<Only send Frontier serial number when getting Frontier.root udpate.
<<Thu, Mar 18, 1999 at 3:21:10 PM by PBS
<<sn = user.prefs.serialNumber
local (rpcPath = user.betty.prefs.rpcClientDefaultPath);
case true { //PBS 08/06/01: send serial number for Frontier, Manila, and mainResponder
adrPrefs == @user.rootUpdates.servers.Radio;
adrPrefs == @user.rootUpdates.servers.radioCommunityServer;
adrPrefs == @user.rootUpdates.servers.serverMonitor;
adrPrefs == @user.rootUpdates.servers.Prefs;
adrPrefs == @user.rootUpdates.servers.Frontier;
adrPrefs == @user.rootUpdates.servers.Manila;
adrPrefs == @user.rootUpdates.servers.mainResponder {
sn = user.prefs.serialNumber;
if string.lower (adrprefs^.server) endswith ".userland.com" {
try { //add path args to rpcPath
local (args); new (tableType, @args);
args.org = user.prefs.organization;
args.name = user.prefs.name;
args.email = user.prefs.mailAddress;
args.sn = user.prefs.serialNumber;
if defined (config.mainResponder.domains) {
args.domains = sizeOf (config.mainResponder.domains)}
else {
args.domains = 0};
if defined (config.manila.sites) {
args.sites = sizeOf (config.manila.sites)}
else {
args.sites = 0};
if defined (radioCommunityServerData.users) {
args.rcsUsers = sizeOf (radioCommunityServerData.users)}
else {
args.rcsUsers = 0};
rpcPath = rpcPath + "?" + webserver.encodeArgs (@args)}}}}
else {
if defined (adrPrefs^.serialNum) {
sn = adrPrefs^.serialNum}
else {
sn = 0}};
local (paramlist = {adrPrefs^.lastupdate, platform, sn, user.prefs.name, user.prefs.mailAddress, frontierVersion});
bundle { //PBS 01/07/02: send usernum to radioupdates.userland.com
<<if tcp.equalNames (adrPrefs^.server, "radioupdates.userland.com")
<<paramlist = paramlist + user.radio.prefs.usernum
if string.lower (adrPrefs^.server) == "radioupdates.userland.com" {
paramlist = paramlist + user.radio.prefs.usernum}};
if flguestdb {
paramlist = {adrprefs^.dbname} + {tcp.addressDecode (tcp.myAddress ())} + paramlist};
local (newpartstable);
local (i);
msg ("Connecting to " + adrPrefs^.server + " to get " + serverName + " update...");
newpartstable = betty.rpc.client (adrPrefs^.server, adrPrefs^.port, adrPrefs^.method, @paramlist, false, rpcPath:rpcPath, ticksToTimeOut:user.rootUpdates.prefs.timeout);
<<bundle //original call
<<newpartstable = betty.rpc.client (adrPrefs^.server, adrPrefs^.port, adrPrefs^.method, @paramlist, false, ticksToTimeOut:user.rootUpdates.prefs.timeout)
if not defined (newPartsTable.parts) {
if newPartsTable beginsWith "There are no new or changed parts available." or newPartsTable beginsWith "Your root is already updated" {
msg ("");
if flDialogs {
dialog.notify (newPartsTable)}
else {
msg (newPartsTable)};
if adrCtNewParts != nil { //4/16/02; 4:33:24 PM by JES: make the new part count available to the caller, even if there are no new parts
adrCtNewParts^ = ctNewParts};
return (true)}};
if adrPrefs^.autobackup { //10/18/98; 11:36:58 AM by DW
if flguestdb {
backups.init (); //make sure user.backups.folder is set
local (alist = string.parseaddress (adrInRoot));
local (source = alist [1]);
filemenu.save (source);
local (dest);
bundle { //set dest to point at the destination file
dest = file.filefrompath (source);
if string.lower (dest) endswith ".root" {
dest = string.delete (dest, sizeof (dest) - 4, 5); //pop off .root
dest = dest + "." + user.backups.nextBackup + ".root"}
else {
dest = dest + "." + user.backups.nextBackup};
dest = user.backups.folder + dest;
user.backups.nextBackup++};
file.sureFilePath (dest); //AR 11/15/1999
file.copy (source, dest);
filemenu.save ();
msg (dest + " is " + string.megabytestring (file.size (dest)))}
else {
backups.backuproot ()}};
for i = 1 to sizeof (newpartstable.parts) {
local (adrnewpart = @newpartstable.parts [i]);
local (bytes = binary (base64.decode (adrnewpart^)));
local (flpartinstalled = true);
setBinaryType (@bytes, tableType);
unpack (@bytes, adrnewpart);
on installPart (adrpart) {
if defined (user.rootUpdates.protected) { //Don't import into these places.
local (j);
for j = 1 to sizeOf (user.rootUpdates.protected) {
local (oneItem = address (user.rootUpdates.protected [j]));
if table.tableContains (oneItem, adrpart^.adr) { //Don't install.
flpartinstalled = false;
return (true)}}};
if defined (adrpart^.platform) {
if string.lower (adrpart^.platform) != "both" { // Only install parts for this platform.
if string.lower (adrpart^.platform) != string.lower (platform) {
return (true)}}};
try {
if flguestdb { //we got a relative address, convert it to a real address
adrpart^.adr = address (addressprefixstring + "." + adrpart^.adr)};
msg ("Importing " + adrpart^.adr + ".");
local (adrcallback, ixcallback, ctcallbacks, flInstallPart = true);
bundle { //call beforeInstallPart callbacks
ctcallbacks = sizeOf (user.rootUpdates.callbacks.beforeInstallPart);
for ixcallback = 1 to ctcallbacks {
local (adrcallback = @user.rootUpdates.callbacks.beforeInstallPart[ixcallback] );
try {
while typeOf (adrcallback^) == addressType {
adrcallback = adrcallback^};
if not adrcallback^ (adrpart^.adr, string (adrpart^.changes) ) {
flInstallPart = false;
break}}}};
if flInstallPart {
adrpart^.adr^ = adrpart^.data};
bundle { //call afterInstallPart callbacks
ctcallbacks = sizeOf (user.rootUpdates.callbacks.afterInstallPart);
for ixcallback = 1 to ctcallbacks {
local (adrcallback = @user.rootUpdates.callbacks.afterInstallPart[ixcallback] );
try {
while typeOf (adrcallback^) == addressType {
adrcallback = adrcallback^};
adrcallback^ (adrpart^.adr, string (adrpart^.changes) )}}}}
else {
flpartinstalled = false;
if flDialogs {
if not dialog.yesNo ("Installation of \"" + adrpart^.adr + "\" failed. Continue installation?") {
return (false)}}};
local (logline);
bundle { //clean up address so you can 2click on it
logline = string (adrpart^.adr);
local (ix = string.patternmatch ("].", logline));
if ix != 0 {
logline = string.delete (logline, 1, ix + 1)}};
bundle { //log the update
on cleanNote (s) {
if s != "" {
s = string.replaceAll (s, "\r\n", "\r");
s = string.replace (s, ": ", ":\r");
s = string.replaceAll (s, ". ", ".\r");
s = string.replaceAll (s, "-- ", "--\r");
s = "Changed " + s;
s = string.replaceAll (s, "\r\r\r\r", "\r\r");
s = string.replaceAll (s, "\r\r\r", "\r\r");
s = string.popTrailing (s, "\r")};
return (s)};
local (changenote = cleanNote (string (adrpart^.changes)));
export.addToLog (logline, adrPrefs^.url, changenote);
if user.rootupdates.prefs.flShowUpdatesToUser {
if not flLogOutlineOpened {
if not defined (user.rootupdates.prefs.adrOutlineForUser^) {
new (outlinetype, user.rootupdates.prefs.adrOutlineForUser);
local (oldtarget = target.set (user.rootupdates.prefs.adrOutlineForUser));
op.setLineText (user.rootupdates.prefs.adrOutlineForUser + " created on " + clock.now ());
target.set (oldtarget);
flNewLogOutline = true};
local (oldtarget = target.set (user.rootupdates.prefs.adrOutlineForUser));
op.firstSummit ();
op.insert ("Updated " + fname + " on " + clock.now (), up);
target.set (oldtarget);
flLogOutlineOpened = true};
local (oldtarget = target.set (user.rootupdates.prefs.adrOutlineForUser));
op.insert (logline, right);
if sizeof (changenote) > 0 {
op.insert (changenote, right);
op.go (left, 1);
op.collapse ()};
op.go (left, 1);
target.set (oldtarget)}};
return (true)};
if not installPart (adrnewpart) {
break};
if flpartinstalled {
ctnewparts++}};
if defined (newPartsTable.lastUpdateTime) { //PBS 08/20/00: get last update time from server, if supplied
adrPrefs^.lastUpdate = newPartsTable.lastUpdateTime}
else { //PBS 08/20/00: fallback -- use clock.now () to set last update time
adrPrefs^.lastUpdate = date.netStandardString (clock.now ())};
adrPrefs^.serialNum = newPartsTable.serialNum;
if flguestdb {
msg ("Saving " + adrprefs^.dbname + "...");
fileMenu.saveMyRoot (adrInRoot);
msg ("")}
else {
menu.installMainMenu ();
system.menus.buildSuitesSubMenu (); //re-build in case any new suites were imported.
msg ("Saving Frontier.root...");
fileMenu.save ();
msg ("")};
if flLogOutlineOpened and not flQuietMode {
edit (user.rootupdates.prefs.adrOutlineForUser);
if flNewLogOutline {
window.zoom (user.rootupdates.prefs.adrOutlineForUser)}}
else {
thread.sleepFor (0);
if ctnewparts == 1 {
if flDialogs {
dialog.alert ("1 new part loaded from \"" + adrPrefs^.server + "\".")}
else {
msg ("1 new part loaded from \"" + adrPrefs^.server + "\".")}}
else {
if flDialogs {
dialog.alert (ctnewparts + " new parts loaded from \"" + adrPrefs^.server + "\".")}
else {
msg (ctnewparts + " new parts loaded from \"" + adrPrefs^.server + "\".")}}}}
else { //handle errors
on alert (message) {
if flDialogs {
return (dialog.alert (message))};
scriptError (message)};
local (lowerError = string.lower (tryerror));
local (errmsg = "Can't update the root because ");
case true {
lowerError contains "socket is not connected" {
alert (errmsg + "the server is not responding.")};
lowerError contains "can't get the address of";
lowerError contains "poorly formed xml text";
lowerError contains "missing tag" {
alert (errmsg + "the server timed out.")}}
else {
alert (errmsg + "\"" + tryError + ".\"")};
return (false)};
if adrCtNewParts != nil { //01/21/01 JES: make the new part count available to the caller
adrCtNewParts^ = ctNewParts};
return (true)};
getUpdate (channelname, adrroot)}
<<bundle //test code
<<local (ctnewparts)
<<update (this, false, @ctnewparts)
<<dialog.alert (ctnewparts + " new parts.")
This listing is for code that runs in the OPML Editor environment. I created these listings because I wanted the search engines to index it, so that when I want to look up something in my codebase I don't have to use the much slower search functionality in my object database. Dave Winer.