/******************************************************************************* * Copyright (c) 2009 Red Hat Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Jeff Briggs, Henry Hughes, Ryan Morse, Roland Grunberg, Anithra P J *******************************************************************************/ package org.eclipse.linuxtools.internal.systemtap.ui.ide.handlers; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.net.URI; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.filesystem.URIUtil; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.window.Window; import org.eclipse.jface.wizard.ProgressMonitorPart; import org.eclipse.linuxtools.internal.systemtap.ui.ide.IDEPlugin; import org.eclipse.linuxtools.internal.systemtap.ui.ide.Localization; import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.PathEditorInput; import org.eclipse.linuxtools.internal.systemtap.ui.ide.launcher.SystemTapScriptLaunch; import org.eclipse.linuxtools.internal.systemtap.ui.ide.preferences.EnvironmentVariablesPreferencePage; import org.eclipse.linuxtools.internal.systemtap.ui.ide.preferences.IDEPreferenceConstants; import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.StapErrorParser; import org.eclipse.linuxtools.systemtap.graphing.ui.widgets.ExceptionErrorDialog; import org.eclipse.linuxtools.systemtap.ui.consolelog.ScpClient; import org.eclipse.linuxtools.systemtap.ui.consolelog.structures.RemoteScriptOptions; import org.eclipse.linuxtools.systemtap.ui.consolelog.structures.ScriptConsole; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IPathEditorInput; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.handlers.HandlerUtil; import org.eclipse.ui.ide.ResourceUtil; import com.jcraft.jsch.JSchException; /** * This <code>Action</code> is used to run a SystemTap script that is currently open in the editor. * Contributors: * Ryan Morse - Original author. * Red Hat Inc. - Copied most code from RunScriptAction here and made it into * base class for run actions. * @since 2.0 */ public class RunScriptHandler extends AbstractHandler { /** * @since 2.0 */ private RemoteScriptOptions remoteOptions = null; private IEditorPart targetEditor = null; private String fileName = null; private String tmpfileName = null; private IPath path = null; private IProject project = null; private SystemTapScriptLaunch launch = null; private final List<String> cmdList = new ArrayList<>(); /** * @since 2.0 */ public void setPath(IPath path) { this.path = path; URI uri = URIUtil.toURI(path); IFile[] files = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocationURI(uri); if (files.length > 0) { this.project = files[0].getProject(); } } /** * @since 2.3 */ public void setLaunch(SystemTapScriptLaunch launch) { this.launch = launch; } /** * Adds the given String to the list of commands to be * passed to systemtap when running the command * @param option */ public void addComandLineOptions(String option) { cmdList.add(option); } /** * Set the options for running the script remotely. If the script is to be run locally, * pass <code>null</code> as the only parameter. * @param remoteOptions The remote options of the script run, or <code>null</code> if the script * is to be run locally. * @since 3.0 */ public void setRemoteScriptOptions(RemoteScriptOptions remoteOptions) { this.remoteOptions = remoteOptions; } public boolean getRunLocal() { return remoteOptions == null; } /** * @since 2.1 */ public IProject getProject() { return project; } /** * The main body of this event. Starts by making sure the current editor is valid to run, * then builds the command line arguments for stap and retrieves the environment variables. * Finally, it gets an instance of <code>ScriptConsole</code> to run the script. */ @Override public Object execute(ExecutionEvent event) throws ExecutionException { try { executeAction(event); } catch (ExecutionException e) { // If the event isn't null, an error dialog must be displayed now. if (event != null) { ExceptionErrorDialog.openError( Localization.getString("RunScriptHandler.Error"), //$NON-NLS-1$ Localization.getString("RunScriptHandler.ErrorMessage"), e); //$NON-NLS-1$ } throw e; } return null; } private void executeAction(ExecutionEvent event) throws ExecutionException { cmdList.clear(); final boolean local = getRunLocal(); findTargetEditor(event); findFilePath(); tryEditorSave(event); if (!local) { prepareNonLocalScript(); } final String[] script = buildStandardScript(); final String[] envVars = EnvironmentVariablesPreferencePage.getEnvironmentVariables(); Display.getDefault().asyncExec(() -> { String name = getConsoleName(); if (ScriptConsole.instanceIsRunning(name)) { MessageDialog dialog = new MessageDialog( PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), Messages.RunScriptHandler_AlreadyRunningDialogTitle, null, MessageFormat.format(Messages.RunScriptHandler_AlreadyRunningDialogMessage, fileName), MessageDialog.QUESTION, new String[]{"Yes", "No"}, 0); //$NON-NLS-1$ //$NON-NLS-2$ if (dialog.open() != Window.OK) { if (launch != null) { launch.forceRemove(); } return; } } final ScriptConsole console = ScriptConsole.getInstance(name); if (!local) { console.run(script, envVars, remoteOptions, new StapErrorParser()); } else { console.runLocally(script, envVars, new StapErrorParser(), getProject()); } scriptConsoleInitialized(console); }); } private String getConsoleName() { return getRunLocal() ? fileName : MessageFormat.format(Messages.RunScriptHandler_NonLocalTitle, fileName, remoteOptions.userName, remoteOptions.hostName); } /** * Once a console for running the script has been created this * function is called so that observers can be added for example * @param console * @since 2.0 */ protected void scriptConsoleInitialized(ScriptConsole console) { if (launch != null && path != null) { launch.setConsole(console); } } private void findTargetEditor(ExecutionEvent event) { if (event != null) { targetEditor = HandlerUtil.getActiveEditor(event); } else { for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) { IWorkbenchPage activePage = window.getActivePage(); IEditorPart edTest = activePage.getActiveEditor(); if (edTest != null && editorMatchesPath(edTest.getEditorInput())) { targetEditor = edTest; } else { for (IEditorReference ref : activePage.getEditorReferences()) { try { if (editorMatchesPath(ref.getEditorInput())) { targetEditor = ref.getEditor(false); break; } } catch (PartInitException e) { continue; } } } } } } private boolean editorMatchesPath(IEditorInput input) { return input instanceof IPathEditorInput && ((IPathEditorInput) (input)).getPath().equals(path); } /** * Checks whether the directory to which the given file * belongs is a valid directory. Currently this function just * checks if the given file does not belong to the tapset * directory. * @param fileName * @return true if the given path is valid false otherwise. * @since 1.2 */ private void findFilePath() throws ExecutionException { if (path != null) { fileName = path.toOSString(); } else if (targetEditor == null) { // Cannot have neither a path nor an editor. throw new ExecutionException(Localization.getString("RunScriptHandler.noScriptFile")); //$NON-NLS-1$ } else if (targetEditor.getEditorInput() instanceof PathEditorInput) { fileName = ((PathEditorInput) targetEditor.getEditorInput()).getPath().toString(); } else { fileName = ResourceUtil.getFile(targetEditor.getEditorInput()).getLocation().toString(); } } /** * If an editor containing the file to be run is open & dirty, save it, if appropriate. * @param event */ private void tryEditorSave(final ExecutionEvent event) { // No need to save if the script will already be saved with its project when launched. if (project != null) { return; } if (targetEditor != null && targetEditor.isDirty()) { Display.getDefault().syncExec(() -> { Shell shell = event != null ? HandlerUtil.getActiveShell(event) : PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); targetEditor.doSave(new ProgressMonitorPart(shell, new FillLayout())); }); } } /** * Attempts to set up a channel for a script that runs on a non-local host or user. * @return <code>true</code> on success, <code>false</code> on failure. * @throws ExecutionException If failure occurs during a (non-simple) launch, * this will throw an exception instead of returning <code>false</code>. */ private void prepareNonLocalScript() throws ExecutionException { try { ScpClient scpclient = new ScpClient(remoteOptions); tmpfileName = new Path("/tmp").append(getFileName(fileName)).toOSString(); //$NON-NLS-1$ scpclient.transfer(fileName, tmpfileName); } catch (final JSchException | IOException e) { String message = e instanceof JSchException ? Localization.getString("RunScriptHandler.checkCredentials") //$NON-NLS-1$ : Localization.getString("RunScriptHandler.ioError"); //$NON-NLS-1$ throw new ExecutionException(message, e); } } private String getFileName(String fileName) { return new File(fileName).getName(); } /** * The command line argument generation method used by <code>RunScriptAction</code>. This generates * a stap command line that includes the tapsets specified in user preferences, a guru mode flag * if necessary, and the path to the script on disk. * @return The command to invoke to start the script running in stap. * @since 2.0 */ private String[] buildStandardScript() throws ExecutionException { getImportedTapsets(); if (isGuru()) { cmdList.add("-g"); //$NON-NLS-1$ } return finalizeScript(); } /** * Adds the tapsets that the user has added in preferences to the input <code>ArrayList</code> * @param cmdList The list to add the user-specified tapset locations to. * @since 2.0 */ private void getImportedTapsets() { IPreferenceStore preferenceStore = IDEPlugin.getDefault().getPreferenceStore(); String[] tapsets = preferenceStore.getString(IDEPreferenceConstants.P_TAPSETS).split(File.pathSeparator); //Get all imported tapsets if (tapsets.length > 0 && tapsets[0].trim().length() > 0) { for (int i = 0; i < tapsets.length; i++) { cmdList.add("-I"); //$NON-NLS-1$ cmdList.add(tapsets[i]); } } } /** * Checks the current script to determine if guru mode is required in order to run. This is determined * by the presence of embedded C. * @return True if the script contains embedded C code. */ private boolean isGuru() throws ExecutionException { File f = new File(fileName); try (FileReader fr = new FileReader(f)) { int curr = 0; int prev = 0; boolean front = false; boolean embedded = false; boolean inLineComment = false; boolean inBlockComment = false; while (-1 != (curr = fr.read())) { if (!inLineComment && !inBlockComment && prev == '%' && curr == '{') { front = true; } else if (!inLineComment && !inBlockComment && prev == '%' && curr == '}' && front) { embedded = true; break; } else if (!inBlockComment && ((prev == '/' && curr == '/') || curr == '#')) { inLineComment = true; } else if (!inLineComment && prev == '/' && curr == '*') { inBlockComment = true; } else if (curr == '\n') { inLineComment = false; } else if (prev == '*' && curr == '/') { inBlockComment = false; } prev = curr; } if (embedded) { return true; } } catch (FileNotFoundException fnfe) { throw new ExecutionException(Localization.getString("RunScriptHandler.couldNotOpenScriptFile"), fnfe); //$NON-NLS-1$ } catch (IOException ie) { throw new ExecutionException(Localization.getString("RunScriptHandler.fileIOError"), ie); //$NON-NLS-1$ } return false; } /** * Produces a <code>String[]</code> from the <code>ArrayList</code> passed in with stap inserted * as the first entry, and the filename as the last entry. Used to convert the arguments generated * earlier in <code>buildStandardScript</code> such as tapset locations and guru mode into an actual * command line argument array that can be passed to <code>Runtime.exec</code>. * @return An array suitable to pass to <code>Runtime.exec</code> to start stap on this file. * @since 2.0 */ private String[] finalizeScript() throws ExecutionException { // Make sure script name only contains underscores and/or alphanumeric characters. if (!Pattern.matches("^[a-z0-9_A-Z]+$", //$NON-NLS-1$ getFileNameWithoutExtension(getFileName(fileName)))) { throw new ExecutionException(Messages.RunScriptHandler_InvalidScriptMessage); } String[] script = new String[cmdList.size() + 2]; script[0] = "stap"; //$NON-NLS-1$ script[script.length - 1] = !getRunLocal() ? tmpfileName : fileName; for (int i = 0; i < cmdList.size(); i++) { script[i + 1] = cmdList.get(i); } return script; } private String getFileNameWithoutExtension(String fileName) { int dotIndex = fileName.lastIndexOf('.'); return dotIndex != -1 ? fileName.substring(0, dotIndex) : fileName; } }