/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
*
* 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 com.google.dart.tools.update.core.internal.jobs;
import com.google.dart.engine.sdk.DirectoryBasedDartSdk;
import com.google.dart.server.AnalysisServerStatusListener;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.core.DartCoreDebug;
import com.google.dart.tools.core.dart2js.ProcessRunner;
import com.google.dart.tools.core.model.DartSdkManager;
import com.google.dart.tools.update.core.Revision;
import com.google.dart.tools.update.core.UpdateCore;
import com.google.dart.tools.update.core.UpdateManager;
import com.google.dart.tools.update.core.internal.UpdateUtils;
import org.eclipse.core.runtime.AssertionFailedException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* An action that installs an available Dart Editor update.
*/
public class InstallUpdateAction extends Action {
/**
* Internal representation of an executable file that needs to be renamed before update and
* cleaned up after update.
*/
private static class Executable {
static void add(List<Executable> list, String name, File executable) {
if (executable != null) {
list.add(new Executable(name, executable));
}
}
private final String name;
private final File executable;
private final File oldExecutable;
Executable(String name, File executable) {
this.name = name;
this.executable = executable;
this.oldExecutable = new File(executable.getAbsolutePath() + ".old");
}
boolean deleteOld() {
return !oldExecutable.exists() || oldExecutable.delete();
}
String getExistingProcessMessage() {
return "Update complete, but existing " + name + " process still running.\n\n"
+ oldExecutable.getAbsolutePath();
}
String getRenameFailedMessage() {
return "Could not update " + name + ". Please terminate any running\n" + name
+ " processes, check the file permissions, and try again.\n\n"
+ executable.getAbsolutePath() + "\n" + oldExecutable.getAbsolutePath();
}
boolean rename() {
return !executable.exists() || (deleteOld() && executable.renameTo(oldExecutable));
}
void restore() {
oldExecutable.renameTo(executable);
}
}
private static class RetryUpdateDialog extends MessageDialog {
public RetryUpdateDialog(Shell parentShell) {
super(
parentShell,
UpdateJobMessages.InstallUpdateAction_bad_zip_dialog_title,
null,
UpdateJobMessages.InstallUpdateAction_bad_zip_dialog_msg,
MessageDialog.QUESTION,
new String[] {
UpdateJobMessages.InstallUpdateAction_bad_zip_retry_confirm,
UpdateJobMessages.InstallUpdateAction_bad_zip_dialog_cancel},
0);
}
}
private static final String SLASH = System.getProperty("file.separator");
private static final String INSTALL_SCRIPT = "install.py";
private static final String PROP_EXIT_CODE = "eclipse.exitcode"; //$NON-NLS-1$
private static final String PROP_EXIT_DATA = "eclipse.exitdata"; //$NON-NLS-1$
private static final String PROP_VM = "eclipse.vm"; //$NON-NLS-1$
private static final String PROP_COMMANDS = "eclipse.commands"; //$NON-NLS-1$
private static final String PROP_VMARGS = "eclipse.vmargs"; //$NON-NLS-1$
private static final String CMD_VMARGS = "-vmargs"; //$NON-NLS-1$
private static final String NEW_LINE = System.getProperty("line.separator", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
private final UpdateManager updateManager;
private static final FileFilter UPDATE_OVERRIDE_FILTER = new FileFilter() {
/**
* Returns <code>true</code> if this file should be overwritten, <code>false</code> otherwise
*/
@Override
public boolean accept(File file) {
String name = file.getName();
//org.eclipse.equinox.simpleconfigurator/bundles.info
if (name.equals("bundles.info")) { //$NON-NLS-1$
return true;
}
// DartEditor.app/Contents/MacOS/DartEditor.ini
if (name.equals("DartEditor.ini")) { //$NON-NLS-1$
//mac INI files need to be overwritten since they're signed
if (DartCore.isMac()) {
return true;
}
//on linux and windows, we handle a merge post-copy
return false;
}
if (name.equals("editor.properties")) { //$NON-NLS-1$
return false;
}
return false;
}
};
/**
* Create an instance.
*/
public InstallUpdateAction(UpdateManager updateManager) {
this.updateManager = updateManager;
}
@Override
public void run() {
if (resourcesNeedSaving()) {
//prompt to save dirty editors
if (!MessageDialog.openConfirm(
getShell(),
UpdateJobMessages.InstallUpdateAction_confirm_save_title,
UpdateJobMessages.InstallUpdateAction_confirm_save_msg)) {
return;
}
//attempt to close dirty editors
if (!PlatformUI.getWorkbench().saveAllEditors(false)) {
MessageDialog.openError(
getShell(),
UpdateJobMessages.InstallUpdateAction_errorTitle,
UpdateJobMessages.InstallUpdateAction_error_in_save);
return;
}
}
boolean isTerminated = true;
String failMsg = "";
final CountDownLatch latch = new CountDownLatch(1);
if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) {
AnalysisServerStatusListener listener = new AnalysisServerStatusListener() {
@Override
public void isAliveServer(boolean isAlive) {
if (!isAlive) {
latch.countDown();
}
}
};
DartCore.getAnalysisServer().addStatusListener(listener);
DartCore.getAnalysisServer().server_shutdown();
try {
if (!latch.await(3, TimeUnit.SECONDS)) {
failMsg = "Unable to shutdown Analysis Server";
isTerminated = false;
}
} catch (InterruptedException e) {
// do nothing
}
}
DirectoryBasedDartSdk sdk = DartSdkManager.getManager().getSdk();
List<Executable> executables = new ArrayList<Executable>();
// check if executables are updated only when sdk in use is the one
// in the editor install directory
if (DartSdkManager.getManager().isDefaultSdk()) {
Executable.add(executables, "Dart VM", sdk.getVmExecutable());
Executable.add(executables, "Dartium", DartSdkManager.getManager().getDartiumExecutable());
}
int index = 0;
while (index < executables.size()) {
if (!executables.get(index).rename()) {
Executable failedRename = executables.get(index);
--index;
while (index >= 0) {
executables.get(index).restore();
--index;
}
isTerminated = false;
failMsg = failedRename.getRenameFailedMessage();
break;
}
++index;
}
if (!isTerminated) {
MessageDialog.openError(getShell(), UpdateJobMessages.InstallUpdateAction_errorTitle, failMsg);
return;
}
try {
if (applyUpdate()) {
for (Executable executable : executables) {
if (!executable.deleteOld()) {
MessageDialog.openError(
getShell(),
UpdateJobMessages.InstallUpdateAction_errorTitle,
executable.getExistingProcessMessage());
}
}
restart();
}
} catch (Throwable th) {
UpdateCore.logError(th);
MessageDialog.openError(
getShell(),
UpdateJobMessages.InstallUpdateAction_errorTitle,
UpdateJobMessages.InstallUpdateAction_errorMessage);
}
}
private boolean applyUpdate() throws InvocationTargetException, InterruptedException {
final boolean result[] = new boolean[1];
new ProgressMonitorDialog(getShell()) {
@Override
protected void configureShell(Shell shell) {
shell.setText(UpdateJobMessages.InstallUpdateAction_progress_mon_title);
}
}.run(true, false, new IRunnableWithProgress() {
@Override
public void run(IProgressMonitor monitor) throws InvocationTargetException,
InterruptedException {
try {
result[0] = doApplyUpdate(monitor);
} catch (IOException e) {
throw new InvocationTargetException(e);
}
}
});
return result[0];
}
//TODO (pquitslund): this step may be unnecessary if writing bundles.info suffices
private String buildCommandLine() {
String property = System.getProperty(PROP_VM);
if (property == null) {
throw new AssertionFailedException("System property \"" + PROP_VM + "\" not set"); //$NON-NLS-1$ //$NON-NLS-2$
}
StringBuffer result = new StringBuffer(512);
result.append(property);
result.append(NEW_LINE);
// append the vmargs and commands. Assume that these already end in \n
String vmargs = System.getProperty(PROP_VMARGS);
if (vmargs != null) {
result.append(vmargs);
}
//TODO (pquitslund): where does this really belong?
result.append("-Declipse.refreshBundles=true"); //$NON-NLS-1$
result.append(NEW_LINE);
property = System.getProperty(PROP_COMMANDS);
if (property != null) {
result.append(property);
}
// put the vmargs back at the very end (the eclipse.commands property
// already contains the -vm arg)
if (vmargs != null) {
result.append(CMD_VMARGS);
result.append(NEW_LINE);
result.append(vmargs);
}
return result.toString();
}
private void cleanupTempDir(File tmpDir, IProgressMonitor monitor) {
File[] files = tmpDir.listFiles();
monitor.beginTask(UpdateJobMessages.InstallUpdateAction_cleanup_task, files.length);
for (File file : files) {
UpdateUtils.delete(file, monitor);
}
monitor.done();
}
private void deleteUpdateDirectory(IProgressMonitor monitor) {
IPath updateDir = UpdateCore.getUpdateDirPath();
try {
UpdateUtils.deleteDirectory(updateDir.toFile(), monitor);
} catch (Throwable th) {
// Don't let exceptions in cleanup block update
UpdateCore.logError(th);
}
}
private boolean doApplyUpdate(IProgressMonitor monitor) throws IOException {
File installer = getInstaller();
if (installer != null && installer.exists()) {
return runInstaller(installer);
}
File installTarget = UpdateUtils.getUpdateInstallDir();
//TODO (pquitslund): only necessary for testing
if (!installTarget.exists()) {
installTarget.mkdir();
}
File installScript = new File(installTarget, INSTALL_SCRIPT);
File tmpDir = UpdateUtils.getUpdateTempDir();
SubMonitor mon = SubMonitor.convert(monitor, null, installScript.exists() ? 120 : 100);
cleanupTempDir(tmpDir, mon.newChild(3));
Revision latestStagedUpdate = updateManager.getLatestStagedUpdate();
File updateZip = latestStagedUpdate.getLocalPath().toFile();
if (latestStagedUpdate == Revision.UNKNOWN || !UpdateUtils.isZipValid(updateZip)) {
final boolean[] retry = new boolean[1];
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
retry[0] = new RetryUpdateDialog(getShell()).open() == 0;
}
});
if (retry[0]) {
Revision latest = UpdateManager.getInstance().getLatestRevision();
UpdateManager.getInstance().scheduleDownload(latest);
}
return false;
}
monitor.setTaskName(UpdateJobMessages.InstallUpdateAction_extract_task);
UpdateUtils.unzip(
updateZip,
tmpDir,
UpdateJobMessages.InstallUpdateAction_extract_task,
mon.newChild(20));
monitor.setTaskName(UpdateJobMessages.InstallUpdateAction_preparing_task);
terminateRunningDartLaunches();
File sdkDir = new File(installTarget, "dart-sdk"); //$NON-NLS-1$
UpdateUtils.deleteDirectory(sdkDir, mon.newChild(4));
UpdateUtils.deleteDirectory(new File(installTarget, "samples"), mon.newChild(4)); //$NON-NLS-1$
// TODO(keertip): check can be removed after all installs are on version with android dir
File androidDir = new File(installTarget, "android");
if (androidDir.exists()) {
UpdateUtils.deleteDirectory(androidDir, mon.newChild(4));
}
File dartium = DartSdkManager.getManager().getDartiumWorkingDirectory(installTarget);
try {
UpdateUtils.delete(dartium, mon.newChild(2));
} catch (Throwable th) {
//TODO(pquitslund): handle delete errors
UpdateCore.logError(th);
}
monitor.setTaskName(UpdateJobMessages.InstallUpdateAction_install_task);
File installDir = new File(tmpDir, "dart"); //$NON-NLS-1$
int fileCount = UpdateUtils.countFiles(installDir);
UpdateUtils.copyDirectory(
installDir,
installTarget,
UPDATE_OVERRIDE_FILTER,
mon.newChild(67).setWorkRemaining(fileCount));
//update/merge DartEditor.ini
if (!DartCore.isMac()) {
//mac INI files are not merged since that would throw off signing
mergeIniFile(installDir, installTarget);
}
mergePropertiesFile(installDir, installTarget);
//ensure executables (such as the analyzer, pub and VM) have the exec bit set
UpdateUtils.ensureExecutable(new File(sdkDir, "bin").listFiles()); //$NON-NLS-1$
UpdateUtils.ensureExecutable(DartSdkManager.getManager().getDartiumExecutable());
//run install.py if present
if (installScript.exists()) {
monitor.setTaskName("Running " + installScript.getName() + " script");
runInstallScript(installScript, mon.newChild(20));
}
// Cleanup
deleteUpdateDirectory(monitor);
return true;
}
private File getIni(File dir) {
//NOTE: only used for Windows and Linux
return new File(dir, "DartEditor.ini"); //$NON-NLS-1$
}
/**
* @return
*/
private File getInstaller() {
IPath zipPath = updateManager.getLatestStagedUpdate().getLocalPath();
IPath msiFile = zipPath.removeFileExtension().addFileExtension("msi");
File installer = msiFile.toFile();
return installer;
}
private File getProperties(File dir) {
return new File(dir, "editor.properties"); //$NON-NLS-1$
}
private Shell getShell() {
return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
}
private boolean isDartLaunch(ILaunch launch) {
try {
return launch.getLaunchConfiguration().getType().getIdentifier().startsWith("com.google"); //$NON-NLS-1$
} catch (CoreException e) {
UpdateCore.logError(e);
}
return false;
}
private void mergeIniFile(File installDir, File installTarget) {
File latestIni = getIni(installDir);
File currentIni = getIni(installTarget);
try {
INIRewriter.mergeAndWrite(currentIni, latestIni);
} catch (IOException e) {
UpdateCore.logError(e);
}
}
private void mergePropertiesFile(File installDir, File installTarget) {
File latestProperties = getProperties(installDir);
File currentProperties = getProperties(installTarget);
try {
PropertiesRewriter.mergeAndWrite(currentProperties, latestProperties);
} catch (IOException e) {
UpdateCore.logError(e);
}
}
private boolean resourcesNeedSaving() {
for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) {
for (IWorkbenchPage page : window.getPages()) {
if (page.getDirtyEditors().length > 0) {
return true;
}
}
}
return false;
}
private void restart() {
String commandLine = buildCommandLine();
System.setProperty(PROP_EXIT_CODE, Integer.toString(24));
System.setProperty(PROP_EXIT_DATA, commandLine);
PlatformUI.getWorkbench().restart();
}
private boolean runInstaller(File msiFile) {
//msiexec.exe /i PATH_TO_NEW_INSTALLER /quiet
List<String> args = new ArrayList<String>();
args.add("msiexec.exe");
args.add("/i");
args.add(msiFile.getName());
//TODO (pquitslund): investigate how reliable the quiet flag is
//args.add("/quiet");
ProcessBuilder builder = new ProcessBuilder(args);
builder.directory(msiFile.getParentFile());
builder.redirectErrorStream(true);
ProcessRunner runner = new ProcessRunner(builder);
try {
runner.runAsync();
} catch (IOException e) {
DartCore.logError(msiFile.getName() + " IOException" + SLASH + runner.getStdOut(), e);
}
// Return false to indicate NO restart
return false;
}
private void runInstallScript(File installScript, SubMonitor mon) {
mon.beginTask("Running " + installScript.getName(), IProgressMonitor.UNKNOWN);
ProcessBuilder builder = new ProcessBuilder("python", installScript.getName());
builder.directory(installScript.getParentFile());
builder.redirectErrorStream(true);
ProcessRunner runner = new ProcessRunner(builder);
int result;
try {
result = runner.runSync(mon);
} catch (IOException e) {
DartCore.logError(installScript.getName() + " IOException" + SLASH + runner.getStdOut(), e);
return;
}
if (result != 0) {
DartCore.logError(installScript.getName() + " terminated abnormally: " + result + SLASH
+ runner.getStdOut());
}
}
private void terminate(ILaunch launch) {
try {
launch.terminate();
} catch (DebugException e) {
UpdateCore.logError(e);
}
}
private void terminateRunningDartLaunches() {
ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager();
for (ILaunch launch : launchManager.getLaunches()) {
if (!launch.isTerminated() && isDartLaunch(launch) && launch.canTerminate()) {
terminate(launch);
}
}
}
}