/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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; import javax.swing.*; import javax.swing.text.*; import net.java.sip.communicator.plugin.desktoputil.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.httputil.*; import net.java.sip.communicator.service.update.*; import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.Logger; import org.jitsi.service.resources.*; import org.jitsi.service.version.*; import org.jitsi.util.*; /** * 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 UpdateServiceImpl implements UpdateService { /** * 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; /** * The Logger used by the UpdateServiceImpl class and its * instances for logging output. */ private static final Logger logger = Logger.getLogger(UpdateServiceImpl.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"; /** * Initializes a new Web browser Component instance and navigates * it to a specific URL. * * @param url the URL to navigate the new Web browser Component * instance * @return the new Web browser Component instance which has been * navigated to the specified url */ private static Component createBrowser(String url) { // Initialize the user interface. JEditorPane editorPane = new JEditorPane(); editorPane.setContentType("text/html"); editorPane.setEditable(false); JScrollPane scrollPane = new JScrollPane(); scrollPane.setViewportView(editorPane); // Navigate the user interface to the specified URL. try { Document document = editorPane.getDocument(); if (document instanceof AbstractDocument) ((AbstractDocument) document).setAsynchronousLoadPriority(0); editorPane.setPage(new URL(url)); } catch (Throwable t) { if (t instanceof ThreadDeath) throw (ThreadDeath) t; else { logger.error( "Failed to navigate the Web browser to: " + url, t); } } return scrollPane; } /** * 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) UpdateServiceImpl.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) && (UpdateServiceImpl.checkForUpdatesDialog == checkForUpdatesDialog)) UpdateServiceImpl.checkForUpdatesDialog = null; } } /** * Gets the current (software) version. * * @return the current (software) version */ private static Version getCurrentVersion() { return getVersionService().getCurrentVersion(); } /** * Returns the currently registered instance of version service. * @return the current version service. */ private static VersionService getVersionService() { return ServiceUtils.getService( UpdateActivator.bundleContext, VersionService.class); } /** * 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"); /* * Make sure that download_link points to the architecture * of the running application. */ if (downloadLink != null) { if (OSUtils.IS_LINUX32) { downloadLink = downloadLink.replace("amd64", "i386"); } else if (OSUtils.IS_LINUX64) { downloadLink = downloadLink.replace("i386", "amd64"); } else if (OSUtils.IS_WINDOWS32) { downloadLink = downloadLink.replace("x64", "x86"); } else if (OSUtils.IS_WINDOWS64) { downloadLink = downloadLink.replace("x86", "x64"); } } changesLink = updateLink.substring( 0, updateLink.lastIndexOf("/") + 1) + props.getProperty("changes_html"); try { VersionService versionService = getVersionService(); Version latestVersionObj = versionService.parseVersionString(latestVersion); if(latestVersionObj != null) return latestVersionObj.compareTo( getCurrentVersion()) <= 0; else logger.error("Version obj not parsed(" + latestVersion + ")"); } catch(Throwable t) { logger.error("Error parsing version string", t); } // fallback to lexicographically compare // of version strings in case of an error 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; } /** * Runs in a daemon/background Thread dedicated to checking whether * a new version of the application is available and notifying the user * about the result of the check. * * @param notifyAboutNewestVersion true to notify the user in case * she is running the newest/latest version available already; otherwise, * false */ private static void runInCheckForUpdatesThread( boolean notifyAboutNewestVersion) { if(isLatestVersion()) { if(notifyAboutNewestVersion) { SwingUtilities.invokeLater( new Runnable() { public void run() { UIService ui = UpdateActivator.getUIService(); ResourceManagementService r = Resources.getResources(); ui.getPopupDialog().showMessagePopupDialog( r.getI18NString( "plugin.updatechecker.DIALOG_NOUPDATE"), r.getI18NString( "plugin.updatechecker.DIALOG_NOUPDATE_TITLE"), PopupDialog.INFORMATION_MESSAGE); } }); } } else { SwingUtilities.invokeLater( new Runnable() { public void run() { if (OSUtils.IS_WINDOWS) showWindowsNewVersionAvailableDialog(); else showGenericNewVersionAvailableDialog(); } }); } } /** * 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; @Override 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) { 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 }; @SuppressWarnings("serial") final JDialog dialog = new SIPCommDialog() { @Override protected void close(boolean escaped) { synchronized (exitCheckForUpdates) { if (exitCheckForUpdates[0]) exitCheckForUpdates(this); } } }; ResourceManagementService r = Resources.getResources(); dialog.setTitle(r.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 = r.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE", new String[] { r.getSettingsString( "service.gui.APPLICATION_NAME") }); if(latestVersion != null) { dialogMsg += r.getI18NString( "plugin.updatechecker.DIALOG_MESSAGE_2", new String[] { r.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); Component browser = createBrowser(changesLink); if (browser != null) { browser.setPreferredSize(new Dimension(550, 200)); contentPane.add(browser, BorderLayout.CENTER); } JPanel buttonPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); final JButton closeButton = new JButton( r.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( r.getI18NString("plugin.updatechecker.BUTTON_INSTALL")); installButton.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { 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.isNightly()) deltaTarget = ver.getNightlyBuildID(); else deltaTarget = ver.toString(); String deltaLink = downloadLink.replace( latestVersion, latestVersion + "-delta-" + deltaTarget); 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", new String[]{ resources.getSettingsString( "service.gui.APPLICATION_NAME") }), 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; } } } /** * 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 { runInCheckForUpdatesThread(notifyAboutNewestVersion); } finally { exitCheckForUpdates(null); } } }; checkForUpdatesThread.setDaemon(true); checkForUpdatesThread.setName( getClass().getName() + ".checkForUpdates"); enterCheckForUpdates(null); try { checkForUpdatesThread.start(); checkForUpdatesThread = null; } finally { if (checkForUpdatesThread != null) exitCheckForUpdates(null); } } }