/* * [VerCheck.java] * * Summary: VerCheck : Applet to check if applications have a new update available. * * Copyright: (c) 2007-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2008-01-25 initial release * 1.1 2008-01-25 new Opera beta, add VerCheck itself * 1.2 2008-01-26 automatically restore default apps on every use, to automatically keep them up to date. * 1.3 2008-01-29 4NT incremented build number 8.02:102 to 8.02:103 * 1.4 2008-01-30 4NT/TCC now repackaged as Take Command. Better thread isolation. Goldwave 5.23 * 1.5 2008-02-01 new Take Command, new Opera Beta * 1.6 2008-02-01 remove obsolete entries. better check for Corel and Safari * 1.7 2008-02-03 new version Take Command, add iTunes * 1.8 2008-02-06 new version Take Command * 1.9 2008-02-07 Firefox 12.0.0.12 and SeaMonkey 1.1.8 * 2.0 2008-02-09 Adobe Acrobat 1.1.2, requires regex and new Take Command * 2.1 2008-02-11 BitTorrent 6.0.2, new Take Command * 2.2 2008-02-14 new version FastStone 6.0 * 2.3 2008-02-16 new version Take Command. New icon for apps released in last week. * 2.4 2008-02-27 new Take Command, Boot-It, iTunes, Copernic, Opera * 2.5 2008-03-03 fix bug causing user apps to disappear, redo persistence, reorg way icons computed. * 2.6 2008-03-09 remove restore defaults button, many version change marker changes. * 2.7 2008-03-26 new firefox, SeaMonkey, intellij * 2.8 2008-04-17 many version detection string changes. * 2.9 2008-05-06 reorder columns so date last updated more visible. * 3.0 2008-08-20 new look under Vista with native fonts, and easier to read background. * 3.1 2008-08-31 all user to add a description field to each app. Export all data to HTML. * 3.2 2008-09-02 make sound work on Vista/JDK 1.6.0_11+ and in application mode. * 3.3 2008-11-17 retry probes of apps that could not connect. * 3.4 2008-12-17 add DONE sound (kettle switch clicking off) when all apps checked * 3.5 2009-01-11 arrange that two copies of VerCheck running at once do not interfere with each other. * 3.6 2009-02-20 refactor to use new HTTP library * 3.7 2009-03-20 define default sand obsoletes in files rather than hard wired into the program. * 3.8 2009-03-31 change order of fields in defaults.csv * 3.9 2009-05-18 now check in three passes so see interesting results sooner. * 4.0 2009-06-03 extra status information about how the check went. * 4.1 2010-02-01 now display date and time last probes all sites. * 4.2 2010-03-24 user selectable Look and Feel. * 4.3 2011-09-22 suppress autoscrolling to avoid conflicting with the user scrolling. * 4.4 2012-03-23 sometimes bypass rechecking when we have done it recently * 4.5 2014-06-06 app names and descriptions may now contain entities. * 4.6 2014-06-15 changed order of columns in defaults.csv to make version URL more accessible. * 4.7 2015-01-04 do probes on multiple threads for speedup * 4.8 2015-11-30 countdown status, ensure important result visible at end. */ package com.mindprod.vercheck; import com.mindprod.common18.BigDate; import com.mindprod.common18.Build; import com.mindprod.common18.CMPAboutJBox; import com.mindprod.common18.EIO; import com.mindprod.common18.FontFactory; import com.mindprod.common18.HybridJ; import com.mindprod.common18.JEButton; import com.mindprod.common18.Laf; import com.mindprod.common18.ST; import com.mindprod.common18.VersionCheck; import com.mindprod.fastcat.FastCat; import com.mindprod.http.Get; import javax.swing.ImageIcon; import javax.swing.JApplet; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.Color; import java.awt.Container; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.PrintWriter; import java.net.URL; import java.util.EnumSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import static com.mindprod.entities.EntifyStrings.entifyHTML; import static java.lang.System.*; /** * VerCheck : Applet to check if applications have a new update available. * * @author Roedy Green, Canadian Mind Products * @version 4.8 2015-11-30 countdown status, ensure important result visible at end. * @since 2008-01-25 */ // TODO: hook up icon for invalid state (e.g. bad url, bad regex, bad date). // TODO: allow post? cookie?, at least internally. public class VerCheck extends JApplet implements Runnable { /* Sometimes probe fails because Diffie Hellman cannot generate a key pair for SSL. This is a known Oracle Java bug that his been around a long time. http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6521495 */ /** * how many threads we use to probe apps. Each thread will probe about 3 apps. */ public static final int PROBE_THREADS = 30; /** * Applet height in pixels */ private static final int APPLET_HEIGHT = 970; /** * Applet width in pixels */ private static final int APPLET_WIDTH = 1228; private static final int FIRST_COPYRIGHT_YEAR = 2007; /** * max size in bytes of one AppToWatch after serialization. */ private static final int SERIALIZED_SIZE = 500; /** * if check within 30 minutes of last check, we don't redo the apps unlikely to change */ private static final long RECENTLY = TimeUnit.MINUTES.toMillis( 30 ); /** * timeout to connect, time till server responds */ private static final int CONNECT_TIMEOUT = ( int ) TimeUnit.SECONDS.toMillis( 70 ); /** * timeout to read entire page, after server responds */ private static final int READ_TIMEOUT = ( int ) TimeUnit.SECONDS.toMillis( 70 ); /** * not displayed copyright */ @SuppressWarnings( { "UnusedDeclaration" } ) private static final String EMBEDDED_COPYRIGHT = "Copyright: (c) 2007-2017 Roedy Green, Canadian Mind Products, http://mindprod.com"; private static final String INSTRUCTIONS = "Double click to edit, then fill in fields for all the programs you want to check, " + "then click \"check for new versions\".\n" + "Select the row to insert, then click (+) to add a new application.\n" + "Select the row to delete, then click (-) to remove an application."; /** * when this version was released */ @SuppressWarnings( { "UnusedDeclaration" } ) private static final String RELEASE_DATE = "2015-11-30"; /** * title of Applet */ private static final String TITLE_STRING = "VerCheck Version Change Detector"; /** * embedded version string */ @SuppressWarnings( { "UnusedDeclaration" } ) private static final String VERSION_STRING = "4.8"; /** * background colour, pale green to match website */ private static final Color BACKGROUND_FOR_BODY = Build.BACKGROUND_FOR_BLENDING; /** * fresh summary background Color, when probe done this session */ private static final Color BACKGROUND_FOR_FRESH_SUMMARY = Color.WHITE; /** * background colour for instructions. Default grey is too dark */ private static final Color BACKGROUND_FOR_INSTRUCTIONS = new Color( 0xf8f8f8 ); /** * checking background colour */ private static final Color BACKGROUND_FOR_PROGRESS = new Color( 0x005e6e/* dark cyan */ ); /** * stale summary background Color */ private static final Color BACKGROUND_FOR_STALE_SUMMARY = new Color( 0xffffdd /* off white cream eggshell */ ); /** * fresh summary color, when probe done this session */ private static final Color FOREGROUND_FOR_FRESH_SUMMARY = new Color( 0x005e6e/* dark cyan */ ); /** * instruction normal color */ private static final Color FOREGROUND_FOR_INSTRUCTIONS = new Color( 0x339911 ); /** * checking colour */ private static final Color FOREGROUND_FOR_PROGRESS = new Color( 0xccffcc/* light green */ ); /** * stale summary color */ private static final Color FOREGROUND_FOR_STALE_SUMMARY = new Color( 0xcd6839/* sienna 4 */ ); /** * for titles */ private static final Color FOREGROUND_FOR_TITLES = new Color( 0xdc143c ); /** * instructions font */ private static final Font FONT_FOR_INSTRUCTIONS = FontFactory.build( "Dialog", Font.PLAIN, 12 ); /** * progress font */ private static final Font FONT_FOR_PROGRESS = FontFactory.build( "Dialog", Font.PLAIN, 12 ); /** * summary font */ private static final Font FONT_FOR_SUMMARY = FontFactory.build( "Dialog", Font.PLAIN, 12 ); /** * for for title second line */ private static final Font FONT_FOR_TITLE2 = FontFactory.build( "Dialog", Font.PLAIN, 14 ); /** * title font */ private static final Font FONT_FOR_TITLES = FontFactory.build( "Dialog", Font.BOLD, 18 ); /** * green lightning icon to check for new versions */ private static final ImageIcon ICON_FOR_CHECK = new ImageIcon( VerCheck.class.getResource( "image/check.png" ) ); /** * minus icon to remove a row */ private static final ImageIcon ICON_FOR_MINUS = new ImageIcon( VerCheck.class.getResource( "image/minus.png" ) ); /** * + icon to add a row */ private static final ImageIcon ICON_FOR_PLUS = new ImageIcon( VerCheck.class.getResource( "image/plus.png" ) ); /** * sub node for the app descriptions. */ private static final Preferences persistedApps = Preferences.userNodeForPackage( VerCheck.class ).node( "apps" ); /** * where in registry we persist our history */ private static final Preferences persistence = Preferences.userNodeForPackage( VerCheck.class ); /** * name of file from command line to export HTML summary to. Null for Applet suppresses export */ private static String exportFilename = null; /** * have we recently checked everything */ private static boolean isRecentlyChecked = false; /** * how many probes, to go, including ones not started yet */ private static volatile int togo = 0; /** * true if we are running as an application */ private final boolean asApplication; /** * contentPane of the JApplet */ private Container contentPane; /** * button to submit URL to various sites */ private JEButton checkButton; /** * button to remove currently selected app */ private JEButton minusButton; /** * button to add a blank line for new app. */ private JEButton plusButton; /** * title for app */ private JLabel title; /** * second title line for app */ private JLabel title2; /** * control scrolling of the response field */ private JScrollPane scroller; /** * instructions on how to use program */ private JTextArea instructions; /** * summary of how checks went */ private JTextArea summary; /** * how the checking in progressing */ private JTextField progress; /** * one row for each app */ private VerCheckTableModel tableModel; /** * is the user using the scroller */ private boolean userBusyScrolling; /** * constructor */ public VerCheck() { this.asApplication = false; } /** * constructor * * @param asApplication true if running as application. */ public VerCheck( boolean asApplication ) { this.asApplication = asApplication; } /** * Allow this Applet to run as as application as well. * * @param args command line arguments ignored. */ public static void main( String args[] ) { if ( args.length > 0 ) { exportFilename = args[ 0 ]; } // when run as application will call init, start, stop, destroy HybridJ.fireup( new VerCheck( true/* as application */ ), TITLE_STRING + " " + VERSION_STRING, APPLET_WIDTH, APPLET_HEIGHT ); } // end main /** * Analyse Probe Results * * @param rowIndex which row# we are working hon * @param row row object * @param url url to probe this app * @param marker string to look for to verify version * @param result result of probe * @param responseCode numeric http code * @param get Get object for probe. */ private void analyseProbeResult( final int rowIndex, final AppToWatch row, final URL url, final String marker, final String result, final int responseCode, final Get get ) { if ( responseCode == 200 && result != null && result.length() > 0 ) { boolean found; if ( marker.startsWith( "regex:" ) ) { // handle regex, string out past regex: try { final Pattern p = Pattern.compile( marker.substring( "regex:".length() ) ); final Matcher m = p.matcher( result ); found = m.find(); } catch ( PatternSyntaxException | Error e ) { Audio.INVALID_REGEX.play(); found = false; } } else if ( marker.startsWith( "staged:" ) ) { found = Staged.found( result, ST.chopLeadingString( marker, "staged:" ) ); } else { // handle ordinary non-regex search found = result.contains( marker ); } if ( found ) { // state will be adjusted considering dateReleased. tableModel.setState( rowIndex, AppState.UNCHANGED_RELEASED_IN_LAST_MONTH ); } else { // were able to read it, but could not find marker tableModel.setState( rowIndex, AppState.RECENTLY_CHANGED ); Audio.NEW_VERSION.play(); // display info to help figure out a better marker. err.println( "|||| " + row.getAppName() + " : response " + responseCode + " : " + get.getResponseMessage() + " : from " + url.toString() + " : marker " + row.getMarker() + " ||||" ); err.println( result ); err.println( "|||| " + row.getAppName() + " end response ||||" ); } } else { // unable to read page tableModel.setState( rowIndex, AppState.NO_CONNECTION ); Audio.UNABLE_TO_CONNECT.play(); // display response code. err.println( "|||| " + row.getAppName() + " : response " + responseCode + " : " + get.getResponseMessage() + " : " + get.getInterruptResponseMessage() + " : from " + url.toString() + " ||||" ); } } /** * allocate and initialise all the Swing components */ private void buildComponents() { contentPane = getContentPane(); contentPane.setBackground( BACKGROUND_FOR_BODY ); contentPane.setLayout( new GridBagLayout() ); title = new JLabel( TITLE_STRING + " " + VERSION_STRING ); title.setFont( FONT_FOR_TITLES ); title.setForeground( FOREGROUND_FOR_TITLES ); title2 = new JLabel( "released:" + RELEASE_DATE + " build:" + Build.BUILD_NUMBER ); title2.setFont( FONT_FOR_TITLE2 ); title2.setForeground( FOREGROUND_FOR_TITLES ); plusButton = new JEButton( "add app" ); plusButton.setToolTipText( "Add a blank line for a new application." ); plusButton.setIcon( ICON_FOR_PLUS ); plusButton.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { // current row, or -1 if none selected. final int row = tableModel.getJTable().getSelectedRow(); if ( row >= 0 ) { // selection left pointing to new empty record. tableModel.add( row, new AppToWatch() ); } else { // no row selected, so tack on the end. tableModel.add( new AppToWatch() ); // select the new row final int lastRow = tableModel.getRowCount() - 1; tableModel.getSelectionModel().setSelectionInterval( lastRow, lastRow ); } } } ); minusButton = new JEButton( "remove app" ); minusButton.setIcon( ICON_FOR_MINUS ); minusButton.setToolTipText( "Remove currently selected application." ); minusButton.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { // current row, or -1 if none selected. final int rowIndex = tableModel.getJTable().getSelectedRow(); if ( 0 <= rowIndex && rowIndex < tableModel.getRowCount() ) { tableModel.remove( rowIndex ); // leave selection pointing at next row that moved up. } else { // no row selected, nothing to delete, Should not happen. Toolkit.getDefaultToolkit().beep(); } } } ); // the minus button does not work except when there is a row selected. tableModel.getSelectionModel().addListSelectionListener( new ListSelectionListener() { /** * Called whenever the value of the * selection changes. * @param e the event that characterizes the * change. */ public void valueChanged( ListSelectionEvent e ) { minusButton.setEnabled( !tableModel .getSelectionModel() .isSelectionEmpty() ); } } ); checkButton = new JEButton( "check for new versions" ); checkButton.setIcon( ICON_FOR_CHECK ); checkButton.setToolTipText( "Check all programs to see if there is a new version" ); checkButton.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { Thread t = new Thread( VerCheck.this ); // check the apps on a separate thread // so Swing thread can update screen. t.start();// which triggers run } } ); progress = new JTextField( "" ); progress.setFont( FONT_FOR_PROGRESS ); progress.setForeground( FOREGROUND_FOR_PROGRESS ); progress.setBackground( BACKGROUND_FOR_PROGRESS ); progress.setEditable( false ); progress.setMargin( new Insets( 3, 3, 3, 3 ) ); progress.setVisible( false ); summary = new JTextArea( "", 4, 120 ); summary.setFont( FONT_FOR_SUMMARY ); summary.setForeground( FOREGROUND_FOR_STALE_SUMMARY ); summary.setBackground( BACKGROUND_FOR_STALE_SUMMARY ); summary.setEditable( false ); summary.setMargin( new Insets( 3, 3, 3, 3 ) ); instructions = new JTextArea( INSTRUCTIONS, 4, 120 ); instructions.setFont( FONT_FOR_INSTRUCTIONS ); instructions.setForeground( FOREGROUND_FOR_INSTRUCTIONS ); instructions.setBackground( BACKGROUND_FOR_INSTRUCTIONS ); instructions.setEditable( false ); instructions.setMargin( new Insets( 3, 3, 3, 3 ) ); // contain the response in JScrollPane. scroller = new JScrollPane( tableModel.getJTable(), JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); // control the speed effect of the wheelhouse scroller.getVerticalScrollBar().setUnitIncrement( 16 ); scroller.getVerticalScrollBar().addAdjustmentListener( new AdjustmentListener() { /** * detect user fiddling with the scroller */ public void adjustmentValueChanged( AdjustmentEvent e ) { userBusyScrolling = e.getValueIsAdjusting(); } } ); } /** * build a menu with Look & Feel and About across the top */ private void buildMenu() { // turn on anti-alias System.setProperty( "swing.aatext", "true" ); final JMenuBar menubar = new JMenuBar(); setJMenuBar( menubar ); final JMenu lafMenu = Laf.buildLookAndFeelMenu(); if ( lafMenu != null ) { menubar.add( lafMenu ); } final JMenu menuHelp = new JMenu( "Help" ); menubar.add( menuHelp ); final JMenuItem aboutItem = new JMenuItem( "About" ); menuHelp.add( aboutItem ); aboutItem.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { new CMPAboutJBox( TITLE_STRING, VERSION_STRING, "Checks websites to see if there is a new version of a given program.", "", "freeware", RELEASE_DATE, FIRST_COPYRIGHT_YEAR, "Roedy Green", "VERCHECK", "1.8" ); } } ); } /** * probe all apps to see if version has changed * * @param group description of which group of apps we are testing, eg. troublesome * @param wanted which apps we should check on this pass. * @param wantedSNI which set of sites we want to probe */ private void checkAllApps( final String group, final boolean wantedSNI, final EnumSet wanted ) { // remove empty elts tableModel.removeEmpties(); // put in order by appName. tableModel.sort(); // no longer mark us unknown prior to pass. Leave old status in place. // set date now so items checked will be computed as of this date. /* don't count this as a check if done one recently */ final long now = System.currentTimeMillis(); isRecentlyChecked = now <= AppToWatch.timestampFullyChecked + RECENTLY; if ( !isRecentlyChecked ) { AppToWatch.timestampFullyChecked = now; } AppToWatch.datePartiallyChecked = AppToWatch.localToday; AppToWatch.timestampPartiallyChecked = now; // check website for each app in the table, reporting results as we go. // User might delete rows while probe is running. final ExecutorService es = Executors.newFixedThreadPool( PROBE_THREADS ); int rowIndex = 0; togo = 0; // count how many probes will will do for ( AppToWatch row : tableModel ) { final AppState state; final boolean sni; synchronized ( row ) { state = row.getState(); sni = row.isSNIEnabled(); } if ( wantedSNI == sni && wanted.contains( state ) ) { togo++; } } // end for displayToGo( group ); // do the probes for appropriate apps. for ( AppToWatch row : tableModel ) { final AppState state; final boolean sni; synchronized ( row ) { state = row.getState(); sni = row.isSNIEnabled(); } final int rowIndexF = rowIndex; final AppToWatch rowF = row; if ( wantedSNI == sni && wanted.contains( state ) ) { es.submit( new Runnable() { public void run() { probeOneApp( group, rowIndexF, rowF ); } } ); } rowIndex++; } try { es.shutdown(); es.awaitTermination( 5, TimeUnit.MINUTES ); } catch ( InterruptedException e ) { } } /** * stop user from pressing buttons */ private void disableButtons() { minusButton.setEnabled( false ); plusButton.setEnabled( false ); checkButton.setEnabled( false ); } /** * Display the to-go progress message * * @param group description of which group of apps we are testing */ private void displayToGo( final String group ) { final String progressText; if ( togo == 0 ) { progressText = ""; } else { progressText = togo + " " + group + " to go"; } SwingUtilities.invokeLater( new Runnable() { public void run() { progress.setText( progressText ); progress.setVisible( true ); } } ); } /** * let user from press buttons */ private void enableButtons() { minusButton.setEnabled( true ); plusButton.setEnabled( true ); checkButton.setEnabled( true ); } /** * when done, ensure any NO_CONNECTION or RECENTLY_CHANGED found is visible */ private void ensureImportantVisible() { int bestRowIndex = -1; AppState bestState = AppState.UNTESTED; for ( int rowIndex = 0; rowIndex < tableModel.getRowCount(); rowIndex++ ) { AppState candidateState = tableModel.get( rowIndex ).getState(); switch ( candidateState ) { case NO_CONNECTION: switch ( bestState ) { case UNTESTED: bestState = AppState.NO_CONNECTION; bestRowIndex = rowIndex; break; default: } break; case RECENTLY_CHANGED: switch ( bestState ) { case UNTESTED: case NO_CONNECTION: bestState = AppState.RECENTLY_CHANGED; bestRowIndex = rowIndex; break; default: } break; default: } } // end for if ( bestRowIndex >= 0 && !userBusyScrolling ) { tableModel.ensureVisible( bestRowIndex ); } } /** * export latest information as an HTML table. * Usually writes to E:\mindprod\jgloss\include\vercheckexport.htmlfrag when run as Application. */ private void export() { if ( !asApplication || exportFilename == null ) { return; } try { // export filename, comes from Vercheck command line. // O P E N final PrintWriter prw = EIO.getPrintWriter( new File( exportFilename ), 4 * 1024, EIO.UTF8 ); // W R I T E final FastCat sb = new FastCat( 24 ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); sb.append( "\n" ); prw.print( sb.toString() ); for ( AppToWatch rowData : tableModel ) { sb.clear(); sb.append( "" ); switch ( state ) { case RECENTLY_CHANGED: case UNCHANGED_RELEASED_IN_LAST_WEEK: sb.append( "\n" ); prw.print( sb.toString() ); } prw.print( "
Roedy Green’s recommended utilities
Roedy’s Recommended Utilities
Last Verified " ); sb.append( BigDate.localToday().toString() ); sb.append( "
-UtilityVersionReleasedDescription
" ); final String downloadURL = rowData.getDownloadInstructionsURL(); if ( downloadURL.length() > 0 ) { sb.append( "", entifyHTML( rowData.getAppName() ), "" ); } else { sb.append( entifyHTML( rowData.getAppName() ) ); } sb.append( "" ); break; default: sb.append( "" ); } sb.append( entifyHTML( rowData.getVersion() ) ); sb.append( "" ); sb.append( rowData.getDateReleased().toString() ); sb.append( "" ); sb.append( entifyHTML( rowData.getDescription() ) ); sb.append( "
\n" ); prw.close(); } catch ( FileNotFoundException e ) { err.println( "Unable to export VerCheck data to HTML" ); } } /** * layout fields using GridBagLayout */ private void layoutFields() { /* 0------------------ 1 -----------2 -----3-----4----- 0 title--------------------------title2 --------------- 0 1 icon_appName_ver_date_description_url_marker_(table)- 1 2 progress ----------------------(+)---(-)--check------ 2 3 progress-------------instructions-------------------- 3 ---0------------------ 1 -----------2 -----3-----4----- */ // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( title, new GridBagConstraints( 0, 0, 2, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets( 10, 10, 5, 5 ), 0, 0 ) ); contentPane.add( title2, new GridBagConstraints( 2, 0, 2, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 10, 5, 5, 5 ), 0, 0 ) ); contentPane.add( scroller /* contains response */, new GridBagConstraints( 0, 1, 5, 1, 100.0, 100.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets( 5, 10, 5, 10 ), 0, 0 ) ); contentPane.add( plusButton, new GridBagConstraints( 2, 2, 1, 1, 1.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 5, 5, 5 ), 0, 0 ) ); contentPane.add( minusButton, new GridBagConstraints( 3, 2, 1, 1, 1.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 5, 5, 5 ), 0, 0 ) ); contentPane.add( checkButton, new GridBagConstraints( 4, 2, 1, 1, 1.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 5, 5, 10 ), 0, 0 ) ); contentPane.add( progress, /* overlays summary and instructions at bottom */ new GridBagConstraints( 0, 2, 1, 1, 100.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets( 5, 10, 10, 5 ), 0, 0 ) ); contentPane.add( summary, /* bottom left */ new GridBagConstraints( 0, 2, 1, 2, 100.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets( 5, 10, 10, 5 ), 0, 0 ) ); contentPane.add( instructions, /* bottom right */ new GridBagConstraints( 1, 3, 4, 1, 100.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets( 5, 5, 10, 10 ), 0, 0 ) ); } /** * probe one app * * @param group description of which group of apps we are testing * @param rowIndex which row we are working on * @param row row object */ private void probeOneApp( final String group, final int rowIndex, final AppToWatch row ) { tableModel.setState( rowIndex, AppState.CHECKING ); final int frozenRowIndex = rowIndex; SwingUtilities.invokeLater( new Runnable() { public void run() { instructions.setVisible( false ); summary.setVisible( false ); validate(); if ( !userBusyScrolling ) { tableModel.ensureVisible( frozenRowIndex ); } } } ); Thread.yield(); Audio.CLICK.play(); // starting check. final URL url; final String marker; final boolean sni; synchronized ( row ) { url = row.getVersionURL(); marker = row.getMarker(); sni = row.isSNIEnabled(); } if ( url != null && marker != null ) { // read a page from the app's website if ( sni ) { Get.enableSNI(); } else { Get.disableSNI(); } final Get get = new Get(); get.setConnectTimeout( CONNECT_TIMEOUT ); get.setReadTimeout( READ_TIMEOUT ); get.setInstanceFollowRedirects( true /* might be Cloudflare redirect */ ); // <><><><><><><><><><><><><><><><><><><><><><><><><> assert System.getProperty( "jsse.enableSNIExtension" ).equals( "true" ) == sni : "sni property corrupt"; assert System.getProperty( "jdk.tls.ephemeralDHKeySize" ).equals( "2048" ) : "DHKeySize property corrupt"; // this is the guts, check with the website for version change. String result = get.send( url, Get.UTF8 ); int responseCode = get.getResponseCode(); // <><><><><><><><><><><><><><><><><><><><><><><><><> // special kludge for Microsoft that sometimes stalls if it is busy, give in one more try. if ( responseCode >= 200 && result != null && result.indexOf( " 0 ) { try { Thread.sleep( 1000 ); // just once, not a loop } catch ( InterruptedException e ) { // nothing special } result = get.send( url, Get.UTF8 ); responseCode = get.getResponseCode(); } analyseProbeResult( rowIndex, row, url, marker, result, responseCode, get ); } else { // can't do the check. We don't have an URL and marker tableModel.setState( rowIndex, AppState.UNTESTED ); Audio.INCOMPLETE.play(); } togo--; displayToGo( group ); } /** * restore the canned set of apps the way the program was before any data entry. Leave the user's new entries as is, * Revert any modified entries or deleted entries. */ private void restoreDefaultApps() { for ( AppToWatch aDefaultApp : DefaultApps.defaults ) { int rowIndex = tableModel.getRowForApp( aDefaultApp.getAppName() ); if ( rowIndex < 0 ) { // This default does not yet exist, add it at the end. tableModel.add( aDefaultApp ); } else { // already exists, replace with fields from default, but leaving the old status aDefaultApp.setState( tableModel.get( rowIndex ).getState() ); tableModel.set( rowIndex, aDefaultApp ); } } for ( String obsolete : DefaultApps.obsoletes ) { // does not matter if already deleted. tableModel.remove( obsolete ); } tableModel.sort(); } /** * restore the table data from the registry. AllRows is already allocated. Defaults will modify these later. */ @SuppressWarnings( "unchecked" ) private void restorePersisted() { try { tableModel.clear(); if ( persistence.getLong( "serialVersionUID", 0 ) != AppToWatch.serialVersionUID ) { err.println( "Out of date stored state in registry." ); tableModel.clear(); return; } AppToWatch.datePartiallyChecked = ( new BigDate( persistence.getInt( "datePartiallyChecked", BigDate.NULL_ORDINAL ) ) ); AppToWatch.modifiedTimestamp = persistence.getLong( "modifiedTimestamp", Long.MIN_VALUE ); AppToWatch.timestampFullyChecked = persistence.getLong( "timestampFullyChecked", Long.MIN_VALUE ); AppToWatch.timestampPartiallyChecked = persistence.getLong( "timestampPartiallyChecked", Long.MIN_VALUE ); for ( String key : persistedApps.keys() ) { // O P E N registry final byte[] bai = persistedApps.getByteArray( key, null ); if ( bai == null ) { err.println( "existing state corrupted in the registry." ); tableModel.clear(); return; } final ByteArrayInputStream bais = new ByteArrayInputStream( bai ); final ObjectInputStream ois = new ObjectInputStream( bais ); // R E A D tableModel.add( ( AppToWatch ) ois.readObject() ); // C L O S E ois.close(); } // end for } // end try catch ( BackingStoreException e ) { err.println( "Corrupted Preferences keys in registry." ); tableModel.clear(); } catch ( IOException e ) { err.println( "no existing state in the registry to restore " + e.getMessage() ); tableModel.clear(); } catch ( ClassNotFoundException e ) { err.println(); e.printStackTrace( err ); err.println( "corrupted state of registry data." ); err.println(); tableModel.clear(); } } /** * save the table data in the registry. Table is stored in serialised AppToWatch objects. */ private void savePersisted() { if ( asApplication ) { out.println( "saving state..." ); } try { // don't save if some other app or this one has already saved. long lastModifiedTimestamp = persistence.getLong( "modifiedTimestamp", Long.MIN_VALUE ); if ( AppToWatch.modifiedTimestamp < lastModifiedTimestamp ) { err.println( "Another instance of VerCheck is running." ); return; } if ( AppToWatch.modifiedTimestamp == lastModifiedTimestamp ) { // already stored by this instance. return; } // the entire array is too big to write as one persisted field. // So we write app=encoded parms, one line per app // lead section of global parameters persistence.putLong( "serialVersionUID", AppToWatch.serialVersionUID ); persistence.putInt( "datePartiallyChecked", AppToWatch.datePartiallyChecked.getOrdinal() ); persistence.putLong( "modifiedTimestamp", AppToWatch.modifiedTimestamp ); persistence.putLong( "timestampFullyChecked", AppToWatch.timestampFullyChecked ); persistence.putLong( "timestampPartiallyChecked", AppToWatch.timestampPartiallyChecked ); // get rid of ALL previous state persistedApps.clear(); for ( AppToWatch app : tableModel ) { switch ( app.getState() ) { case INVALID: case UNTESTED: // don't try to save junk continue; } // O P E N final ByteArrayOutputStream baos = new ByteArrayOutputStream( SERIALIZED_SIZE ); final ObjectOutputStream oos = new ObjectOutputStream( baos ); // W R I T E oos.writeObject( app ); final byte[] result = baos.toByteArray(); // C L O S E oos.close(); persistedApps.putByteArray( app.getAppName(), result ); } // end for persistedApps.flush(); persistence.flush(); } catch ( IOException e ) { err.println(); e.printStackTrace( err ); err.println( "problem saving state" ); err.println(); } catch ( BackingStoreException e ) { err.println(); e.printStackTrace( err ); err.println( "Cannot save Preferences state." ); err.println(); } // export as HTML as well for inclusion on website export(); } /** * Called by the browser or Applet viewer to inform * this Applet that it is being reclaimed and that it should destroy * any resources that it has allocated. */ public void destroy() { out.println( "destroying..." ); tableModel.destroy(); checkButton = null; contentPane = null; instructions = null; minusButton = null; plusButton = null; progress = null; scroller = null; summary = null; title2 = null; title = null; } /** * Called by the browser or Applet viewer to inform * this Applet that it has been loaded into the system. */ @Override public void init() { out.println( "initing..." ); Container contentPane = this.getContentPane(); if ( !VersionCheck.isJavaVersionOK( 1, 7, 0, contentPane ) ) { // effectively abort return; } buildMenu(); // also initial L&F tableModel = new VerCheckTableModel(); tableModel.init(); buildComponents(); layoutFields(); this.validate(); this.setVisible( true ); } /** * check the state of every application in the model. Run as separate thread. */ public void run() { SwingUtilities.invokeLater( new Runnable() { public void run() { disableButtons(); } } ); // probe all apps to see if version has changed, Do it in passes to see most interesting stuff first. // We don't check EMPTIES Get.disableSNI(); checkAllApps( "troublesome", false, EnumSet.of( AppState.RECENTLY_CHANGED, AppState.UNTESTED, AppState.NO_CONNECTION, AppState.INVALID, AppState.CHECKING ) ); if ( !isRecentlyChecked ) { /* bypass stable apps if checked them already recently. */ checkAllApps( "untested", false, EnumSet.of( AppState.UNCHANGED_RELEASED_IN_LAST_WEEK, AppState.UNCHANGED_RELEASED_IN_LAST_MONTH, AppState.UNCHANGED_RELEASED_MORE_THAN_A_MONTH_AGO, AppState.UNCHANGED_RELEASED_MORE_THAN_A_YEAR_AGO ) ); } checkAllApps( "no connection", false, EnumSet.of( AppState.NO_CONNECTION ) ); // brokens a second time Get.enableSNI(); checkAllApps( "troublesome sni", true, EnumSet.of( AppState.RECENTLY_CHANGED, AppState.UNTESTED, AppState.NO_CONNECTION, AppState.INVALID, AppState.CHECKING ) ); if ( !isRecentlyChecked ) { /* bypass stable apps if checked them already recently. */ checkAllApps( "untested sni", true, EnumSet.of( AppState.UNCHANGED_RELEASED_IN_LAST_WEEK, AppState.UNCHANGED_RELEASED_IN_LAST_MONTH, AppState.UNCHANGED_RELEASED_MORE_THAN_A_MONTH_AGO, AppState.UNCHANGED_RELEASED_MORE_THAN_A_YEAR_AGO ) ); } checkAllApps( "no connection sni", true, EnumSet.of( AppState.NO_CONNECTION ) ); // brokens a second time final int rowCount = tableModel.getRowCount(); // save results away in case app later crashes savePersisted(); // completed all apps, put things back the way they were. final int frozenRowIndex = rowCount - 1; SwingUtilities.invokeLater( new Runnable() { public void run() { progress.setVisible( false ); instructions.setVisible( true ); final int rows = tableModel.getRowCount(); int broken = 0; int changed = 0; int invalid = 0; int unknown = 0; for ( int i = 0; i < rows; i++ ) { switch ( tableModel.getState( i ) ) { case RECENTLY_CHANGED: changed++; break; case NO_CONNECTION: broken++; break; case INVALID: invalid++; break; case UNTESTED: unknown++; break; default: } } StringBuilder sb = new StringBuilder( 300 ); sb.append( rows ); sb.append( " applications last checked: " ); sb.append( AppToWatch.getTimestampCheckedString() ); if ( changed != 0 ) { sb.append( "\n" ); sb.append( changed ); sb.append( " recently changed" ); } if ( broken != 0 ) { sb.append( "\n" ); sb.append( broken ); sb.append( " no connection" ); } if ( invalid != 0 ) { sb.append( "\n" ); sb.append( invalid ); sb.append( " invalid" ); } if ( unknown != 0 ) { sb.append( "\n" ); sb.append( unknown ); sb.append( " untested" ); } if ( changed + invalid + unknown > 0 ) { sb.append( "\nmanual correction needed" ); /* hearing this sound also implies persistence complete. */ Audio.INCOMPLETE.play(); HybridJ.setRetCode( 1 ); // provisional } else { /* broken does not count as incomplete, no work required on user's part */ /* hearing this sound also implies persistence complete. */ sb.append( "\ndone" ); Audio.DONE.play(); HybridJ.setRetCode( 0 ); } summary.setForeground( FOREGROUND_FOR_FRESH_SUMMARY ); summary.setBackground( BACKGROUND_FOR_FRESH_SUMMARY ); summary.setText( sb.toString() ); summary.setVisible( true ); enableButtons(); validate(); // changing instructions changes amount of room for table. if ( !userBusyScrolling ) { ensureImportantVisible(); } } } ); } /** * usual Applet start, run after init. */ public void start() { out.println( "starting..." ); restorePersisted(); // override anything in the persisted state with latest settings in the Applet itself. restoreDefaultApps(); instructions.setText( INSTRUCTIONS ); // will use stale colours summary.setText( "Last checked: " + AppToWatch.getTimestampCheckedString() ); } /** * usual Applet stop, run before destroy. */ public void stop() { out.println( "stopping..." ); // will save edits since last check savePersisted(); } }