/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.plugin.update; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import java.util.List; // disambiguation import javax.swing.*; import javax.swing.text.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.httputil.*; import net.java.sip.communicator.service.resources.*; import net.java.sip.communicator.service.update.UpdateService; import net.java.sip.communicator.service.version.*; import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.swing.*; /** * Implements checking for software updates, downloading and applying them i.e. * the very logic of the update plug-in. * * @author Damian Minkov * @author Lyubomir Marinov */ public class Update implements UpdateService { /** * The Logger used by the Update class for logging output. */ private static final Logger logger = Logger.getLogger(Update.class); /** * The name of the property which specifies the update link in the * configuration file. */ private static final String PROP_UPDATE_LINK = "net.java.sip.communicator.UPDATE_LINK"; /** * The link pointing to the ChangeLog of the update. */ private static String changesLink; /** * The JDialog, if any, which is associated with the currently * executing "Check for Updates". While the "Check for Updates" * functionality cannot be entered, clicking the "Check for Updates" menu * item will bring it to the front. */ private static JDialog checkForUpdatesDialog; /** * The link pointing at the download of the update. */ private static String downloadLink; /** * The indicator/counter which determines how many methods are currently * executing the "Check for Updates" functionality so that it is known * whether it can be entered. */ private static int inCheckForUpdates = 0; /** * The latest version of the software found at the configured update * location. */ private static String latestVersion; /** * Invokes "Check for Updates". * * @param notifyAboutNewestVersion true if the user is to be * notified if they have the newest version already; otherwise, * false */ public synchronized void checkForUpdates( final boolean notifyAboutNewestVersion) { if (inCheckForUpdates > 0) { if (checkForUpdatesDialog != null) checkForUpdatesDialog.toFront(); return; } Thread checkForUpdatesThread = new Thread() { @Override public void run() { try { if(isLatestVersion()) { if(notifyAboutNewestVersion) { ResourceManagementService resources = Resources.getResources(); UpdateActivator.getUIService() .getPopupDialog() .showMessagePopupDialog( resources.getI18NString( "plugin.updatechecker.DIALOG_NOUPDATE"), resources.getI18NString( "plugin.updatechecker.DIALOG_NOUPDATE_TITLE"), PopupDialog.INFORMATION_MESSAGE); } } else if (OSUtils.IS_WINDOWS) showWindowsNewVersionAvailableDialog(); else showGenericNewVersionAvailableDialog(); } finally { exitCheckForUpdates(null); } } }; checkForUpdatesThread.setDaemon(true); enterCheckForUpdates(null); try { checkForUpdatesThread.start(); checkForUpdatesThread = null; } finally { if (checkForUpdatesThread != null) exitCheckForUpdates(null); } } /** * Tries to create a new FileOutputStream for a temporary file into * which the setup is to be downloaded. Because temporary files generally * have random characters in their names and the name of the setup may be * shown to the user, first tries to use the name of the URL to be * downloaded because it likely is prettier. * * @param url the URL of the file to be downloaded * @param extension the extension of the File to be created or * null for the default (which may be derived from url) * @param dryRun true to generate a File in * tempFile and not open it or false to generate a * File in tempFile and open it * @param tempFile a File array of at least one element which is to * receive the created File instance at index zero (if successful) * @return the newly created FileOutputStream * @throws IOException if anything goes wrong while creating the new * FileOutputStream */ private static FileOutputStream createTempFileOutputStream( URL url, String extension, boolean dryRun, File[] tempFile) throws IOException { /* * Try to use the name from the URL because it isn't a "randomly" * generated one. */ String path = url.getPath(); File tf = null; FileOutputStream tfos = null; if ((path != null) && (path.length() != 0)) { int nameBeginIndex =path.lastIndexOf('/'); String name; if (nameBeginIndex > 0) { name = path.substring(nameBeginIndex + 1); nameBeginIndex = name.lastIndexOf('\\'); if (nameBeginIndex > 0) name = name.substring(nameBeginIndex + 1); } else name = path; /* * Make sure the extension of the name is EXE so that we're able to * execute it later on. */ int nameLength = name.length(); if (nameLength != 0) { int baseNameEnd = name.lastIndexOf('.'); if (extension == null) extension = ".exe"; if (baseNameEnd == -1) name += extension; else if (baseNameEnd == 0) { if (!extension.equalsIgnoreCase(name)) name += extension; } else name = name.substring(0, baseNameEnd) + extension; try { String tempDir = System.getProperty("java.io.tmpdir"); if ((tempDir != null) && (tempDir.length() != 0)) { tf = new File(tempDir, name); if (!dryRun) tfos = new FileOutputStream(tf); } } catch (FileNotFoundException fnfe) { // Ignore it because we'll try File#createTempFile(). } catch (SecurityException se) { // Ignore it because we'll try File#createTempFile(). } } } // Well, we couldn't use a pretty name so try File#createTempFile(). if ((tfos == null) && !dryRun) { tf = File.createTempFile("setup", ".exe"); tfos = new FileOutputStream(tf); } tempFile[0] = tf; return tfos; } /** * Downloads a remote file specified by its URL into a local file. * * @param url the URL of the remote file to download * @return the local File into which url has been * downloaded or null if there was no response from the * url * @throws IOException if an I/O error occurs during the download */ private static File download(String url) throws IOException { final File[] tempFile = new File[1]; FileOutputStream tempFileOutputStream = null; boolean deleteTempFile = true; tempFileOutputStream = createTempFileOutputStream( new URL(url), /* * The default extension, possibly derived from url, is * fine. Besides, we do not really have information about * any preference. */ null, /* Do create a FileOutputStream. */ false, tempFile); try { HttpUtils.HTTPResponseResult res = HttpUtils.openURLConnection(url); if (res != null) { InputStream content = res.getContent(); // Track the progress of the download. ProgressMonitorInputStream input = new ProgressMonitorInputStream(null, url, content); /* * Set the maximum value of the ProgressMonitor to the size of * the file to download. */ input.getProgressMonitor().setMaximum( (int) res.getContentLength()); try { final BufferedOutputStream output = new BufferedOutputStream(tempFileOutputStream); try { int read = -1; byte[] buff = new byte[1024]; while((read = input.read(buff)) != -1) output.write(buff, 0, read); } finally { output.close(); tempFileOutputStream = null; } deleteTempFile = false; } finally { try { input.close(); } catch (IOException ioe) { /* * Ignore it because we've already downloaded the setup * and that's what matters most. */ } } } } finally { try { if (tempFileOutputStream != null) tempFileOutputStream.close(); } finally { if (deleteTempFile && (tempFile[0] != null)) { tempFile[0].delete(); tempFile[0] = null; } } } return tempFile[0]; } /** * Notifies this UpdateCheckActivator that a method is entering the * "Check for Updates" functionality and it is thus not allowed to enter it * again. * * @param checkForUpdatesDialog the JDialog associated with the * entry in the "Check for Updates" functionality if any. While "Check for * Updates" cannot be entered again, clicking the "Check for Updates" menu * item will bring the checkForUpdatesDialog to the front. */ private static synchronized void enterCheckForUpdates( JDialog checkForUpdatesDialog) { inCheckForUpdates++; if (checkForUpdatesDialog != null) Update.checkForUpdatesDialog = checkForUpdatesDialog; } /** * Notifies this UpdateCheckActivator that a method is exiting the * "Check for Updates" functionality and it may thus be allowed to enter it * again. * * @param checkForUpdatesDialog the JDialog which was associated * with the matching call to {@link #enterCheckForUpdates(JDialog)} if any */ private static synchronized void exitCheckForUpdates( JDialog checkForUpdatesDialog) { if (inCheckForUpdates == 0) throw new IllegalStateException("inCheckForUpdates"); else { inCheckForUpdates--; if ((checkForUpdatesDialog != null) && (Update.checkForUpdatesDialog == checkForUpdatesDialog)) Update.checkForUpdatesDialog = null; } } /** * Gets the current (software) version. * * @return the current (software) version */ private static Version getCurrentVersion() { VersionService verService = ServiceUtils.getService( UpdateActivator.bundleContext, VersionService.class); return verService.getCurrentVersion(); } /** * Determines whether we are currently running the latest version. * * @return true if we are currently running the latest version; * otherwise, false */ private static boolean isLatestVersion() { try { String updateLink = UpdateActivator.getConfiguration().getString( PROP_UPDATE_LINK); if(updateLink == null) { updateLink = Resources.getUpdateConfigurationString("update_link"); } if(updateLink == null) { if (logger.isDebugEnabled()) logger.debug( "Updates are disabled, faking latest version."); } else { HttpUtils.HTTPResponseResult res = HttpUtils.openURLConnection(updateLink); if (res != null) { InputStream in = null; Properties props = new Properties(); try { in = res.getContent(); props.load(in); } finally { in.close(); } latestVersion = props.getProperty("last_version"); downloadLink = props.getProperty("download_link"); changesLink = updateLink.substring( 0, updateLink.lastIndexOf("/") + 1) + props.getProperty("changes_html"); return latestVersion.compareTo( getCurrentVersion().toString()) <= 0; } } } catch (Exception e) { logger.warn( "Could not retrieve latest version or compare it to current" + " version", e); /* * If we get an exception, then we will return that the current * version is the newest one in order to prevent opening the dialog * notifying about the availability of a new version. */ } return true; } /** * Shows dialog informing about the availability of a new version with a * Download button which launches the system Web browser. */ private static void showGenericNewVersionAvailableDialog() { /* * Before showing the dialog, we'll enterCheckForUpdates() in order to * notify that it is not safe to enter "Check for Updates" again. If we * don't manage to show the dialog, we'll have to exitCheckForUpdates(). * If we manage though, we'll have to exitCheckForUpdates() but only * once depending on its modality. */ final boolean[] exitCheckForUpdates = new boolean[] { false }; final JDialog dialog = new SIPCommDialog() { private static final long serialVersionUID = 0L; protected void close(boolean escaped) { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0]) exitCheckForUpdates(this); } } }; ResourceManagementService resources = Resources.getResources(); dialog.setTitle( resources.getI18NString("plugin.updatechecker.DIALOG_TITLE")); JEditorPane contentMessage = new JEditorPane(); contentMessage.setContentType("text/html"); contentMessage.setOpaque(false); contentMessage.setEditable(false); String dialogMsg = resources.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE", new String[] { resources.getSettingsString( "service.gui.APPLICATION_NAME") }); if(latestVersion != null) dialogMsg += resources.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE_2", new String[] { resources.getSettingsString( "service.gui.APPLICATION_NAME"), latestVersion }); contentMessage.setText(dialogMsg); JPanel contentPane = new TransparentPanel(new BorderLayout(5,5)); contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPane.add(contentMessage, BorderLayout.CENTER); JPanel buttonPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); final JButton closeButton = new JButton( resources.getI18NString( "plugin.updatechecker.BUTTON_CLOSE")); closeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dialog.dispose(); if (exitCheckForUpdates[0]) exitCheckForUpdates(dialog); } }); if(downloadLink != null) { JButton downloadButton = new JButton( resources.getI18NString( "plugin.updatechecker.BUTTON_DOWNLOAD")); downloadButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(OSUtils.IS_LINUX64) downloadLink = downloadLink.replace("i386", "amd64"); UpdateActivator.getBrowserLauncher().openURL(downloadLink); /* * Do the same as the Close button in order to not duplicate * the code. */ closeButton.doClick(); } }); buttonPanel.add(downloadButton); } buttonPanel.add(closeButton); contentPane.add(buttonPanel, BorderLayout.SOUTH); dialog.setContentPane(contentPane); dialog.pack(); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); dialog.setLocation( screenSize.width/2 - dialog.getWidth()/2, screenSize.height/2 - dialog.getHeight()/2); synchronized (exitCheckForUpdates) { enterCheckForUpdates(dialog); exitCheckForUpdates[0] = true; } try { dialog.setVisible(true); } finally { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0] && dialog.isModal()) exitCheckForUpdates(dialog); } } } /** * Shows dialog informing about new version with button Install * which triggers the update process. */ private static void showWindowsNewVersionAvailableDialog() { /* * Before showing the dialog, we'll enterCheckForUpdates() in order to * notify that it is not safe to enter "Check for Updates" again. If we * don't manage to show the dialog, we'll have to exitCheckForUpdates(). * If we manage though, we'll have to exitCheckForUpdates() but only * once depending on its modality. */ final boolean[] exitCheckForUpdates = new boolean[] { false }; final JDialog dialog = new SIPCommDialog() { private static final long serialVersionUID = 0L; protected void close(boolean escaped) { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0]) exitCheckForUpdates(this); } } }; ResourceManagementService resources = Resources.getResources(); dialog.setTitle( resources.getI18NString("plugin.updatechecker.DIALOG_TITLE")); JEditorPane contentMessage = new JEditorPane(); contentMessage.setContentType("text/html"); contentMessage.setOpaque(false); contentMessage.setEditable(false); /* * Use the font of the dialog because contentMessage is just like a * label. */ contentMessage.putClientProperty( JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); String dialogMsg = resources.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE", new String[] { resources.getSettingsString( "service.gui.APPLICATION_NAME") }); if(latestVersion != null) { dialogMsg += resources.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE_2", new String[] { resources.getSettingsString( "service.gui.APPLICATION_NAME"), latestVersion }); } contentMessage.setText(dialogMsg); JPanel contentPane = new SIPCommFrame.MainContentPane(); contentMessage.setBorder(BorderFactory.createEmptyBorder(10, 0, 20, 0)); contentPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); contentPane.add(contentMessage, BorderLayout.NORTH); JScrollPane scrollChanges = new JScrollPane(); scrollChanges.setPreferredSize(new Dimension(550, 200)); JEditorPane changesHtml = new JEditorPane(); changesHtml.setContentType("text/html"); changesHtml.setEditable(false); changesHtml.setBorder(BorderFactory.createLoweredBevelBorder()); scrollChanges.setViewportView(changesHtml); contentPane.add(scrollChanges, BorderLayout.CENTER); try { Document changesHtmlDocument = changesHtml.getDocument(); if (changesHtmlDocument instanceof AbstractDocument) { ((AbstractDocument) changesHtmlDocument) .setAsynchronousLoadPriority(0); } changesHtml.setPage(new URL(changesLink)); } catch (Exception e) { logger.error("Cannot set changes Page", e); } JPanel buttonPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); final JButton closeButton = new JButton( resources.getI18NString( "plugin.updatechecker.BUTTON_CLOSE")); closeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dialog.dispose(); if (exitCheckForUpdates[0]) exitCheckForUpdates(dialog); } }); if(downloadLink != null) { JButton installButton = new JButton( resources.getI18NString( "plugin.updatechecker.BUTTON_INSTALL")); installButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(OSUtils.IS_WINDOWS64) downloadLink = downloadLink.replace("x86", "x64"); enterCheckForUpdates(null); try { /* * Do the same as the Close button in order to not * duplicate the code. */ closeButton.doClick(); } finally { boolean windowsUpdateThreadHasStarted = false; try { new Thread() { @Override public void run() { try { windowsUpdate(); } finally { exitCheckForUpdates(null); } } }.start(); windowsUpdateThreadHasStarted = true; } finally { if (!windowsUpdateThreadHasStarted) exitCheckForUpdates(null); } } } }); buttonPanel.add(installButton); } buttonPanel.add(closeButton); contentPane.add(buttonPanel, BorderLayout.SOUTH); dialog.setContentPane(contentPane); dialog.pack(); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); dialog.setLocation( screenSize.width/2 - dialog.getWidth()/2, screenSize.height/2 - dialog.getHeight()/2); synchronized (exitCheckForUpdates) { enterCheckForUpdates(dialog); exitCheckForUpdates[0] = true; } try { dialog.setVisible(true); } finally { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0] && dialog.isModal()) exitCheckForUpdates(dialog); } } } /** * Implements the very update procedure on Windows which includes without * being limited to: *
    *
  1. Downloads the setup in a temporary directory.
  2. *
  3. Warns that the update procedure will shut down the application.
  4. *
  5. Executes the setup in a separate process and shuts down the * application.
  6. *
*/ private static void windowsUpdate() { /* * Firstly, try a delta update which contains a bspatch file to be used * to reconstruct the latest MSI from the locally-cached one. If it * fails, fall back to a full update. */ File delta = null; boolean deleteDelta = true; File msi = null; try { String deltaTarget = null; Version ver = getCurrentVersion(); if(ver.isPreRelease()) deltaTarget = ver.getNightlyBuildID(); else deltaTarget = String.valueOf(ver.getVersionRevision()); String deltaLink = downloadLink.replace( latestVersion, latestVersion + "-delta-" + deltaTarget); /* * TODO Download the delta update regardless of the logging level * once the generation of delta updates is implemented and the whole * functionality performs satisfactory. */ if (!deltaLink.equalsIgnoreCase(downloadLink)) delta = download(deltaLink); if (delta != null) { File[] deltaMsi = new File[1]; createTempFileOutputStream( delta.toURI().toURL(), ".msi", /* * Do not actually create a FileOutputStream, we just * want the File (name). */ true, deltaMsi); Process process = new ProcessBuilder( delta.getCanonicalPath(), "--quiet", deltaMsi[0].getCanonicalPath()) .start(); int exitCode = 1; while (true) { try { exitCode = process.waitFor(); break; } catch (InterruptedException ie) { /* * Ignore it, we're interested in the exit code of the * process. */ } } if (0 == exitCode) { deleteDelta = false; msi = deltaMsi[0]; } } } catch (Exception e) { /* Ignore it, we'll try the full update. */ } finally { if (deleteDelta && (delta != null)) { delta.delete(); delta = null; } } /* * Secondly, either apply the delta update or download and apply a full * update. */ boolean deleteMsi = true; deleteDelta = true; try { if (msi == null) msi = download(downloadLink); if (msi != null) { ResourceManagementService resources = Resources.getResources(); if(UpdateActivator.getUIService() .getPopupDialog().showConfirmPopupDialog( resources.getI18NString( "plugin.updatechecker.DIALOG_WARN"), resources.getI18NString( "plugin.updatechecker.DIALOG_TITLE"), PopupDialog.YES_NO_OPTION, PopupDialog.QUESTION_MESSAGE) == PopupDialog.YES_OPTION) { List command = new ArrayList(); /* * If a delta update is in effect, the delta will execute * the latest MSI it has previously recreated from the * locally-cached MSI. Otherwise, a full update is in effect * and it will just execute itself. */ command.add( ((delta == null) ? msi : delta).getCanonicalPath()); command.add("--wait-parent"); if (delta != null) { command.add("--msiexec"); command.add(msi.getCanonicalPath()); } command.add( "SIP_COMMUNICATOR_AUTOUPDATE_INSTALLDIR=\"" + System.getProperty("user.dir") + "\""); deleteMsi = false; deleteDelta = false; /* * The setup has been downloaded. Now start it and shut * down. */ new ProcessBuilder(command).start(); UpdateActivator.getShutdownService().beginShutdown(); } } } catch(FileNotFoundException fnfe) { ResourceManagementService resources = Resources.getResources(); UpdateActivator.getUIService() .getPopupDialog().showMessagePopupDialog( resources.getI18NString( "plugin.updatechecker.DIALOG_MISSING_UPDATE"), resources.getI18NString( "plugin.updatechecker.DIALOG_NOUPDATE_TITLE"), PopupDialog.INFORMATION_MESSAGE); } catch (Exception e) { if (logger.isInfoEnabled()) logger.info("Could not update", e); } finally { /* * If we've failed, delete the temporary file into which the setup * was supposed to be or has already been downloaded. */ if (deleteMsi && (msi != null)) { msi.delete(); msi = null; } if (deleteDelta && (delta != null)) { delta.delete(); delta = null; } } } }