diff --git a/README.md b/README.md new file mode 100644 index 0000000..04f1ccc --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# TRAINS - PyCharm Plugin + +PyCharm plugin allow syncing of local repo information to remote debugger machine. + +## Usage + +Install latest plugin from [Releases](https://github.com/allegroai/trains-pycharm-plugin/releases) + +*Optional*, configure TRAINS host and credentials: + +0. With PyCharm +1. Open Settings -> Tools -> TRAINS +2. Add your TRAINS host (for example: http://localhost:8008) +3. Add your TRAINS user credentials key/secret + +## Known Problems + +- None: so far... +- Let us known in the [issues](https://github.com/allegroai/trains-pycharm-plugin/issues) section diff --git a/Trains.iml b/Trains.iml new file mode 100644 index 0000000..34110fd --- /dev/null +++ b/Trains.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/TrainsPlugin.iml b/TrainsPlugin.iml new file mode 100644 index 0000000..e025b20 --- /dev/null +++ b/TrainsPlugin.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml new file mode 100644 index 0000000..c9bb123 --- /dev/null +++ b/resources/META-INF/plugin.xml @@ -0,0 +1,43 @@ + + com.trains.plugin + TRAINS + 0.1.4 + trainsai.io + + + Configure TRAINS credentials:
+ Open Settings -> Tools -> TRAINS
+ Add your TRAINS user credentials: key & secret
+ + ]]>
+ + +
  • 0.1.1 - Initial beta release
  • +
  • 0.1.2 - Windows support
  • +
  • 0.1.3 - Windows git fix
  • +
  • 0.1.4 - Added: TRAINS host configuration
  • + + (c) allegro.ai + ]]> +
    + + + + com.intellij.modules.lang + com.intellij.modules.python + com.intellij.modules.platform + Git4Idea + + + + + + + + +
    diff --git a/src/Barrier.java b/src/Barrier.java new file mode 100644 index 0000000..709f234 --- /dev/null +++ b/src/Barrier.java @@ -0,0 +1,32 @@ +/** + * Helps to synchronize between two or more threads that one waits for another. + * + * Usage : + * + * [[waitForDone()]] on the thread that's waiting for the barrier + * [[done()]] on the thread that other threads are waiting for (when done) + */ +class Barrier { + + private final Object monitor = new Object(); + private volatile boolean open = false; + + Barrier(boolean open) { + this.open = open; + } + + void waitForDone() throws InterruptedException { + synchronized (monitor) { + while (!open) { + monitor.wait(); + } + } + } + + void done() { + synchronized (monitor) { + open = true; + monitor.notifyAll(); + } + } +} diff --git a/src/HookConfigurable.java b/src/HookConfigurable.java new file mode 100644 index 0000000..f380193 --- /dev/null +++ b/src/HookConfigurable.java @@ -0,0 +1,197 @@ +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +public class HookConfigurable implements Configurable { + + private static final String PATH_HOST = "trains.host"; + private static final String PATH_KEY = "trains.key"; + private static final String PATH_SECRET = "trains.secret"; + + private JTextField userHost; + private JTextField userKey; + private JTextField userSecret; + private static String storedHost = null; + private static String storedKey = null; + private static String storedSecret = null; + + private final Project project; + + public HookConfigurable(Project project) { + this.project = project; + if (storedHost == null || storedSecret == null || storedKey==null) { + loadFromProperties(project); + } + } + + private static void loadFromProperties(Project project){ + PropertiesComponent properties = PropertiesComponent.getInstance(project); + storedKey = properties.getValue(PATH_KEY); + storedSecret = properties.getValue(PATH_SECRET); + storedHost = properties.getValue(PATH_HOST); + } + + @Nls + @Override + public String getDisplayName() { + return "TRAINS"; + } + + @Nullable + @Override + public String getHelpTopic() { + return null; + } + + @Nullable + @Override + public JComponent createComponent() { + userHost = new JTextField(); + userKey = new JTextField(); + userSecret = new JTextField(); + + JPanel container = new JPanel(new GridLayoutManager(4, 2, + new Insets(0, 0, 0, 0), 12, 12)); + + GridConstraints pathLabelConstraint0 = new GridConstraints(); + pathLabelConstraint0.setRow(0); + pathLabelConstraint0.setColumn(0); + pathLabelConstraint0.setFill(GridConstraints.FILL_HORIZONTAL); + pathLabelConstraint0.setVSizePolicy(GridConstraints.SIZEPOLICY_CAN_SHRINK); + container.add(new JLabel("TRAINS server: "), pathLabelConstraint0); + + GridConstraints pathFieldConstraint0 = new GridConstraints(); + pathFieldConstraint0.setHSizePolicy(GridConstraints.SIZEPOLICY_WANT_GROW); + pathFieldConstraint0.setFill(GridConstraints.FILL_HORIZONTAL); + pathFieldConstraint0.setAnchor(GridConstraints.ANCHOR_WEST); + pathFieldConstraint0.setRow(0); + pathFieldConstraint0.setColumn(1); + pathFieldConstraint0.setVSizePolicy(GridConstraints.SIZEPOLICY_CAN_SHRINK); + container.add(userHost, pathFieldConstraint0); + + + GridConstraints pathLabelConstraint = new GridConstraints(); + pathLabelConstraint.setRow(1); + pathLabelConstraint.setColumn(0); + pathLabelConstraint.setFill(GridConstraints.FILL_HORIZONTAL); + pathLabelConstraint.setVSizePolicy(GridConstraints.SIZEPOLICY_CAN_SHRINK); + container.add(new JLabel("User credentials: Key"), pathLabelConstraint); + + GridConstraints pathFieldConstraint = new GridConstraints(); + pathFieldConstraint.setHSizePolicy(GridConstraints.SIZEPOLICY_WANT_GROW); + pathFieldConstraint.setFill(GridConstraints.FILL_HORIZONTAL); + pathFieldConstraint.setAnchor(GridConstraints.ANCHOR_WEST); + pathFieldConstraint.setRow(1); + pathFieldConstraint.setColumn(1); + pathFieldConstraint.setVSizePolicy(GridConstraints.SIZEPOLICY_CAN_SHRINK); + container.add(userKey, pathFieldConstraint); + + + GridConstraints pathLabelConstraint2 = new GridConstraints(); + pathLabelConstraint2.setRow(2); + pathLabelConstraint2.setColumn(0); + pathLabelConstraint2.setFill(GridConstraints.FILL_HORIZONTAL); + pathLabelConstraint2.setVSizePolicy(GridConstraints.SIZEPOLICY_CAN_SHRINK); + container.add(new JLabel("User credentials: Secret "), pathLabelConstraint2); + + GridConstraints pathFieldConstraint2 = new GridConstraints(); + pathFieldConstraint2.setHSizePolicy(GridConstraints.SIZEPOLICY_WANT_GROW); + pathFieldConstraint2.setFill(GridConstraints.FILL_HORIZONTAL); + pathFieldConstraint2.setAnchor(GridConstraints.ANCHOR_WEST); + pathFieldConstraint2.setRow(2); + pathFieldConstraint2.setColumn(1); + pathFieldConstraint2.setVSizePolicy(GridConstraints.SIZEPOLICY_CAN_SHRINK); + container.add(userSecret, pathFieldConstraint2); + + + JPanel spacer = new JPanel(); + GridConstraints spacerConstraints = new GridConstraints(); + spacerConstraints.setRow(3); + spacerConstraints.setFill(GridConstraints.FILL_BOTH); + container.add(spacer, spacerConstraints); + + return container; + } + + @Override + public boolean isModified() { + if (storedKey == null && userKey != null) { + return true; + } + + if (storedSecret == null && userSecret != null) { + return true; + } + + if (storedHost == null && userHost != null) { + return true; + } + + return !storedKey.equals(userKey.getText()) || !storedSecret.equals(userSecret.getText()) + || !storedHost.equals(userHost.getText()); + } + + @Override + public void apply() throws ConfigurationException { + storedKey = userKey.getText().trim(); + storedSecret = userSecret.getText().trim(); + storedHost = userHost.getText().trim(); + + PropertiesComponent properties = PropertiesComponent.getInstance(project); + properties.setValue(PATH_HOST, storedHost); + properties.setValue(PATH_KEY, storedKey); + properties.setValue(PATH_SECRET, storedSecret); + } + + @Override + public void reset() { + if (userHost != null) { + userHost.setText(storedHost); + } + if (userKey != null) { + userKey.setText(storedKey); + } + if (userSecret != null) { + userSecret.setText(storedSecret); + } + } + + @Override + public void disposeUIResources() { + userKey = null; + userSecret = null; + userHost = null; + } + + static String getStoredKey(Project project) { + if (storedKey==null && project!=null){ + loadFromProperties(project); + } + return storedKey; + } + + static String getStoredSecret(Project project) { + if (storedSecret==null && project!=null){ + loadFromProperties(project); + } + return storedSecret; + } + + static String getStoredHost(Project project) { + if (storedHost==null && project!=null){ + loadFromProperties(project); + } + return storedHost; + } +} \ No newline at end of file diff --git a/src/OsUtil.java b/src/OsUtil.java new file mode 100644 index 0000000..c6c7a3b --- /dev/null +++ b/src/OsUtil.java @@ -0,0 +1,9 @@ +class OsUtil { + + private OsUtil() { + + } + + final static boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); + final static boolean isMac = System.getProperty("os.name").toLowerCase().startsWith("mac os"); +} diff --git a/src/ProcessResult.java b/src/ProcessResult.java new file mode 100644 index 0000000..e421aaa --- /dev/null +++ b/src/ProcessResult.java @@ -0,0 +1,30 @@ +class ProcessResult { + + private InterruptedException exception = null; + private int exitCode = -1; + private boolean canceled = false; + + boolean isCanceled() { + return canceled; + } + + void setCanceled() { + canceled = true; + } + + int getExitCode() { + return exitCode; + } + + void setExitCode(int exitCode) { + this.exitCode = exitCode; + } + + InterruptedException getException() { + return exception; + } + + void setException(InterruptedException exception) { + this.exception = exception; + } +} diff --git a/src/TrainsRunExtension.java b/src/TrainsRunExtension.java new file mode 100644 index 0000000..ce8c507 --- /dev/null +++ b/src/TrainsRunExtension.java @@ -0,0 +1,208 @@ +import com.intellij.execution.ExecutionException; +import com.intellij.execution.configurations.GeneralCommandLine; +// import com.intellij.execution.configurations.ParamsGroup; +import com.intellij.execution.configurations.RunnerSettings; +// import com.intellij.ide.util.PropertiesComponent; +import com.intellij.openapi.options.SettingsEditor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.util.*; +import com.intellij.openapi.wm.WindowManager; +import com.intellij.ui.awt.RelativePoint; +// import com.jetbrains.python.debugger.PyDebugRunner; +import com.jetbrains.python.run.AbstractPythonRunConfiguration; +// import com.jetbrains.python.run.PythonCommandLineState; +// import com.jetbrains.python.run.PythonRunConfiguration; +import com.jetbrains.python.run.PythonRunConfigurationExtension; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import git4idea.config.GitVcsApplicationSettings; + +import javax.swing.*; +import java.io.*; +import java.util.*; +import java.util.concurrent.TimeUnit; + + +public class TrainsRunExtension extends PythonRunConfigurationExtension { + private Project project = null; + + @Override + protected void readExternal(@NotNull AbstractPythonRunConfiguration runConfiguration, @NotNull Element element) throws InvalidDataException { + } + + @Override + protected void writeExternal(@NotNull AbstractPythonRunConfiguration runConfiguration, @NotNull Element element) throws WriteExternalException { + } + + @Nullable + @Override + protected SettingsEditor createEditor(@NotNull AbstractPythonRunConfiguration configuration) { + return null; + } + + @Nullable + @Override + protected String getEditorTitle() { + return "TRAINS (configuration)"; + } + + @Override + public boolean isApplicableFor(@NotNull AbstractPythonRunConfiguration configuration) { + return true; + } + + @Override + public boolean isEnabledFor(@NotNull AbstractPythonRunConfiguration applicableConfiguration, @Nullable RunnerSettings runnerSettings) { + return true; + } + + @Override + protected void patchCommandLine(AbstractPythonRunConfiguration configuration, @Nullable RunnerSettings runnerSettings, + GeneralCommandLine cmdLine, String runnerId) throws ExecutionException { + // Reference: org.python.pydev.debug.profile.PyProfilePreferences.addProfileArgs(List, boolean, boolean) + project = configuration.getProject(); + if (HookConfigurable.getStoredKey(project) != null && !HookConfigurable.getStoredKey(project).isEmpty()) + cmdLine.withEnvironment("ALG_API_ACCESS_KEY", HookConfigurable.getStoredKey(project)); + if (HookConfigurable.getStoredSecret(project) != null && !HookConfigurable.getStoredSecret(project).isEmpty()) + cmdLine.withEnvironment("ALG_API_SECRET_KEY", HookConfigurable.getStoredSecret(project)); + if (HookConfigurable.getStoredHost(project) != null && !HookConfigurable.getStoredHost(project).isEmpty()) + cmdLine.withEnvironment("ALG_API_HOST", HookConfigurable.getStoredHost(project)); + + String git = null; + // first try new API + try { + git = GitVcsApplicationSettings.getInstance().getSavedPathToGit(); + } + catch (Throwable t){ + } + // if we failed try the old API + if (git == null) + { + try { + git = GitVcsApplicationSettings.getInstance().getPathToGit(); + } catch (Throwable t) { + } + } + if (git == null) { + project = null; + return; + } + + if (OsUtil.isWindows) { + git = String.format("\"%s\"", git); + } + String path = project.getBasePath(); + + String gitRepo = runCommand(git + " remote get-url origin", path, false); + String gitBranch = runCommand(git + " rev-parse --abbrev-ref --symbolic-full-name @{u}", path, false); + String gitCommit = runCommand(git + " rev-parse HEAD", path, false); + String gitRoot = runCommand(git + " rev-parse --show-toplevel", path, false); + String gitStatus = runCommand(git + " status -s", path, false); + String gitDiff = runCommand(git + " diff", path, true); + if (gitRepo!=null) + cmdLine.withEnvironment("ALG_VCS_REPO_URL", gitRepo); + if (gitBranch!=null) + cmdLine.withEnvironment("ALG_VCS_BRANCH", gitBranch); + if (gitCommit!=null) + cmdLine.withEnvironment("ALG_VCS_COMMIT_ID", gitCommit); + if (gitRoot!=null) { + String root = configuration.getWorkingDirectory().replace(gitRoot, "."); + cmdLine.withEnvironment("ALG_VCS_ROOT", root.isEmpty() ? "." : root); + } + if (gitStatus!=null) + cmdLine.withEnvironment("ALG_VCS_STATUS", Base64.getEncoder().encodeToString(gitStatus.getBytes())); + if (gitDiff!=null) + cmdLine.withEnvironment("ALG_VCS_DIFF", Base64.getEncoder().encodeToString(gitDiff.getBytes())); + project = null; + } + + private String runCommand(String cmd, String path, boolean useTempFile) { + String output = null; + File tempFile = null; + try + { + if (useTempFile){ + tempFile = File.createTempFile(".trains_git_diff", ".txt"); + cmd += " > " + tempFile.getAbsolutePath(); + } + String[] fullCommand = getCommand(cmd); + Process proc = Runtime.getRuntime().exec(fullCommand ,null, new File(path)); + BufferedReader br=null; + if (!useTempFile) { + InputStream stdin = proc.getInputStream(); + InputStreamReader isr = new InputStreamReader(stdin); + br = new BufferedReader(isr); + } + if (!proc.waitFor(5, TimeUnit.SECONDS)) { + proc.destroyForcibly(); + if (tempFile != null) + tempFile.delete(); + openWarning("warning", String.format("execution timed out: %s", cmd)); + return output; + } + if (br==null) + br = new BufferedReader(new FileReader(tempFile.getAbsoluteFile())); + // check if there was an error, update nothing + if (proc.exitValue()!=0) { + if (tempFile != null) + tempFile.delete(); + // System.out.println(": "+cmd); + return output; + } + String line = null; + // System.out.println(""); + while ( (line = br.readLine()) != null) { + // System.out.println(line); + if (output != null) + output += "\n"+line; + else + output = line; + } + // System.out.println(""); + // System.out.println("Process exitValue: " + exitVal); + if (tempFile != null) + tempFile.delete(); + } + catch (Throwable t) + { + if (tempFile != null) + tempFile.delete(); + t.printStackTrace(); + } + return output; + } + + private String[] getCommand(String scriptCmd) { + if (OsUtil.isWindows) { + return new String[]{"C:\\Windows\\System32\\cmd.exe", "/c", scriptCmd}; + } else if (OsUtil.isMac) { + return new String[]{"/bin/bash", "-c", scriptCmd}; + } else { + return new String[]{"/bin/bash", "-c", scriptCmd}; + } + } + + private void openWarning(final String title, final String message) { + JBPopupFactory.getInstance().createHtmlTextBalloonBuilder( + "TRAINS " + title + ": " + message, MessageType.WARNING, null + ).setFadeoutTime(3000) + .createBalloon().show( + RelativePoint.getNorthWestOf(WindowManager.getInstance().getStatusBar(project).getComponent()), + Balloon.Position.atRight + ); + } + + private void openError(final String title, final String message) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + JOptionPane.showMessageDialog(new JFrame(), message, title, JOptionPane.ERROR_MESSAGE); + } + }); + } + +}