chiark / gitweb /
style: add a comment about main() not being called
[jarrg-ian.git] / src / net / chiark / yarrg / MarketUploader.java
index 4e04ee42eadad0a49fb9e7f0cd8715499c5c1ae5..980d5de1b527f0b2968e54196ce8292e98d060d6 100644 (file)
@@ -21,34 +21,42 @@ import java.util.regex.*;
 import java.util.prefs.Preferences;
 import java.beans.*;
 
-/**
-     MarketUploader is a class that handles the uploading of market
-     data from Yohoho! Puzzle Pirates via the Java Accessibility
-     API.
-*
-     MarketUploader initializes after the main YPP window has
-     initialized. It provides a simple window with a "Capture
-     Market Data" button displayed.  Upon clicking this button, a
-     progress dialog is displayed, and the data is processed and
-     submitted to the YARRG and PCTB servers. If any errors occur,
-     an error dialog is shown, and processing returns, the button
-     becoming re-enabled.
-*/
+/*
+ *     MarketUploader is a class that handles the uploading of market
+ *     data from Yohoho! Puzzle Pirates via the Java Accessibility
+ *     API.
+ *
+ *     MarketUploader initializes after the main YPP window has
+ *     initialized. It provides a simple window with a "Capture
+ *     Market Data" button displayed.  Upon clicking this button, a
+ *     progress dialog is displayed, and the data is processed and
+ *     submitted to the YARRG and PCTB servers. If any errors occur,
+ *     an error dialog is shown, and processing returns, the button
+ *     becoming re-enabled.
+ */
 public class MarketUploader
 implements Runnable, TopLevelWindowListener, GUIInitializedListener {
+  // UI object references which are set during startup
   private JFrame frame = null;
   private Window window = null;
+
+  // Genuinely global variables
+  private PrintStream dtxt = null;
+  public int uploadcounter = 0;
+
+  // UI objects which are enabled/disabled, cleared/set, created/destroyed,
+  //  etc., for each upload
   private JButton findMarket = null;
   private JLabel resultSummary = null;
   private JLabel arbitrageResult = null;
-  private int unknownPCTBcommods = 0;
-  private long startTime = 0;
+  private ProgressMonitor progmon = null;
 
+  // PCTB protocol parameters
   private final static String PCTB_LIVE_HOST_URL = "http://pctb.crabdance.com/";
   private final static String PCTB_TEST_HOST_URL = "http://pctb.ilk.org/";
   private String PCTB_HOST_URL;
 
-  // Yarrg protocol parameters
+  // YARRG protocol parameters
   private final static String YARRG_CLIENTNAME = "jpctb greenend";
   private final static String YARRG_CLIENTVERSION =
            net.chiark.yarrg.Version.version;
@@ -59,38 +67,36 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     "http://upload.yarrg.chiark.net/test/commod-update-receiver";
   private String YARRG_URL;
 
+  // Preferences
   private boolean uploadToYarrg;
   private boolean uploadToPCTB;
   private boolean showArbitrage;
 
+  // Values cleared/set for each upload, or used during upload processing
+  private long startTime = 0;
+
   private String islandName = null;
   private String oceanName = null;
   private java.util.concurrent.CountDownLatch latch = null;
 
   private AccessibleContext sidePanel;
+
+  // PCTB-specific variables
+  private int unknownPCTBcommods = 0;
   private HashMap<String,Integer> commodMap;
-  public PrintStream dtxt = null;
 
-  private PropertyChangeListener changeListener = new PropertyChangeListener() {
-      public void propertyChange(PropertyChangeEvent e) {
-       if(e.getNewValue() != null && 
-          e.getPropertyName().equals
-          (AccessibleContext.ACCESSIBLE_CHILD_PROPERTY)) {
-         Accessible islandInfo = 
-           descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0,0,1,2});;
-         String text = 
-           islandInfo.getAccessibleContext().getAccessibleText()
-           .getAtIndex(AccessibleText.SENTENCE,0);
-         int index = text.indexOf(":");
-         String name = text.substring(0,index);
-         islandName = name;
-         // if (dtxt!=null) dtxt.println(islandName);
-         sidePanel.removePropertyChangeListener(this);
-         latch.countDown();
-       }
-      }
-    };
 
+  /*****************************************
+   * UPLOAD-TARGET-INDEPENDENT CODE        *
+   *****************************************/
+
+
+  /*
+   * UTILITY METHODS AND SUBCLASSES
+   *
+   * Useable on any thread.
+   *
+   */
   private int parseQty(String str) {
     if (str.equals(">1000")) {
       return 1001;
@@ -99,10 +105,11 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     }
   }
 
-  private void progresslog(String s) {
+  private void debuglog(String s) {
     if (dtxt == null) return;
     long now = new Date().getTime();
-    dtxt.println("progress "+(now - startTime)+"ms "+s);
+    dtxt.println("progress "+(now - startTime)+"ms "
+                +Thread.currentThread().getName()+": "+s);
   }
 
   private void debug_write_stringdata(String what, String data)
@@ -121,156 +128,18 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     strm.close();
   }
 
-  private void progressNote(ProgressMonitor pm, String s) {
-    String arb = null;
-    if (arbitrageResult != null)
-      arb = arbitrageResult.getText();
-    if (arb != null && arb.length() != 0)
-      s = "<html>" + arb + "<br>" + s;
-    pm.setNote(s);
-  }
        
-  /**
-   *   An abstract market offer, entailing a commodity being bought or sold by
-   *   a shoppe, for a certain price in a certain quantity. Not instantiable.
+  /*
+   * ENTRY POINT AND STARTUP
    *
-   *   @see Buy
-   *   @see Sell
-   */
-  abstract class Offer {
-    public int commodity, price, quantity, shoppe;
-    /**
-     * Create an offer from <code>record</code>, determining the shoppe Id from
-     * <code>stallMap</code> and the commodity Id from <code>commodMap</code>.
-     * <code>priceIndex</code> should be the index of the price in the record
-     * (the quantity will be <code>priceIndex + 1</code>).
-     *
-     * @param record the record with data to create the offer from
-     * @param stallMap a map containing the ids of the various stalls
-     * @param commodMap a map containing the ids of the various commodities
-     * @param priceIndex the index of the price in the record
-     */
-    public Offer(ArrayList<String> record, 
-                LinkedHashMap<String,Integer> stallMap, 
-                HashMap<String,Integer> commodMap,
-                int priceIndex) {
-      Integer commodId = commodMap.get(record.get(0));
-      if(commodId == null) {
-       throw new IllegalArgumentException();
-      }
-      commodity = commodId.intValue();
-      price = Integer.parseInt(record.get(priceIndex));
-      String qty = record.get(priceIndex+1);
-      quantity = parseQty(qty);
-      shoppe = stallMap.get(record.get(1)).intValue();
-    }
-               
-    /**
-     * Returns a human-readable version of this offer, useful for debugging
-     * 
-     * @return human-readable offer
-     */
-    public String toString() {
-      return "[C:" + commodity + ",$" + price + ",Q:"
-       + quantity + ",S:" + shoppe + "]";
-    }
-  }
-       
-  /**
-   *   An offer from a shoppe or stall to buy a certain quantity of a
-   *   commodity for a certain price. If placed in an ordered Set,
-   *   sorts by commodity index ascending, then by buy price
-   *   descending, and finally by stall id ascending.
+   * Main thread and/or event thread
    */
-  class Buy extends Offer implements Comparable<Buy> {
-    /**
-     * Creates a new <code>Buy</code> offer from the given
-     * <code>record</code> using the other parameters to determine
-     * stall id and commodity id of the offer.
-     *
-     * @param record the record with data to create the offer from
-     * @param stallMap a map containing the ids of the various stalls
-     * @param commodMap a map containing the ids of the various commodities
-     */
-    public Buy(ArrayList<String> record,
-              LinkedHashMap<String,Integer> stallMap,
-              HashMap<String,Integer> commodMap) {
-      super(record,stallMap,commodMap,2);
-    }
-               
-    /**
-     * Sorts by commodity index ascending, then price descending,
-     * then stall id ascending.
-     */
-    public int compareTo(Buy buy) {
-      // organize by: commodity index, price, stall index
-      if(commodity == buy.commodity) {
-       // organize by price, then by stall index
-       if(price == buy.price) {
-         // organize by stall index
-         return shoppe>buy.shoppe ? 1 : -1;
-       } else if(price > buy.price) {
-         return -1;
-       } else {
-         return 1;
-       }
-      } else if(commodity > buy.commodity) {
-       return 1;
-      } else {
-       return -1;
-      }
-    }
-  }
-       
-  /**
-   *   An offer from a shoppe or stall to sell a certain quantity of
-   *   a commodity for a certain price. If placed in an ordered Set,
-   *   sorts by commodity index ascending, then by sell price
-   *   ascending, and finally by stall id ascending.
-   */
-  class Sell extends Offer implements Comparable<Sell> {
-    /**
-     * Creates a new <code>Sell</code> offer from the given
-     * <code>record</code> using the other parameters to determine
-     * stall id and commodity id of the offer.
-     *
-     * @param record the record with data to create the offer from
-     * @param stallMap a map containing the ids of the various stalls
-     * @param commodMap a map containing the ids of the various commodities
-     */
-    public Sell(ArrayList<String> record,
-               LinkedHashMap<String,Integer> stallMap,
-               HashMap<String,Integer> commodMap) {
-      super(record,stallMap,commodMap,4);
-    }
-               
-    /**
-     * Sorts by commodity index ascending, then price ascending, then
-     * stall id ascending.
-     */
-    public int compareTo(Sell sell) {
-      // organize by: commodity index, price, stall index
-      if(commodity == sell.commodity) {
-       // organize by price, then by stall index
-       if(price == sell.price) {
-         // organize by stall index
-         return shoppe>sell.shoppe ? 1 : -1;
-       } else if(price > sell.price) {
-         return 1;
-       } else {
-         return -1;
-       }
-      } else if(commodity > sell.commodity) {
-       return 1;
-      } else {
-       return -1;
-      }
-    }
+
+  public static void main(String[] args) {
+    // This is not normally called, it seems.
+    new MarketUploader();
   }
-       
-  /**
-   *   Entry point.  Read our preferences.
-   */
+
   public MarketUploader() {
     Preferences prefs = Preferences.userNodeForPackage(getClass());
 
@@ -294,22 +163,21 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     uploadToPCTB=prefs.getBoolean("uploadToPCTB", true);
     showArbitrage=prefs.getBoolean("showArbitrage", true);
 
-    if (dtxt!=null) dtxt.println("main on dispatch thread: "+
-                                EventQueue.isDispatchThread());
+    debuglog("main on dispatch thread: "+EventQueue.isDispatchThread());
     EventQueue.invokeLater(this);
   }
 
   /*
    * We arrange to wait for the GUI to be initialised, then look at
-   * every top-level window, and if it
+   * every top-level window to see if the Puzzle Pirates window turns up.
    */
   public void run() {
-    if (dtxt!=null) dtxt.println("MarketUploader run()...");
+    debuglog("MarketUploader run()...");
     if (EventQueueMonitor.isGUIInitialized()) {
-      if (dtxt!=null) dtxt.println("MarketUploader GUI already ready");
+      debuglog("MarketUploader GUI already ready");
       guiInitialized();
     } else {
-      if (dtxt!=null) dtxt.println("MarketUploader waiting for GUI");
+      debuglog("MarketUploader waiting for GUI");
       EventQueueMonitor.addGUIInitializedListener(this);
     }
   }
@@ -318,13 +186,13 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     Window ws[]= EventQueueMonitor.getTopLevelWindows();
     EventQueueMonitor.addTopLevelWindowListener(this);
     for (int i=0; i<ws.length; i++) {
-      if (dtxt!=null) dtxt.println("MarketUploader existing toplevel "+i);
+      debuglog("MarketUploader existing toplevel "+i);
       topLevelWindowCreated(ws[i]);
     }
   }
 
   public void topLevelWindowDestroyed(Window w) {
-    if (dtxt!=null) dtxt.println("MarketUploader destroyed toplevel");
+    debuglog("MarketUploader destroyed toplevel");
   }
        
   public void topLevelWindowCreated(Window w) {
@@ -332,21 +200,19 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
       // already got it
       return;
     String name = w.getAccessibleContext().getAccessibleName();
-    if (dtxt!=null) dtxt.println("MarketUploader new toplevel "+name);
+    debuglog("MarketUploader checking toplevel "+name);
     if (!name.equals("Puzzle Pirates"))
+      // Only if we're running alongside a Window named "Puzzle Pirates"
       return;
-    if (dtxt!=null) dtxt.println("MarketUploader found toplevel, creating gui");
+    debuglog("MarketUploader found toplevel, creating gui");
     window = w;
     createGUI();
     frame.setVisible(true);
   }
        
-  /**
-   *   Set up the GUI, with its window and one-button
-   *   interface. Only initialize if we're running alongside
-   *   a Window named "Puzzle Pirates" though.
-   */
   private void createGUI() {
+    // Actually set up our GUI
+    on_ui_thread();
     frame = new JFrame("Jarrg Uploader");
     frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
     GridLayout layout = new GridLayout(2,1);
@@ -356,26 +222,38 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     findMarket = new JButton("Upload Market Data");
     findMarket.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
+         // This is called when the user clicks "upload"
+         on_ui_thread();
+
+         uploadcounter++;
          findMarket.setEnabled(false);
-         new Thread() {
+         resultSummary.setText("");
+         arbitrageResult.setText("");
+         new Thread("MarketUploader-uploader-"+uploadcounter) {
            public void run() {
              startTime = new Date().getTime();
-             resultSummary.setText("");
-             arbitrageResult.setText("");
              unknownPCTBcommods = 0;
              try {
-               runUpload();
+               runUpload(uploadcounter);
              } catch(Exception e) {
                error(e.toString());
                e.printStackTrace();
-               resultSummary.setText("failed");
-             } finally {
-               if(sidePanel != null) {
-                 // remove it if it's still attached
-                 sidePanel.removePropertyChangeListener(changeListener);
-               }
              }
-             findMarket.setEnabled(true);
+             try {
+               new UIX() { public void body() { 
+                 if(sidePanel != null) {
+                   sidePanel.removePropertyChangeListener(changeListener);
+                 }
+                 if (progmon != null) {
+                   progmon.close();
+                   progmon = null;
+                 }
+                 findMarket.setEnabled(true);
+               }}.exec("tidying");
+             } catch (Exception e) {
+               System.err.println("exception tidying on UI thread:");
+               e.printStackTrace();
+             }
            }
          }.start();
        }
@@ -394,79 +272,111 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
 
     frame.pack();
   }
-       
-  /**
-   *   Finds the island name from the /who tab, sets global islandName variable
-   */
-  private void getIsland() {
 
-    // If the league tracker is there, we can skip the faff
-    // and ask for its tooltip, since we're on a boat
+  /*
+   * THREAD HANDLING
+   *
+   * Special measures are needed because:
+   *  - it is not permitted to use any Swing UI classes or objects
+   *    other than on the Swing event thread
+   *  - we want to run our upload asynchronously
+   *  - we want to do some computation asynchronously (eg, the
+   *    arbitrage and upload data prep)
+   *
+   * So what we do is this:
+   *  1. When the user asks to upload, we spawn a new thread
+   *     to do the upload ("MarketUploader-uploader-*", see
+   *     the call to "new Thread" inside createGUI.
+   *  2. Whenever that thread needs to touch a UI object it
+   *     uses EventQueue.invokeLater or .invokeAndWait to
+   *     perform the relevant action.  We wrap these calls up
+   *     in three utility classes:
+   *        UIA  - runs code on UI thread, asynchronously
+   *        UIX  - runs code on UI thread, waits for it to finish
+   *        UIXR - as UIX but also returns a value
+   *     These hide the details of the EventQueue class and also do
+   *     some debugging and argument shuffling; the calling syntax is
+   *     still painful, unfortunately, and there is a weird constraint
+   *     on variables used inside the inner body.  For a simple
+   *     example, see the handling of "summary" and "summary_final"
+   *     for the call to UIX at the bottom of runUpload.
+   *  3. Try to put everything back when that thread exits.
+   *
+   * Additionally:
+   *  a. There is another thread spawed early to get a timestamp from
+   *     YARRG, if we are uploading there.
+   *  b. Finding the island name can involve callbacks which run in
+   *     the UI event thread.  Basically we do the work there, and use
+   *     a CountDownLatch to cause the uploader thread to wait as
+   *     appropriate.
+   */
 
-    Accessible leagueTrackerContainer =
-      descendNodes(window,new int[] {0,1,0,0,2,1});
-    Accessible leagueTrackerItself =
-      descendByClass(leagueTrackerContainer,
-                    "com.threerings.yohoho.sea.client.LeagueTracker");
-    Accessible leagueTracker = descend(leagueTrackerItself, 1);
-    try {
-      islandName = ((JLabel)leagueTracker).getToolTipText();
-    } catch (NullPointerException e) {
-      // evidently we're actually on an island
+  private void on_ui_thread() { assert(EventQueue.isDispatchThread()); }
+  private void on_our_thread() { assert(!EventQueue.isDispatchThread()); }
 
-      islandName = null;
-      AccessibleContext chatArea =
-       descendNodes(window,new int[] {0,1,0,0,0,2,0,0,2})
-       .getAccessibleContext();
-      // attach the property change listener to the outer sunshine
-      // panel if the "ahoy" tab is not active, otherwise attach it to
-      // the scroll panel in the "ahoy" tab.
-      if(!"com.threerings.piracy.client.AttentionListPanel".
-        equals(descendNodes(window,new int[] {0,1,0,0,2,2,0})
-               .getClass().getCanonicalName())) {
-       sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2})
-         .getAccessibleContext();
+  private abstract class UIA implements Runnable {
+    private String what;
+    public abstract void body();
+    public void run() {
+      debuglog("UIA 2 "+what+" begin");
+      body();
+      debuglog("UIA 3 "+what+" done");
+    }
+    public void exec(String what_in) {
+      what = what_in;
+      debuglog("UIA 1 "+what+" request");
+      EventQueue.invokeLater(this);
+    }
+  };
+  private abstract class UIXR<ReturnType> implements Runnable {
+    public abstract ReturnType bodyr();
+    public ReturnType return_value;
+    private String what;
+    public void run() { 
+      debuglog("UIX 2 "+what+" begin");
+      return_value = bodyr();
+      debuglog("UIX 3 "+what+" done");
+    }
+    public ReturnType exec(String what_in) throws Exception {
+      what = what_in;
+      if (EventQueue.isDispatchThread()) {
+       debuglog("UIX 1 "+what+" (event thread) entry");
+       this.run();
+       debuglog("UIX 4 "+what+" (event thread) exit");
       } else {
-       sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0})
-         .getAccessibleContext();
-      }
-      sidePanel.addPropertyChangeListener(changeListener);
-      latch = new java.util.concurrent.CountDownLatch(1);
-      // make the Players Online ("/who") panel appear
-      AccessibleEditableText chat = chatArea.getAccessibleEditableText();
-      chat.setTextContents("/w");
-      int c = chatArea.getAccessibleAction().getAccessibleActionCount();
-      for(int i=0;i<c;i++) {
-       if("notify-field-accept".equals(chatArea.getAccessibleAction()
-                                       .getAccessibleActionDescription(i))) {
-         chatArea.getAccessibleAction().doAccessibleAction(i);
-       }
+       debuglog("UIX 1 "+what+" (other thread) entry");
+       EventQueue.invokeAndWait(this);
+       debuglog("UIX 4 "+what+" (other thread) exit");
       }
+      return return_value;
     }
-  }
-
-  /**
-   *      Find the ocean name from the window title, and set global
-   *      oceanName variable
-   */
-  private void getOcean() {
-    oceanName = null;
-    AccessibleContext topwindow = window.getAccessibleContext();
-    oceanName = topwindow.getAccessibleName()
-      .replaceAll(".*on the (\\w+) ocean", "$1");
-  }
-
+  };
+  private abstract class UIX extends UIXR<Object> implements Runnable {
+    public abstract void body();
+    public Object bodyr() { body(); return null; }
+  };
 
-  /**
-   *   Shows a dialog with the error <code>msg</code>.
+  /*
+   * ERROR REPORTING AND GENERAL UTILITIES
    *
-   *   @param msg a String describing the error that occured.
-   */
-  private void error(String msg) {
-    JOptionPane.showMessageDialog(frame,msg,"Error",JOptionPane.ERROR_MESSAGE);
+   * Synchronous modal dialogues
+   * error and error_html may be called from any thread
+   */ 
+
+  public void error(final String msg) {
+    try {
+      new UIX() { public void body() {
+       resultSummary.setText("failed");
+       JOptionPane.showMessageDialog(frame,msg,"Error",
+                                     JOptionPane.ERROR_MESSAGE);
+      }}.exec("error()");
+    } catch (Exception e) {
+      System.err.println("exception reporting to UI thread:");
+      e.printStackTrace();
+    }
   }
        
-  private void error_html(String msg, String html) {
+  public void error_html(final String msg, String html) {
     Pattern body = Pattern.compile("<body>(.*)</body>",
                                   Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
     Matcher m = body.matcher(html);
@@ -480,80 +390,99 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     }
     String whole_msg = "<html><h1>Error</h1>"+msg
       +"<h1>PCTB Server said:</h1><blockquote>"+html+"</blockquote>";
-    if (dtxt!=null) dtxt.println("###" + whole_msg + "###");
+    debuglog("###" + whole_msg + "###");
+  
+    error(whole_msg);
+  }
 
-    JOptionPane.showMessageDialog(frame,whole_msg,"Error",
-                                 JOptionPane.ERROR_MESSAGE);
+  public void progressNote(final String s_in) throws Exception {
+    new UIA() { public void body() {
+      String arb = null;
+      arb = arbitrageResult.getText();
+      String s = s_in;
+      if (arb != null && arb.length() != 0)
+       s = "<html>" + arb + "<br>" + s;
+      progmon.setNote(s);
+    }}.exec("progressNote "+s_in);
   }
-       
-  /**
-   *   Run the data collection process, and upload the results. This
-   *   is the method that calls most of the other worker methods for
-   *   the process. If an error occurs, the method will call the
-   *   error method and return early, freeing up the button to be
-   *   clicked again.
-   *
-   *   @exception Exception if an error we didn't expect occured
+  public void setProgress(final int nv) throws Exception {
+    new UIA() { public void body() {
+      progmon.setProgress(nv);
+    }}.exec("setProgress "+nv);
+  }
+  public boolean checkCancelled() throws Exception {
+    return new UIXR<Boolean>() { public Boolean bodyr() {
+      boolean can = progmon.isCanceled();
+      if (can) resultSummary.setText("cancelled");
+      return new Boolean(can);
+    }}.exec("checkCancelled").booleanValue();
+  }
+
+
+  /*
+   * ACTUAL DATA COLLECTION AND UPLOAD
    */
-  private class YarrgTimestampFetcher extends Thread {
-    public String ts = null;
-    public void run() {
-      try {
-       ts = getYarrgTimestamp();
-       progresslog("(async) yarrg timestamp ready.");
-      } catch(Exception e) {
-       error("Error getting YARRG timestamp: "+e);
-      }
-    }
-  };
 
-  private void runUpload() throws Exception {
-    progresslog("starting");
+  private void runUpload(int counter) throws Exception {
+    // Runs the data collection process, and upload the results.
+    // In most cases of error, we call error() (which synchronously
+    // reports the error) and then simply return.
+
+    on_our_thread();
 
-    ProgressMonitor pm = new ProgressMonitor
-      (frame,"Processing Market Data","Getting table data",0,100);
-    pm.setMillisToDecideToPopup(0);
-    pm.setMillisToPopup(0);
     boolean doneyarrg = false, donepctb = false;
     YarrgTimestampFetcher yarrgts_thread = null;
 
+    debuglog("starting");
+
     if (uploadToYarrg) {
-      progresslog("(async) yarrg timestamp...");
-      yarrgts_thread = new YarrgTimestampFetcher();
+      debuglog("(async) yarrg timestamp...");
+      yarrgts_thread = new YarrgTimestampFetcher(counter);
       yarrgts_thread.start();
     }
 
-    AccessibleTable accesstable = findMarketTable();
-    if(accesstable == null) {
-      error("Market table not found!"+
-           " Please open the Buy/Sell Commodities interface.");
-      return;
-    }
-    if(accesstable.getAccessibleRowCount() == 0) {
-      error("No data found, please wait for the table to have data first!");
-      return;
-    }
-    if(!isDisplayAll()) {
-      error("Please select \"All\" from the Display: popup menu.");
-      return;
-    }
+    final AccessibleTable accesstable = 
+    new UIXR<AccessibleTable>() { public AccessibleTable bodyr() {
+      progmon = new ProgressMonitor
+       (frame,"Processing Market Data","Getting table data",0,100);
+      progmon.setMillisToDecideToPopup(0);
+      progmon.setMillisToPopup(0);
+
+      AccessibleTable at = findMarketTable();
+      if(at == null) {
+       error("Market table not found!"+
+             " Please open the Buy/Sell Commodities interface.");
+       return null;
+      }
+      if(at.getAccessibleRowCount() == 0) {
+       error("No data found, please wait for the table to have data first!");
+       return null;
+      }
+      if(!isDisplayAll()) {
+       error("Please select \"All\" from the Display: popup menu.");
+       return null;
+      }
+
+      debuglog("(async) getisland...");
+      getIsland();
+      debuglog("getocean...");
+      getOcean();
+      debuglog("getocean done");
 
-    progresslog("(async) getisland...");
-    getIsland();
-    progresslog("getocean...");
-    getOcean();
-    progresslog("getocean done");
+      return at;
+    }}.exec("accesstable");
+    if (accesstable == null) return;
 
     if (latch != null) {
       latch.await(2, java.util.concurrent.TimeUnit.SECONDS);
     }
-    progresslog("(async) getisland done");
+    debuglog("(async) getisland done");
 
     String yarrgts = null;
     if (yarrgts_thread != null) {
-      progresslog("(async) yarrg timestamp join...");
+      debuglog("(async) yarrg timestamp join...");
       yarrgts_thread.join();
-      progresslog("(async) yarrg timestamp joined.");
+      debuglog("(async) yarrg timestamp joined.");
       yarrgts = yarrgts_thread.ts;
     }
 
@@ -562,47 +491,53 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
       return;
     }
 
-    progresslog("table check...");
+    debuglog("table check...");
 
-    String headings_expected[] = new String[]
-      { "Commodity", "Trading outlet", "Buy price",
-       "Will buy", "Sell price", "Will sell" };
-    ArrayList<ArrayList<String>> headers =
-      getData(accesstable.getAccessibleColumnHeader());
-    if (headers.size() != 1) {
-      error("Table headings not one row! " + headers.toString());
-      return;
-    }
-    if (headers.get(0).size() < 6 ||
-       headers.get(0).size() > 7) {
-      error("Table headings not six or seven columns! " + headers.toString());
-      return;
-    }
-    for (int col=0; col<headings_expected.length; col++) {
-      String expd = headings_expected[col];
-      String got = headers.get(0).get(col);
-      if (expd.compareTo(got) != 0) {
-       error("Table heading for column "+col
-             +" is not \""+expd+"\" but \""+got+"\".\n\n"
-             +"Please do not reorder the table when using this tool.");
-       return;
+    final ArrayList<ArrayList<String>> data =
+    new UIXR<ArrayList<ArrayList<String>>>
+          () { public ArrayList<ArrayList<String>> bodyr() {
+      String headings_expected[] = new String[]
+       { "Commodity", "Trading outlet", "Buy price",
+         "Will buy", "Sell price", "Will sell" };
+
+      ArrayList<ArrayList<String>> headers =
+       getData(accesstable.getAccessibleColumnHeader());
+      if (headers.size() != 1) {
+       error("Table headings not one row! " + headers.toString());
+       return null;
+      }
+      if (headers.get(0).size() < 6 ||
+         headers.get(0).size() > 7) {
+       error("Table headings not six or seven columns! " + headers.toString());
+       return null;
+      }
+      for (int col=0; col<headings_expected.length; col++) {
+       String expd = headings_expected[col];
+       String got = headers.get(0).get(col);
+       if (expd.compareTo(got) != 0) {
+         error("Table heading for column "+col
+               +" is not \""+expd+"\" but \""+got+"\".\n\n"
+               +"Please do not reorder the table when using this tool.");
+         return null;
+       }
       }
-    }
 
-    progresslog("table read...");
+      debuglog("table read...");
 
-    ArrayList<ArrayList<String>> data = getData(accesstable);
+      return getData(accesstable);
+    }}.exec("data");
+    if (data == null) return;
 
     if (showArbitrage) {
-      progresslog("arbitrage...");
+      debuglog("arbitrage...");
       calculateArbitrage(data);
-      progresslog("arbitrage done.");
+      debuglog("arbitrage done.");
     }
 
     if (uploadToYarrg && yarrgts != null) {
-      progresslog("yarrg prepare...");
-      progressNote(pm, "Yarrg: Preparing data");
-      pm.setProgress(10);
+      debuglog("yarrg prepare...");
+      progressNote("Yarrg: Preparing data");
+      setProgress(10);
 
       StringBuilder yarrgsb = new StringBuilder();
       String yarrgdata; // string containing what we'll feed to yarrg
@@ -621,44 +556,44 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
 
       yarrgdata = yarrgsb.toString();
 
-      progressNote(pm, "Yarrg: Uploading");
-      progresslog("yarrg upload...");
+      progressNote("Yarrg: Uploading");
+      debuglog("yarrg upload...");
 
       doneyarrg = runYarrg(yarrgts, oceanName, islandName, yarrgdata);
-      progresslog("yarrg done.");
+      debuglog("yarrg done.");
     }
 
     if (uploadToPCTB) {
-      progresslog("pctb prepare...");
-      progressNote(pm, "PCTB: Getting stall names");
-      pm.setProgress(20);
-      if(pm.isCanceled()) {
+      debuglog("pctb prepare...");
+      progressNote("PCTB: Getting stall names");
+      setProgress(20);
+      if(checkCancelled()) {
        return;
       }
       TreeSet<Offer> buys = new TreeSet<Offer>();
       TreeSet<Offer> sells = new TreeSet<Offer>();
       LinkedHashMap<String,Integer> stallMap = getStallMap(data);
-      pm.setProgress(40);
-      progressNote(pm, "PCTB: Sorting offers");
-      if(pm.isCanceled()) {
+      setProgress(40);
+      progressNote("PCTB: Sorting offers");
+      if(checkCancelled()) {
        return;
       }
       // get commod map
                
-      progresslog("pctb commodmap...");
+      debuglog("pctb commodmap...");
       HashMap<String,Integer> commodMap = getCommodMap();
       if(commodMap == null) {
        return;
       }
-      progresslog("pctb commodmap done.");
+      debuglog("pctb commodmap done.");
       int[] offerCount = getBuySellMaps(data,buys,sells,stallMap,commodMap);
-      // if (dtxt!=null) dtxt.println(sells);
-      // if (dtxt!=null) dtxt.println("\n\n\n"+buys);
+      // debuglog(sells);
+      // debuglog("\n\n\n"+buys);
 
       ByteArrayOutputStream outStream = new ByteArrayOutputStream();
-      pm.setProgress(60);
-      progressNote(pm, "PCTB: Sending data");
-      if(pm.isCanceled()) {
+      setProgress(60);
+      progressNote("PCTB: Sending data");
+      if(checkCancelled()) {
        return;
       }
       GZIPOutputStream out = new GZIPOutputStream(outStream);
@@ -668,46 +603,52 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
       dos.writeBytes(getAbbrevStallList(stallMap));
       writeBuySellOffers(buys,sells,offerCount,out);
       out.finish();
-      progresslog("pctb send...");
+      debuglog("pctb send...");
 
       byte[] ba = outStream.toByteArray();
       debug_write_bytes("pctb-marketdata.gz", ba);
 
       InputStream in = sendInitialData(new ByteArrayInputStream(ba));
-      progresslog("pctb sent.");
+      debuglog("pctb sent.");
       if (in == null) return;
-      pm.setProgress(80);
-      if(pm.isCanceled()) {
+      setProgress(80);
+      if(checkCancelled()) {
        return;
       }
-      progressNote(pm, "PCTB: Waiting ...");
-      progresslog("pctb finish...");
+      progressNote("PCTB: Waiting ...");
+      debuglog("pctb finish...");
       donepctb = finishUpload(in);
-      progresslog("pctb done.");
+      debuglog("pctb done.");
     }
-    pm.setProgress(100);
+    setProgress(99);
 
+    String summary;
     if ((uploadToPCTB && !donepctb) ||
        (uploadToYarrg && !doneyarrg)) {
-      resultSummary.setText("trouble");
+      summary= "trouble";
     } else if (unknownPCTBcommods != 0) {
-      resultSummary.setText("PCTB lacks "+unknownPCTBcommods+" commod");
+      summary= "PCTB lacks "+unknownPCTBcommods+" commod(s)";
     } else if (donepctb || doneyarrg) {
-      resultSummary.setText("Done " + islandName);
+      summary= "Done " + islandName;
     } else {
-      resultSummary.setText("uploaded nowhere!");
+      summary= "uploaded nowhere!";
     }
-    progresslog("done.");
-  }
-       
-  /**
-   *   Get the offer data out of the table and cache it in an
-   *   <code>ArrayList</code>.
-   *   
-   *   @param table the <code>AccessibleTable</code> containing the market data
-   *   @return an array of record arrays, each representing a row of the table
+    final String summary_final = summary;
+    new UIX() { public void body() {
+      resultSummary.setText(summary_final);
+    }}.exec("resultSummary.setText");
+
+    debuglog("done.");
+  }
+
+  /*
+   * UPLOAD HELPER FUNCTIONS FOR EXTRACTING SPECIFIC UI DATA
    */
+       
   private ArrayList<ArrayList<String>> getData(AccessibleTable table) {
+    // Gets the offer data out of the table and returns it as an ArrayList
+
+    on_ui_thread();
     ArrayList<ArrayList<String>> data = new ArrayList<ArrayList<String>>();
     for (int i = 0; i < table.getAccessibleRowCount(); i++) {
       ArrayList<String> row = new ArrayList<String>();
@@ -720,113 +661,330 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     return data;
   }
        
-  /**
-   *   @return the table containing market data if it exists,
-   *   otherwise <code>null</code>
-   */
   public AccessibleTable findMarketTable() {
+    // Return the table containing market data if it exists, otherwise null.
+    on_ui_thread();
+
     Accessible node1 = window;
     Accessible node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,0,1,0,0}); 
       // commod market
     // commod market: {0,1,0,0,0,0,1,0,0,1,0}  {0,1,0,0,0,0,1,0,1,0,0,1,0,0})
-    // if (dtxt!=null) dtxt.println(node);
+    // debuglog(node);
     if (!(node instanceof JTable)) {
       node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,1,0,0,1,0,0});
         // commod market
     }
     if (!(node instanceof JTable)) return null;
     AccessibleTable table = node.getAccessibleContext().getAccessibleTable();
-    // if (dtxt!=null) dtxt.println(table);
+    // debuglog(table);
     return table;
   }
        
-  /**
-   *   Utility method to descend through several levels of Accessible children
-   *   at once.
+  private boolean isDisplayAll() {
+    // Returns true iff the "Display:" menu on the commodities
+    // interface in YPP is set to "All"
+    on_ui_thread();
+
+    Accessible button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,0,0,1});
+    if(!(button instanceof JButton)) {
+      button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,1,0,0,0,1});
+    }
+    String display = button.getAccessibleContext().getAccessibleName();
+    if(!display.equals("All")) {
+      return false;
+    }
+    return true;
+  }
+       
+  /*
+   * FUNCTIONS AND CALLBACKS FOR FINDING ISLAND AND OCEAN
+   */
+
+  private void getOcean() {
+    // Finds the ocean name from the window title.
+    // Stores it in the global oceanName
+    on_ui_thread();
+
+    oceanName = null;
+    AccessibleContext topwindow = window.getAccessibleContext();
+    oceanName = topwindow.getAccessibleName()
+      .replaceAll(".*on the (\\w+) ocean", "$1");
+  }
+
+  private void getIsland() {
+    // Tries to find the island name.  Either:
+    //  (a) sets the islandName global
+    // or
+    //  (b) sets latch to a new CountDownLatch, and arranges that 
+    //      at some point later, islandName will be set and the latch
+    //      decremented to zero
+    on_ui_thread();
+
+    // If the league tracker is there, we can skip the faff
+    // and ask for its tooltip, since we're on a boat
+
+    Accessible leagueTrackerContainer =
+      descendNodes(window,new int[] {0,1,0,0,2,1});
+    Accessible leagueTrackerItself =
+      descendByClass(leagueTrackerContainer,
+                    "com.threerings.yohoho.sea.client.LeagueTracker");
+    Accessible leagueTracker = descend(leagueTrackerItself, 1);
+    try {
+      islandName = ((JLabel)leagueTracker).getToolTipText();
+      latch = null;
+    } catch (NullPointerException e) {
+      // evidently we're actually on an island
+
+      islandName = null;
+      AccessibleContext chatArea =
+       descendNodes(window,new int[] {0,1,0,0,0,2,0,0,2})
+       .getAccessibleContext();
+      // attach the property change listener to the outer sunshine
+      // panel if the "ahoy" tab is not active, otherwise attach it to
+      // the scroll panel in the "ahoy" tab.
+      if(!"com.threerings.piracy.client.AttentionListPanel".
+        equals(descendNodes(window,new int[] {0,1,0,0,2,2,0})
+               .getClass().getCanonicalName())) {
+       sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2})
+         .getAccessibleContext();
+      } else {
+       sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0})
+         .getAccessibleContext();
+      }
+      sidePanel.addPropertyChangeListener(changeListener);
+      latch = new java.util.concurrent.CountDownLatch(1);
+      // make the Players Online ("/who") panel appear
+      AccessibleEditableText chat = chatArea.getAccessibleEditableText();
+      chat.setTextContents("/w");
+      int c = chatArea.getAccessibleAction().getAccessibleActionCount();
+      for(int i=0;i<c;i++) {
+       if("notify-field-accept".equals(chatArea.getAccessibleAction()
+                                       .getAccessibleActionDescription(i))) {
+         chatArea.getAccessibleAction().doAccessibleAction(i);
+       }
+      }
+    }
+  }
+
+  private PropertyChangeListener changeListener = new PropertyChangeListener() {
+    // used by getIsland
+    public void propertyChange(PropertyChangeEvent e) {
+      on_ui_thread();
+      if(e.getNewValue() != null && 
+        e.getPropertyName().equals
+        (AccessibleContext.ACCESSIBLE_CHILD_PROPERTY)) {
+       Accessible islandInfo =
+         descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0,0,1,2});;
+       String text = islandInfo.getAccessibleContext().getAccessibleText()
+         .getAtIndex(AccessibleText.SENTENCE,0);
+       int index = text.indexOf(":");
+       String name = text.substring(0,index);
+       islandName = name;
+       // debuglog(islandName);
+       sidePanel.removePropertyChangeListener(this);
+       latch.countDown();
+      }
+    }
+  };
+
+  /*
+   * UTILITY FUNCTIONS FOR WALKING THE UI
    *
-   *   @param parent the node on which to start the descent
-   *   @param path an array of ints, each int being the index of the next
-   *   accessible child to descend.
-   *   @return the <code>Accessible</code> reached by following the
-   *   descent path, or <code>null</code> if the desired path was
-   *   invalid.
+   * These functions all return null if the specified path or child
+   * was not found.
    */
+
   private Accessible descendNodes(Accessible parent, int[] path) {
+    // Descends through several levels of Accessible children in one call.
+    // path[] is an array of ints, each int being the index into the array
+    // of children at a particular point, and thus selects the specific
+    // accessible child to descend to.
+    on_ui_thread();
+
     for(int i=0;i<path.length;i++) {
       if (null == (parent = descend(parent, path[i]))) return null;
     }
     return parent;
   }
-       
-  /**
-   *   Descends one level to the specified child of the parent
-   *   <code>Accessible</code> "node".
-   *   
-   *   @param parent the node with children
-   *   @param childNum the index of the child of <code>parent</code> to return
-   *   @return the <code>childNum</code> child of <code>parent</code>
-   *   or <code>null</code> if the child is not found.
-   */
+
   private Accessible descend(Accessible parent, int childNum) {
+    // Descends one level to the specified child of the parent
+    // childNum is the index of the child within parent
+    on_ui_thread();
+
     if (parent == null) return null;
     int children = parent.getAccessibleContext().getAccessibleChildrenCount();
     if (childNum >= children) {
-      if (dtxt!=null) dtxt.println("DESCEND "+childNum+" > "
-                                  +children+" NOT FOUND");
+      debuglog("DESCEND "+childNum+" > "+children+" NOT FOUND");
       return null;
     }
     Accessible child = parent.getAccessibleContext()
       .getAccessibleChild(childNum);
-    if (dtxt!=null) dtxt.println("DESCEND "+childNum+" "
-                                +child.getClass().getName()+" OK");
+    debuglog("DESCEND "+childNum+" "+child.getClass().getName()+" OK");
     return child;
   }
 
-  /**
-   *   Descends one level to the child which has the specified class.
-   *   
-   *   @param parent the node with children
-   *   @param classname the name of the class, as a string
-   *   @return the child or <code>null</code> if the child is not found.
-   */
   private Accessible descendByClass(Accessible parent, String classname) {
+    // Descends one level to the first child which has the specified class.
+    on_ui_thread();
+
     if (parent == null) return null;
     AccessibleContext ac = parent.getAccessibleContext();
     int children = ac.getAccessibleChildrenCount();
     for (int i=0; i<children; i++) {
       Accessible child = ac.getAccessibleChild(i);
       if (child.getClass().getName() == classname) {
-       if (dtxt!=null) dtxt.println("DESCEND CLASS "+classname+" OK");
+       debuglog("DESCEND CLASS "+classname+" OK");
        return child;
       }
     }
-    if (dtxt!=null) dtxt.println("DESCEND CLASS "+classname+" NOT FOUND");
+    debuglog("DESCEND CLASS "+classname+" NOT FOUND");
     return null;
   }
+  
 
-  public static void main(String[] args) {
-    new MarketUploader();
-  }
+  /*****************************************
+   * PCTB-SPECIFIC HELPER FUNCTIONS ETC.   *
+   *****************************************/
 
   /**
-   *   Returns true if the "Display:" menu on the commodities
-   *   interface in YPP is set to "All"
+   *   An abstract market offer, entailing a commodity being bought or sold by
+   *   a shoppe, for a certain price in a certain quantity. Not instantiable.
    *
-   *   @return <code>true</code> if all commodities are displayed,
-   *   otherwise <code>false</code>
+   *   @see Buy
+   *   @see Sell
    */
-  private boolean isDisplayAll() {
-    Accessible button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,0,0,1});
-    if(!(button instanceof JButton)) {
-      button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,1,0,0,0,1});
+  abstract class Offer {
+    public int commodity, price, quantity, shoppe;
+    /**
+     * Create an offer from <code>record</code>, determining the shoppe Id from
+     * <code>stallMap</code> and the commodity Id from <code>commodMap</code>.
+     * <code>priceIndex</code> should be the index of the price in the record
+     * (the quantity will be <code>priceIndex + 1</code>).
+     *
+     * @param record the record with data to create the offer from
+     * @param stallMap a map containing the ids of the various stalls
+     * @param commodMap a map containing the ids of the various commodities
+     * @param priceIndex the index of the price in the record
+     */
+    public Offer(ArrayList<String> record, 
+                LinkedHashMap<String,Integer> stallMap, 
+                HashMap<String,Integer> commodMap,
+                int priceIndex) {
+      Integer commodId = commodMap.get(record.get(0));
+      if(commodId == null) {
+       throw new IllegalArgumentException();
+      }
+      commodity = commodId.intValue();
+      price = Integer.parseInt(record.get(priceIndex));
+      String qty = record.get(priceIndex+1);
+      quantity = parseQty(qty);
+      shoppe = stallMap.get(record.get(1)).intValue();
     }
-    String display = button.getAccessibleContext().getAccessibleName();
-    if(!display.equals("All")) {
-      return false;
+               
+    /**
+     * Returns a human-readable version of this offer, useful for debugging
+     * 
+     * @return human-readable offer
+     */
+    public String toString() {
+      return "[C:" + commodity + ",$" + price + ",Q:"
+       + quantity + ",S:" + shoppe + "]";
+    }
+  }
+       
+  /**
+   *   An offer from a shoppe or stall to buy a certain quantity of a
+   *   commodity for a certain price. If placed in an ordered Set,
+   *   sorts by commodity index ascending, then by buy price
+   *   descending, and finally by stall id ascending.
+   */
+  class Buy extends Offer implements Comparable<Buy> {
+    /**
+     * Creates a new <code>Buy</code> offer from the given
+     * <code>record</code> using the other parameters to determine
+     * stall id and commodity id of the offer.
+     *
+     * @param record the record with data to create the offer from
+     * @param stallMap a map containing the ids of the various stalls
+     * @param commodMap a map containing the ids of the various commodities
+     */
+    public Buy(ArrayList<String> record,
+              LinkedHashMap<String,Integer> stallMap,
+              HashMap<String,Integer> commodMap) {
+      super(record,stallMap,commodMap,2);
+    }
+               
+    /**
+     * Sorts by commodity index ascending, then price descending,
+     * then stall id ascending.
+     */
+    public int compareTo(Buy buy) {
+      // organize by: commodity index, price, stall index
+      if(commodity == buy.commodity) {
+       // organize by price, then by stall index
+       if(price == buy.price) {
+         // organize by stall index
+         return shoppe>buy.shoppe ? 1 : -1;
+       } else if(price > buy.price) {
+         return -1;
+       } else {
+         return 1;
+       }
+      } else if(commodity > buy.commodity) {
+       return 1;
+      } else {
+       return -1;
+      }
     }
-    return true;
   }
        
+  /**
+   *   An offer from a shoppe or stall to sell a certain quantity of
+   *   a commodity for a certain price. If placed in an ordered Set,
+   *   sorts by commodity index ascending, then by sell price
+   *   ascending, and finally by stall id ascending.
+   */
+  class Sell extends Offer implements Comparable<Sell> {
+    /**
+     * Creates a new <code>Sell</code> offer from the given
+     * <code>record</code> using the other parameters to determine
+     * stall id and commodity id of the offer.
+     *
+     * @param record the record with data to create the offer from
+     * @param stallMap a map containing the ids of the various stalls
+     * @param commodMap a map containing the ids of the various commodities
+     */
+    public Sell(ArrayList<String> record,
+               LinkedHashMap<String,Integer> stallMap,
+               HashMap<String,Integer> commodMap) {
+      super(record,stallMap,commodMap,4);
+    }
+               
+    /**
+     * Sorts by commodity index ascending, then price ascending, then
+     * stall id ascending.
+     */
+    public int compareTo(Sell sell) {
+      // organize by: commodity index, price, stall index
+      if(commodity == sell.commodity) {
+       // organize by price, then by stall index
+       if(price == sell.price) {
+         // organize by stall index
+         return shoppe>sell.shoppe ? 1 : -1;
+       } else if(price > sell.price) {
+         return 1;
+       } else {
+         return -1;
+       }
+      } else if(commodity > sell.commodity) {
+       return 1;
+      } else {
+       return -1;
+      }
+    }
+  }
+
   /**
    *   Gets the list of commodities and their associated commodity ids.
    *
@@ -834,6 +992,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
    *   the commodity id.
    */
   private HashMap<String,Integer> getCommodMap() {
+    on_our_thread();
     if(commodMap != null) {
       return commodMap;
     }
@@ -933,8 +1092,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
        }
       } catch(IllegalArgumentException e) {
        unknownPCTBcommods++;
-       if (dtxt!=null) dtxt.println("Error: Unsupported Commodity \""
-                                    + offer.get(0) + "\"");
+       debuglog("Error: Unsupported Commodity \"" + offer.get(0) + "\"");
       }
     }
     if (buySellCount[0]==0 && buySellCount[1]==0) {
@@ -1081,6 +1239,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
    *   @param file an InputStream open to the gzipped data we want to send
    */
   private InputStream sendInitialData(InputStream file) throws IOException {
+    on_our_thread();
     ClientHttpRequest http =
       new ClientHttpRequest(PCTB_HOST_URL + "upload.php");
     http.setParameter("marketdata","marketdata.gz",file,"application/gzip");
@@ -1114,6 +1273,8 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
    *   @param in stream of data from the server to read
    */
   private boolean finishUpload(InputStream in) throws IOException {
+    on_our_thread();
+
     String html = readstreamstring(in);
     debug_write_stringdata("pctb-initial.html", html);
     Matcher m;
@@ -1173,8 +1334,14 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     }
   }
 
+
+  /*****************************************
+   * YARRG-SPECIFIC HELPER FUNCTIONS ETC.  *
+   *****************************************/
+
   private InputStream post_for_yarrg(ClientHttpRequest http)
   throws IOException {
+    on_our_thread();
     if (!http.post()) {
       String err = readstreamstring(http.resultstream());
       error("<html><h1>Error reported by YARRG server</h1>\n" + err);
@@ -1183,6 +1350,21 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     return http.resultstream();
   }
 
+  private class YarrgTimestampFetcher extends Thread {
+    public YarrgTimestampFetcher(int counter) {
+      super("MarketUploader-YarrgTimestampFetcher-"+uploadcounter);
+    }
+    public String ts = null;
+    public void run() {
+      try {
+       ts = getYarrgTimestamp();
+       debuglog("(async) yarrg timestamp ready.");
+      } catch(Exception e) {
+       error("Error getting YARRG timestamp: "+e);
+      }
+    }
+  };
+
   private String getYarrgTimestamp() throws IOException {
     ClientHttpRequest http = new ClientHttpRequest (YARRG_URL);
     http.setParameter("clientname", YARRG_CLIENTNAME);
@@ -1228,7 +1410,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
   }
 
   private int calculateArbitrageCommodity(ArrayList<SortedSet<int[]>> arb_bs) {
-    // if (dtxt!=null) dtxt.println("ARBITRAGE?");
+    // debuglog("ARBITRAGE?");
     int profit = 0;
     SortedSet<int[]> buys = arb_bs.get(0);
     SortedSet<int[]> sells = arb_bs.get(1);
@@ -1244,9 +1426,9 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
 
       int unitprofit = buy[0] - sell[0];
       int count = buy[1] < sell[1] ? buy[1] : sell[1];
-      // if (dtxt!=null) dtxt.println(" sell @"+sell[0]+" x"+sell[1]
-      //                       +" buy @"+buy[0]+" x"+buy[1]
-      //                      +" => x"+count+" @"+unitprofit);
+      // debuglog(" sell @"+sell[0]+" x"+sell[1]
+      //          +" buy @"+buy[0]+" x"+buy[1]
+      //         +" => x"+count+" @"+unitprofit);
 
       if (unitprofit <= 0)
        break;
@@ -1257,10 +1439,14 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
       if (buy[1]==0) buys.remove(buy);
       if (sell[1]==0) sells.remove(sell);
     }
-    // if (dtxt!=null) dtxt.println(" PROFIT "+profit);
+    // debuglog(" PROFIT "+profit);
     return profit;
   }
 
+  /*****************************************
+   * ARBITRAGE                             *
+   *****************************************/
+
   private class arbitrageOfferComparator implements Comparator {
     public int compare(Object o1, Object o2) {
       int p1 = ((int[])o1)[0];
@@ -1270,7 +1456,8 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
   }
 
   private @SuppressWarnings("unchecked")
-    void calculateArbitrage(ArrayList<ArrayList<String>> data) {
+  void calculateArbitrage(ArrayList<ArrayList<String>> data) 
+  throws InterruptedException {
     int arbitrage = 0;
     ArrayList<SortedSet<int[]>> arb_bs = null;
     String lastcommod = null;
@@ -1278,15 +1465,15 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
 
     for (ArrayList<String> row : data) {
       String thiscommod = row.get(0);
-      // if (dtxt!=null) dtxt.println("ROW "+row.toString());
+      // debuglog("ROW "+row.toString());
       if (lastcommod == null || !thiscommod.equals(lastcommod)) {
        if (lastcommod != null)
          arbitrage += calculateArbitrageCommodity(arb_bs);
-       // if (dtxt!=null) dtxt.println("ROW rdy");
+       // debuglog("ROW rdy");
        arb_bs = new ArrayList<SortedSet<int[]>>(2);
        arb_bs.add(0, new TreeSet<int[]>(compar));
        arb_bs.add(1, new TreeSet<int[]>(compar));
-       // if (dtxt!=null) dtxt.println("ROW init");
+       // debuglog("ROW init");
        lastcommod = thiscommod;
       }
       for (int bs = 0; bs < 2; bs++) {
@@ -1294,19 +1481,23 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
        if (pricestr == null)
          continue;
        int[] entry = new int[2];
-       // if (dtxt!=null) dtxt.println("ROW BS "+bs);
+       // debuglog("ROW BS "+bs);
        entry[0] = parseQty(pricestr);
        entry[1] = parseQty(row.get(bs*2 + 3));
        arb_bs.get(bs).add(entry);
       }
     }
     arbitrage += calculateArbitrageCommodity(arb_bs);
+    String arb;
     if (arbitrage != 0) {
-      arbitrageResult.setText("<html><strong>arbitrage: "+arbitrage
-                             +" poe</strong>");
+      arb = "<html><strong>arbitrage: "+arbitrage+" poe</strong>";
     } else {
-      arbitrageResult.setText("no arbitrage");
+      arb = "no arbitrage";
     }
+    final String arb_final = arb;
+    EventQueue.invokeLater(new Runnable() { public void run() {
+      arbitrageResult.setText(arb_final);
+    }});
   }
     
 }