org.robovm.compiler.plugin.debug.DebuggerLaunchPlugin Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2016 Justin Shapcott.
*
* 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 org.robovm.compiler.plugin.debug;
import org.robovm.compiler.CompilerException;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.plugin.LaunchPlugin;
import org.robovm.compiler.plugin.PluginArgument;
import org.robovm.compiler.plugin.PluginArguments;
import org.robovm.compiler.target.ConsoleTarget;
import org.robovm.compiler.target.LaunchParameters;
import org.robovm.compiler.target.Target;
import org.robovm.compiler.target.ios.IOSDeviceLaunchParameters;
import org.robovm.compiler.target.ios.IOSTarget;
import org.robovm.debugger.Debugger;
import org.robovm.debugger.DebuggerConfig;
import org.robovm.debugger.DebuggerException;
import org.robovm.debugger.hooks.IHooksConnection;
import org.robovm.libimobiledevice.IDeviceConnection;
import org.robovm.libimobiledevice.util.AppLauncherCallback;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author Demyan Kimitsa
*
* this launch plugin starts JDWP debug server in after launch phase
* there is no direct reference to this class as it is picked up
* from classloader.
* Refer {@link org.robovm.compiler.AppCompiler} to find details about LauchPlugin calls
* Refer {@link Config#loadPluginsFromClassPath()} to find how configs are loaded from classloader
* Also corresponding entry has to be done in META-INF/services
*/
@SuppressWarnings({"unused", "JavadocReference"})
public class DebuggerLaunchPlugin extends LaunchPlugin {
private final static String ARG_KEY_LOG_CONSOLE = "logconsole";
private final static String ARG_KEY_SOURCE_PATH = "sourcepath";
private final static String ARG_KEY_JDWP_PORT = "jdwpport";
private final static String ARG_KEY_CLIENT_MODE = "clientmode";
private final static String ARG_KEY_LOG_DIR = "logdir";
private DebuggerConfig debuggerConfig;
private Debugger debugger;
@Override
public PluginArguments getArguments() {
// list of arguments as these passed by idea, check idea/compilation/RoboVMCompileTask
List args = new ArrayList<>();
args.add(new PluginArgument(ARG_KEY_LOG_CONSOLE, "Flag: enables debugger logs to console"));
args.add(new PluginArgument(ARG_KEY_SOURCE_PATH, "Locations of source files"));
args.add(new PluginArgument(ARG_KEY_JDWP_PORT, "TCP port JDWP server should listen or connects to"));
args.add(new PluginArgument(ARG_KEY_CLIENT_MODE, "Flag: specifies that JDWP server shall connect instead of listening"));
args.add(new PluginArgument(ARG_KEY_LOG_DIR, "Custom location of log directory"));
return new PluginArguments("debug", args);
}
@Override
public void beforeLaunch(Config config, LaunchParameters parameters) {
cleanup();
if (!config.isDebug())
return;
// fetch values passed from Idea/Eclipse
Map arguments = parseArguments(config);
String logDir = argumentValue(arguments, ARG_KEY_LOG_DIR, config.getTmpDir().getAbsolutePath());
int jdwpPort = argumentIntValue(arguments, ARG_KEY_JDWP_PORT);
boolean jdwpClientMode = argumentValue(arguments, ARG_KEY_CLIENT_MODE, false);
boolean logConsole = config.isDumpIntermediates() || argumentValue(arguments, ARG_KEY_LOG_CONSOLE, false);
// common parameters to target
parameters.getArguments().add("-rvm:EnableHooks");
parameters.getArguments().add("-rvm:WaitForResume");
Target target = config.getTarget();
// now create debugger config
DebuggerConfig.Builder builder = new DebuggerConfig.Builder();
builder.setJdwpPort(jdwpPort);
builder.setJdwpClienMode(jdwpClientMode);
builder.setLogToConsole(logConsole);
builder.setLogDir(new File(logDir));
builder.setArch(DebuggerConfig.Arch.valueOf(target.getArch().getCpuArch().name()));
// make list of arguments for target
if (ConsoleTarget.TYPE.equals(target.getType())) {
File appDir = config.isSkipInstall() ? config.getTmpDir() : config.getInstallDir();
builder.setAppfile(new File(appDir, config.getExecutableName()));
File hooksPortFile;
try {
hooksPortFile = File.createTempFile("robovm-dbg-console", ".port");
builder.setHooksPortFile(hooksPortFile);
} catch (IOException e) {
throw new CompilerException("Failed to create debugger port file", e);
}
parameters.getArguments().add("-rvm:PrintDebugPort=" + hooksPortFile.getAbsolutePath());
} else if (IOSTarget.TYPE.equals(target.getType())) {
File appDir = new File(config.isSkipInstall() ? config.getTmpDir() : config.getInstallDir(), config.getExecutableName() + ".app");
builder.setAppfile(new File(appDir, config.getExecutableName()));
if (IOSTarget.isSimulatorArch(config.getArch())) {
// launching on simulator, it can write down port number to file on local system
File hooksPortFile;
try {
hooksPortFile = File.createTempFile("robovm-dbg-sim", ".port");
builder.setHooksPortFile(hooksPortFile);
} catch (IOException e) {
throw new CompilerException("Failed to create simulator debuuger port file", e);
}
parameters.getArguments().add("-rvm:PrintDebugPort=" + hooksPortFile.getAbsolutePath());
} else {
// launching on device
IOSDeviceLaunchParameters deviceLaunchParameters = (IOSDeviceLaunchParameters) parameters;
DebuggerLauncherCallback callback = new DebuggerLauncherCallback();
deviceLaunchParameters.setAppLauncherCallback(callback);
deviceLaunchParameters.getArguments().add("-rvm:PrintDebugPort");
// wait for hooks channel from launch callback
builder.setHooksConnection(callback);
}
} else {
throw new IllegalArgumentException("Unsupported target " + target.getType());
}
debuggerConfig = builder.build();
}
@Override
public void afterLaunch(Config config, LaunchParameters parameters, Process process) {
if (!config.isDebug())
return;
// create and start the debugger
debugger = new Debugger(process, debuggerConfig);
debugger.start();
}
@Override
public void launchFailed(Config config, LaunchParameters parameters) {
cleanup();
}
@Override
public void cleanup() {
// shutdown previous instance of debugger
synchronized (this) {
if (debugger != null) {
debugger.shutdown();
}
debugger = null;
debuggerConfig = null;
}
}
private int argumentIntValue(Map arguments, String key) {
String v = arguments.get(key);
if (v == null)
throw new CompilerException("Missing required debugger argument " + key);
return Integer.parseInt(v);
}
boolean argumentBoolValue(Map arguments, String key) {
String v = arguments.get(key);
if (v == null)
throw new CompilerException("Missing required debugger argument " + key);
return Boolean.parseBoolean(v);
}
/**
* callback to receive hook port from device to connect debugger to.
* device will print out [DEBUG] hooks: debugPort=
* check hooks.c/_rvmHookSetupTCPChannel for details
* implements hooks connection interface to provide in and out streams
*/
private static class DebuggerLauncherCallback implements AppLauncherCallback, IHooksConnection {
private final static String tag = "[DEBUG] hooks: debugPort=";
private volatile Integer hooksPort;
private IDeviceConnection deviceConnection;
private String incompleteLine;
private AppLauncherInfo launchInfo;
@Override
public void setAppLaunchInfo(AppLauncherInfo info) {
launchInfo = info;
}
@Override
public byte[] filterOutput(byte[] data) {
if (hooksPort == null) {
// port is not received yet, keep working
String str = new String(data, StandardCharsets.UTF_8);
if (incompleteLine != null) {
str = incompleteLine + str;
incompleteLine = null;
}
int lookingPos = 0;
int newLineIdx = str.indexOf('\n');
while (newLineIdx >= 0 ) {
// get next new line
if (str.startsWith(tag, lookingPos)) {
// got it
hooksPort = Integer.parseInt(str.substring(lookingPos + tag.length(), newLineIdx).trim());
break;
} else {
// move to next line
lookingPos = newLineIdx + 1;
newLineIdx = str.indexOf('\n', newLineIdx + 1);
}
}
// keep trailing line (without eol)
if (hooksPort == null && lookingPos < str.length()) {
incompleteLine = lookingPos != 0 ? str.substring(lookingPos) : str;
}
}
return data;
}
/**
* waits till port hooks port is available and establish connection
*/
@Override
public void connect() {
try {
// FIXME: waiting for app to be deployed and prepared, its should not use TARGET_WAIT_TIMEOUT time
// and it has to be moved to pre-launch sequence
long ts = System.currentTimeMillis();
while (launchInfo == null) {
if (System.currentTimeMillis() - ts > DebuggerConfig.TARGET_DEPLOY_TIMEOUT)
throw new DebuggerException("Timeout while waiting app is deployed");
Thread.sleep(200);
}
// waiting for target to start and hooks are available
ts = System.currentTimeMillis();
while (hooksPort == null) {
if (System.currentTimeMillis() - ts > DebuggerConfig.TARGET_WAIT_TIMEOUT)
throw new DebuggerException("Timeout while waiting app is responding on device");
Thread.sleep(200);
}
deviceConnection = launchInfo.getDevice().connect(hooksPort);
} catch (InterruptedException e) {
throw new DebuggerException(e);
}
}
@Override
public void disconnect() {
deviceConnection.close();
}
@Override
public InputStream getInputStream() {
return deviceConnection.getInputStream();
}
@Override
public OutputStream getOutputStream() {
return deviceConnection.getOutputStream();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy