package org.jetbrains.appcode.reveal;
import com.intellij.CommonBundle;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.configurations.RunnerSettings;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.ui.ExecutionConsole;
import com.intellij.icons.AllIcons;
import com.intellij.internal.statistic.UsageTrigger;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SettingsEditor;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.HyperlinkLabel;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.FormBuilder;
import com.intellij.util.ui.UIUtil;
import com.jetbrains.cidr.execution.*;
import com.jetbrains.cidr.xcode.frameworks.AppleSdk;
import com.jetbrains.cidr.xcode.frameworks.buildSystem.BuildSettingNames;
import com.jetbrains.cidr.xcode.model.*;
import com.jetbrains.cidr.xcode.plist.Plist;
import com.jetbrains.cidr.xcode.plist.PlistDriver;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class RevealRunConfigurationExtension extends AppCodeRunConfigurationExtension {
private static final String REVEAL_SETTINGS_TAG = "REVEAL_SETTINGS";
private static final Key<RevealSettings> REVEAL_SETTINGS_KEY = Key.create(REVEAL_SETTINGS_TAG);
private static final Key<String> BUNDLE_ID_KEY = Key.create("BUNDLE_INFO");
private static final Key<File> FILE_TO_INJECT = Key.create("reveal.library.to.inject");
private static final List<String> KNOWN_FRAMEWORK_NAMES = ContainerUtil.newArrayList("RevealServer", "Reveal");
@NotNull
public static RevealSettings getRevealSettings(@NotNull AppCodeRunConfiguration config) {
RevealSettings settings = config.getUserData(REVEAL_SETTINGS_KEY);
return settings == null ? new RevealSettings() : settings;
}
public static void setRevealSettings(@NotNull AppCodeRunConfiguration runConfiguration, @Nullable RevealSettings settings) {
runConfiguration.putUserData(REVEAL_SETTINGS_KEY, settings);
}
@Override
protected void readExternal(@NotNull AppCodeRunConfiguration runConfiguration, @NotNull Element element)
throws InvalidDataException {
Element settingsTag = element.getChild(REVEAL_SETTINGS_TAG);
RevealSettings settings = null;
if (settingsTag != null) {
settings = new RevealSettings();
settings.autoInject = getAttributeValue(settingsTag.getAttributeValue("autoInject"), settings.autoInject);
settings.autoInstall = getAttributeValue(settingsTag.getAttributeValue("autoInstall"), settings.autoInstall);
settings.askToEnableAutoInstall =
getAttributeValue(settingsTag.getAttributeValue("askToEnableAutoInstall"), settings.askToEnableAutoInstall);
}
setRevealSettings(runConfiguration, settings);
}
private static boolean getAttributeValue(@Nullable String value, boolean defaultValue) {
return value == null ? defaultValue : "true".equals(value);
}
@Override
protected void writeExternal(@NotNull AppCodeRunConfiguration runConfiguration, @NotNull Element element)
throws WriteExternalException {
RevealSettings settings = getRevealSettings(runConfiguration);
Element settingsTag = new Element(REVEAL_SETTINGS_TAG);
settingsTag.setAttribute("autoInject", String.valueOf(settings.autoInject));
settingsTag.setAttribute("autoInstall", String.valueOf(settings.autoInstall));
settingsTag.setAttribute("askToEnableAutoInstall", String.valueOf(settings.askToEnableAutoInstall));
element.addContent(settingsTag);
}
@Override
public void copyConfiguration(@NotNull AppCodeRunConfiguration from, @NotNull AppCodeRunConfiguration to) {
RevealSettings settings = getRevealSettings(from);
setRevealSettings(to, settings.clone());
}
@Nullable
@Override
protected <P extends AppCodeRunConfiguration> SettingsEditor<P> createEditor(@NotNull P configuration) {
return new MyEditor<P>();
}
@Nullable
@Override
protected String getEditorTitle() {
return "Reveal";
}
@Override
protected boolean isApplicableFor(@NotNull AppCodeRunConfiguration configuration) {
if (ApplicationManager.getApplication().isUnitTestMode()) return false;
return configuration instanceof AppCodeAppRunConfiguration;
}
@Override
protected boolean isEnabledFor(@NotNull AppCodeRunConfiguration config, @Nullable RunnerSettings runnerSettings) {
File appBundle = Reveal.getDefaultRevealApplicationBundle();
if (appBundle == null) return false;
if (Reveal.getRevealLib(appBundle, getSdk(config)) == null) return false;
return isAvailableForPlatform(config);
}
private static boolean isAvailableForPlatform(@NotNull final AppCodeRunConfiguration config) {
return ReadAction.compute(() -> {
AppleSdk sdk = getSdk(config);
return sdk != null && (sdk.getPlatform().isIOS() || sdk.getPlatform().isTv());
});
}
@Nullable
private static AppleSdk getSdk(@NotNull final AppCodeRunConfiguration config) {
return ReadAction.compute(() -> {
XCBuildConfiguration xcBuildConfiguration = config.getConfiguration();
return xcBuildConfiguration == null ? null : xcBuildConfiguration.getBaseSdk();
});
}
@Override
public void createAdditionalActions(@NotNull AppCodeRunConfiguration configuration,
@NotNull File product,
@NotNull ExecutionEnvironment environment,
@NotNull BuildConfiguration buildConfiguration,
@NotNull ExecutionConsole console,
@NotNull ProcessHandler processHandler,
@NotNull List<AnAction> actions) throws ExecutionException {
super.createAdditionalActions(configuration, product, environment, buildConfiguration, console, processHandler, actions);
actions.add(new RefreshRevealAction(configuration,
environment,
processHandler,
buildConfiguration.getDestination(),
getBundleID(environment, product)));
}
@Override
public void installIntoApplicationBundle(@NotNull AppCodeRunConfiguration configuration,
@NotNull ExecutionEnvironment environment,
@NotNull BuildConfiguration buildConfiguration,
@NotNull File mainExecutable) throws ExecutionException {
File appBundle = Reveal.getDefaultRevealApplicationBundle();
if (appBundle == null) return;
if (!Reveal.isCompatible(appBundle)) return;
RevealSettings settings = getRevealSettings(configuration);
if (!settings.autoInject) return;
File toInject = installReveal(configuration, buildConfiguration, mainExecutable, settings);
if (toInject == null) return;
UsageTrigger.trigger("appcode.reveal.inject");
environment.putUserData(FILE_TO_INJECT, toInject);
}
@Nullable
private static File installReveal(@NotNull final AppCodeRunConfiguration configuration,
@NotNull BuildConfiguration buildConfiguration,
@NotNull File mainExecutable,
@NotNull final RevealSettings settings) throws ExecutionException {
File appBundle = Reveal.getDefaultRevealApplicationBundle();
if (appBundle == null) throw new ExecutionException("Reveal application bundle not found");
File libReveal = Reveal.getRevealLib(appBundle, getSdk(configuration));
if (libReveal == null) throw new ExecutionException("Reveal library not found");
Reveal.LOG.info("Reveal lib found at " + libReveal);
if (hasBundledRevealLib(buildConfiguration, libReveal)) {
return new File(libReveal.getName());
}
if (hasRevealFramework(buildConfiguration)) {
return null;
}
BuildDestination destination = buildConfiguration.getDestination();
if (!destination.isDevice()) {
return libReveal;
}
if (!settings.autoInstall) {
if (!settings.askToEnableAutoInstall) return null;
final int[] response = new int[1];
UIUtil.invokeAndWaitIfNeeded(
(Runnable)() -> response[0] = Messages.showYesNoDialog("Project is not configured with Reveal library.<br><br>" +
"Would you like to enable automatic library upload for this run configuration?",
"Reveal",
Messages.YES_BUTTON,
Messages.NO_BUTTON,
Messages.getQuestionIcon(),
new DialogWrapper.DoNotAskOption() {
@Override
public boolean isToBeShown() {
return true;
}
@Override
public void setToBeShown(boolean value, int exitCode) {
settings.askToEnableAutoInstall = value;
}
@Override
public boolean canBeHidden() {
return true;
}
@Override
public boolean shouldSaveOptionsOnCancel() {
return false;
}
@NotNull
@Override
public String getDoNotShowMessage() {
return CommonBundle.message("dialog.options.do.not.show");
}
}
));
if (response[0] != Messages.YES) return null;
settings.autoInstall = true;
settings.askToEnableAutoInstall = true; // is user changes autoInstall in future, ask him/her again
setRevealSettings(configuration, settings);
}
UsageTrigger.trigger("appcode.reveal.installOnDevice");
return signAndInstall(libReveal, buildConfiguration, mainExecutable);
}
private static boolean hasBundledRevealLib(@NotNull final BuildConfiguration buildConfiguration, @NotNull final File libReveal) {
return ReadAction.compute(() -> {
PBXTarget target = buildConfiguration.getConfiguration().getTarget();
if (target != null) {
for (PBXBuildFile eachFile : target.getBuildFiles(PBXBuildPhase.Type.RESOURCES)) {
PBXReference ref = eachFile.getFileRef();
String name = ref == null ? null : ref.getFileName();
if (FileUtil.namesEqual(libReveal.getName(), name)) return true;
}
}
return false;
});
}
private static boolean hasRevealFramework(@NotNull BuildConfiguration buildConfiguration) {
return ReadAction.compute(() -> {
List<String> flags = buildConfiguration.getBuildSetting(BuildSettingNames.OTHER_LDFLAGS).getStringList();
return flags.stream().anyMatch(flag -> KNOWN_FRAMEWORK_NAMES.contains(StringUtil.unquoteString(flag)));
});
}
@NotNull
private static File signAndInstall(@NotNull File libReveal,
@NotNull final BuildConfiguration buildConfiguration,
@NotNull File mainExecutable) throws ExecutionException {
File frameworksDir = new File(mainExecutable.getParent(), "Frameworks");
File libRevealCopy = new File(frameworksDir, libReveal.getName());
try {
FileUtil.copy(libReveal, libRevealCopy);
}
catch (IOException e) {
throw new ExecutionException("Cannot create a temporary copy of Reveal library", e);
}
AppCodeInstaller.codesignBinary(buildConfiguration, mainExecutable, frameworksDir.getAbsolutePath(), libRevealCopy.getName());
return new File("Frameworks", libRevealCopy.getName());
}
@NotNull
private static String getBundleID(@NotNull ExecutionEnvironment environment,
@NotNull File product) throws ExecutionException {
String result = environment.getUserData(BUNDLE_ID_KEY);
if (result != null) return result;
File plistFile = new File(product, "Info.plist");
Plist plist = PlistDriver.readAnyFormatSafe(plistFile);
if (plist == null) throw new ExecutionException("Info.plist not found at " + plistFile);
result = plist.getString("CFBundleIdentifier");
if (result == null) throw new ExecutionException("CFBundleIdentifier not found in " + plistFile);
environment.putUserData(BUNDLE_ID_KEY, result);
return result;
}
@Override
public void patchCommandLine(@NotNull ExecutionEnvironment environment, @NotNull GeneralCommandLine cmdLine) {
File toInject = FILE_TO_INJECT.get(environment);
if (toInject != null) {
if (!toInject.isAbsolute()) {
File bundle = new File(cmdLine.getExePath()).getParentFile();
toInject = new File(bundle, toInject.getPath());
}
Reveal.LOG.info("Injecting Reveal lib: " + toInject);
CidrExecUtil.appendSearchPath(cmdLine.getEnvironment(), EnvParameterNames.DYLD_INSERT_LIBRARIES, toInject.getPath());
}
}
private static class MyEditor<T extends AppCodeRunConfiguration> extends SettingsEditor<T> {
private HyperlinkLabel myRevealNotFoundOrIncompatible;
private JBLabel myNotAvailable;
private JBCheckBox myInjectCheckBox;
private JBLabel myInjectHint;
private JBCheckBox myInstallCheckBox;
private JBLabel myInstallHint;
boolean isFound;
boolean isAvailable;
@Override
protected void resetEditorFrom(@NotNull AppCodeRunConfiguration s) {
RevealSettings settings = getRevealSettings(s);
myInjectCheckBox.setSelected(settings.autoInject);
myInstallCheckBox.setSelected(settings.autoInstall);
String notFoundText = null;
boolean found = false;
boolean compatible = false;
File appBundle = Reveal.getDefaultRevealApplicationBundle();
if (appBundle != null) {
found = (Reveal.getRevealLib(appBundle, getSdk(s)) != null);
compatible = Reveal.isCompatible(appBundle);
}
if (!found) {
notFoundText = "Reveal.app not found. You can install it from ";
}
else if (!compatible) {
notFoundText = "Incompatible version of Reveal.app. You can download the latest one from ";
}
if (notFoundText != null) {
myRevealNotFoundOrIncompatible.setHyperlinkText(notFoundText, "revealapp.com", "");
}
isFound = found && compatible;
isAvailable = isAvailableForPlatform(s);
updateControls();
}
@Override
protected void applyEditorTo(@NotNull AppCodeRunConfiguration s) throws ConfigurationException {
RevealSettings settings = getRevealSettings(s);
settings.autoInject = myInjectCheckBox.isSelected();
settings.autoInstall = myInstallCheckBox.isSelected();
setRevealSettings(s, settings);
}
@NotNull
@Override
protected JComponent createEditor() {
FormBuilder builder = new FormBuilder();
myRevealNotFoundOrIncompatible = new HyperlinkLabel();
myRevealNotFoundOrIncompatible.setIcon(AllIcons.RunConfigurations.ConfigurationWarning);
myRevealNotFoundOrIncompatible.setHyperlinkTarget("http://revealapp.com");
myNotAvailable = new JBLabel("<html>" +
"Reveal integration is only available for iOS applications.<br>" +
"OS X targets are not yet supported.<br>" +
"</html>");
myInjectCheckBox = new JBCheckBox("Inject Reveal library on launch");
myInstallCheckBox = new JBCheckBox("Upload Reveal library on the device if necessary");
myInjectHint = new JBLabel(UIUtil.ComponentStyle.SMALL);
myInstallHint = new JBLabel(UIUtil.ComponentStyle.SMALL);
myInjectCheckBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
updateControls();
}
});
builder.addComponent(myNotAvailable);
builder.addComponent(myInjectCheckBox, UIUtil.DEFAULT_VGAP * 3);
builder.setIndent(UIUtil.DEFAULT_HGAP * 4);
builder.addComponent(myInjectHint);
builder.setIndent(UIUtil.DEFAULT_HGAP);
builder.addComponent(myInstallCheckBox);
builder.setIndent(UIUtil.DEFAULT_HGAP * 5);
builder.addComponent(myInstallHint);
JPanel controls = builder.getPanel();
JPanel panel = new JPanel(new BorderLayout());
panel.add(controls, BorderLayout.NORTH);
panel.add(Box.createGlue(), BorderLayout.CENTER);
panel.add(myRevealNotFoundOrIncompatible, BorderLayout.SOUTH);
return panel;
}
private void updateControls() {
boolean controlsEnabled = isFound && isAvailable;
myRevealNotFoundOrIncompatible.setVisible(!isFound);
myNotAvailable.setVisible(!isAvailable);
updateStatusAndHint(myInjectCheckBox, myInjectHint,
controlsEnabled,
"Library is injected on launch using DYLD_INSERT_LIBRARIES variable");
boolean installButtonEnabled = controlsEnabled && myInjectCheckBox.isSelected();
updateStatusAndHint(myInstallCheckBox, myInstallHint,
installButtonEnabled,
"It's not necessary to configure the project manually,<br>" +
"library is signed and uploaded automatically"
);
}
private static void updateStatusAndHint(JComponent comp, JBLabel label, boolean enabled, String text) {
comp.setEnabled(enabled);
label.setEnabled(enabled);
StringBuilder fontString = new StringBuilder();
Color color = enabled ? UIUtil.getLabelForeground() : UIUtil.getLabelDisabledForeground();
if (color != null) {
fontString.append("<font color=#");
UIUtil.appendColor(color, fontString);
fontString.append(">");
}
label.setText("<html>" + fontString + text + "</html>");
}
}
public static class RevealSettings implements Cloneable {
public boolean autoInject;
public boolean autoInstall = true;
public boolean askToEnableAutoInstall = true;
@Override
public RevealSettings clone() {
try {
return (RevealSettings)super.clone();
}
catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
}