1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
;
5 import com
.google
.appengine
.tools
.admin
.OutputPump
;
6 import com
.google
.appengine
.tools
.development
.DevAppServerMain
;
7 import com
.google
.appengine
.tools
.info
.SdkInfo
;
8 import com
.google
.apphosting
.utils
.config
.AppEngineConfigException
;
9 import com
.google
.apphosting
.utils
.config
.AppEngineWebXml
;
10 import com
.google
.apphosting
.utils
.config
.AppEngineWebXmlReader
;
13 import java
.io
.IOException
;
14 import java
.io
.PrintWriter
;
15 import java
.util
.ArrayList
;
16 import java
.util
.List
;
17 import java
.util
.logging
.Logger
;
20 * Launches a process in an operating-system agnostic way. Helps us avoid
21 * idiosyncrasies in scripts for different platforms. Currently this only
22 * works for DevAppServerMain.
24 * Takes a command line invocation like:
27 * java -cp ../lib/appengine-tools-api.jar com.google.appengine.tools.KickStart \
28 * --jvm_flag="-Dlog4j.configuration=log4j.props"
29 * com.google.appengine.tools.development.DevAppServerMain \
30 * --jvm_flag="-agentlib:jdwp=transport=dt_socket,server=y,address=7000"
31 * --address=localhost --port=5005 appDir
37 * java -cp <an_absolute_path>/lib/appengine-tools-api.jar \
38 * -Dlog4j.configuration=log4j.props \
39 * -agentlib:jdwp=transport=dt_socket,server=y,address=7000 \
40 * com.google.appengine.tools.development.DevAppServerMain \
41 * --address=localhost --port=5005 <an_absolute_path>/appDir
44 * while also setting its working directory (if appropriate).
46 * All arguments between {@code com.google.appengine.tools.KickStart} and
47 * {@code com.google.appengine.tools.development.DevAppServerMain}, as well as
48 * all {@code --jvm_flag} arguments after {@code DevAppServerMain}, are consumed
49 * by KickStart. The remaining options after {@code DevAppServerMain} are
50 * given as arguments to DevAppServerMain, without interpretation by
53 * At present, the only valid option to KickStart itself is:
55 * <DT>--jvm_flag=<vm_arg></DT><DD>Passes <vm_arg> as a JVM
56 * argument for the child JVM. May be repeated.</DD>
58 * Additionally, if the --external_resource_dir argument is specified, we use it
59 * to set the working directory instead of the application war directory.
62 public class KickStart
{
64 private static final Logger logger
= Logger
.getLogger(KickStart
.class.getName());
66 private static final String EXTERNAL_RESOURCE_DIR_FLAG
=
67 "--" + DevAppServerMain
.EXTERNAL_RESOURCE_DIR_ARG
;
68 private static final String EXTERNAL_RESOURCE_DIR_ERROR_MESSAGE
=
69 EXTERNAL_RESOURCE_DIR_FLAG
+ "=<path> expected.";
71 private static final String GENERATE_WAR_FLAG
= "--" + DevAppServerMain
.GENERATE_WAR_ARG
;
73 private static final String GENERATED_WAR_DIR_FLAG
=
74 "--" + DevAppServerMain
.GENERATED_WAR_DIR_ARG
;
76 private static final String NO_JAVA_AGENT_FLAG
= "--no_java_agent";
78 private static final String JVM_FLAG
= "--jvm_flag";
79 private static final String JVM_FLAG_ERROR_MESSAGE
=
80 JVM_FLAG
+ "=<flag> expected.\n" + JVM_FLAG
+ " may be repeated to supply multiple flags";
82 private static final String START_ON_FIRST_THREAD_FLAG
= "--startOnFirstThread";
83 private static final String START_ON_FIRST_THREAD_ERROR_MESSAGE
=
84 START_ON_FIRST_THREAD_FLAG
+ "=<boolean> expected";
86 private static final String SDK_ROOT_FLAG
= "--sdk_root";
87 private static final String SDK_ROOT_ERROR_MESSAGE
= SDK_ROOT_FLAG
+ "=<path> expected";
89 private static final String JACOCO_AGENT_JAR_FLAG
= "--jacoco_agent_jar";
90 private static final String JACOCO_AGENT_JAR_ERROR_MESSAGE
=
91 JACOCO_AGENT_JAR_FLAG
+ "=<path> expected.";
92 private static final String JACOCO_AGENT_ARGS_FLAG
= "--jacoco_agent_args";
93 private static final String JACOCO_AGENT_ARGS_ERROR_MESSAGE
=
94 JACOCO_AGENT_ARGS_FLAG
+ "=<jacoco agent args> expected.";
95 private static final String JACOCO_EXEC_FLAG
= "--jacoco_exec";
96 private static final String JACOCO_EXEC_ERROR_MESSAGE
=
97 JACOCO_EXEC_FLAG
+ "=<path> expected.";
99 private Process serverProcess
= null;
101 public static void main(String
[] args
) {
105 private KickStart(String
[] args
) {
106 String entryClass
= null;
108 ProcessBuilder builder
= new ProcessBuilder();
109 String home
= System
.getProperty("java.home");
110 String javaExe
= home
+ File
.separator
+ "bin" + File
.separator
+ "java";
112 List
<String
> jvmArgs
= new ArrayList
<String
>();
113 ArrayList
<String
> appServerArgs
= new ArrayList
<String
>();
114 String jacocoAgentJarArg
= null;
115 String jacocoAgentArgs
= "";
116 String jacocoExecArg
= "jacoco.exec";
118 List
<String
> command
= builder
.command();
119 command
.add(javaExe
);
121 boolean startOnFirstThread
= System
.getProperty("os.name").equalsIgnoreCase("Mac OS X");
122 String externalResourceDirArg
= null;
123 boolean generateWar
= false;
124 boolean noJavaAgent
= false;
126 for (int i
= 0; i
< args
.length
; i
++) {
127 if (args
[i
].startsWith(EXTERNAL_RESOURCE_DIR_FLAG
)) {
128 externalResourceDirArg
= extractValue(args
[i
], EXTERNAL_RESOURCE_DIR_ERROR_MESSAGE
);
129 } else if (args
[i
].startsWith(GENERATED_WAR_DIR_FLAG
)
130 || args
[i
].startsWith(GENERATE_WAR_FLAG
)) {
132 } else if (args
[i
].startsWith(NO_JAVA_AGENT_FLAG
)) {
135 if (args
[i
].startsWith(JVM_FLAG
)) {
136 jvmArgs
.add(extractValue(args
[i
], JVM_FLAG_ERROR_MESSAGE
));
137 } else if (args
[i
].startsWith(START_ON_FIRST_THREAD_FLAG
)) {
139 Boolean
.valueOf(extractValue(args
[i
], START_ON_FIRST_THREAD_ERROR_MESSAGE
));
140 } else if (args
[i
].startsWith(JACOCO_AGENT_JAR_FLAG
)) {
141 jacocoAgentJarArg
= extractValue(args
[i
], JACOCO_AGENT_JAR_ERROR_MESSAGE
);
142 } else if (args
[i
].startsWith(JACOCO_AGENT_ARGS_FLAG
)) {
143 jacocoAgentArgs
= extractValue(args
[i
], JACOCO_AGENT_ARGS_ERROR_MESSAGE
);
144 } else if (args
[i
].startsWith(JACOCO_EXEC_FLAG
)) {
145 jacocoExecArg
= extractValue(args
[i
], JACOCO_EXEC_ERROR_MESSAGE
);
146 } else if (entryClass
== null) {
147 if (args
[i
].charAt(0) == '-') {
148 throw new IllegalArgumentException("Only --jvm_flag may precede classname, not "
151 entryClass
= args
[i
];
152 if (!entryClass
.equals(DevAppServerMain
.class.getName())) {
153 throw new IllegalArgumentException("KickStart only works for DevAppServerMain");
157 appServerArgs
.add(args
[i
]);
161 if (entryClass
== null) {
162 throw new IllegalArgumentException("missing entry classname");
165 if (externalResourceDirArg
== null && generateWar
) {
167 "Generating a war directory requires " + "--" + EXTERNAL_RESOURCE_DIR_FLAG
);
170 File newWorkingDir
= newWorkingDir(externalResourceDirArg
,
171 appServerArgs
.toArray(new String
[appServerArgs
.size()]));
172 builder
.directory(newWorkingDir
);
174 if (startOnFirstThread
) {
175 jvmArgs
.add("-XstartOnFirstThread");
178 String classpath
= System
.getProperty("java.class.path");
179 StringBuilder newClassPath
= new StringBuilder();
180 assert classpath
!= null : "classpath must not be null";
181 String
[] paths
= classpath
.split(File
.pathSeparator
);
182 for (int i
= 0; i
< paths
.length
; ++i
) {
183 newClassPath
.append(new File(paths
[i
]).getAbsolutePath());
184 if (i
!= paths
.length
- 1) {
185 newClassPath
.append(File
.pathSeparator
);
188 String sdkRoot
= null;
189 String appDir
= null;
190 List
<String
> absoluteAppServerArgs
= new ArrayList
<String
>(appServerArgs
.size());
192 for (int i
= 0; i
< appServerArgs
.size(); ++i
) {
193 String arg
= appServerArgs
.get(i
);
194 if (arg
.startsWith(SDK_ROOT_FLAG
)) {
195 sdkRoot
= new File(extractValue(arg
, SDK_ROOT_ERROR_MESSAGE
)).getAbsolutePath();
196 arg
= SDK_ROOT_FLAG
+ "=" + sdkRoot
;
197 } else if (arg
.startsWith(EXTERNAL_RESOURCE_DIR_FLAG
)) {
198 arg
= EXTERNAL_RESOURCE_DIR_FLAG
+ "="
199 + new File(extractValue(arg
, EXTERNAL_RESOURCE_DIR_ERROR_MESSAGE
)).getAbsolutePath();
200 } else if (i
== appServerArgs
.size() - 1) {
201 if (!arg
.startsWith("-")) {
202 File file
= new File(arg
);
204 arg
= new File(arg
).getAbsolutePath();
209 absoluteAppServerArgs
.add(arg
);
211 if (sdkRoot
== null) {
212 sdkRoot
= SdkInfo
.getSdkRoot().getAbsolutePath();
214 boolean isVM
= false;
215 if (appDir
!= null) {
216 isVM
= isVMRuntime(appDir
);
220 jvmArgs
.add("-D--enable_all_permissions=true");
223 String agentJar
= sdkRoot
+ "/lib/agent/appengine-agent.jar";
224 agentJar
= agentJar
.replace('/', File
.separatorChar
);
225 if (jacocoAgentJarArg
!= null) {
226 jvmArgs
.add("-D--enable_all_permissions=true");
227 String jacocoAgentJar
= new File(jacocoAgentJarArg
).getAbsolutePath();
228 if (!jacocoAgentArgs
.isEmpty()) {
229 jacocoAgentArgs
= jacocoAgentArgs
+ ",";
231 jacocoAgentArgs
= jacocoAgentArgs
+ "destfile=" + new File(jacocoExecArg
).getAbsolutePath();
232 jvmArgs
.add("-javaagent:" + jacocoAgentJar
+ "=" + jacocoAgentArgs
);
233 if (newClassPath
.length() > 0) {
234 newClassPath
.append(File
.pathSeparator
);
236 newClassPath
.append(agentJar
);
238 jvmArgs
.add("-javaagent:" + agentJar
);
242 String jdkOverridesJar
= sdkRoot
+ "/lib/override/appengine-dev-jdk-overrides.jar";
243 jdkOverridesJar
= jdkOverridesJar
.replace('/', File
.separatorChar
);
244 jvmArgs
.add("-Xbootclasspath/p:" + jdkOverridesJar
);
245 command
.addAll(jvmArgs
);
246 command
.add("-classpath");
247 command
.add(newClassPath
.toString());
248 command
.add(entryClass
);
249 command
.add("--property=kickstart.user.dir=" + System
.getProperty("user.dir"));
251 command
.add("--no_java_agent");
253 command
.addAll(absoluteAppServerArgs
);
255 logger
.fine("Executing " + command
);
256 System
.out
.println("Executing " + command
);
258 Runtime
.getRuntime().addShutdownHook(new Thread() {
261 if (serverProcess
!= null) {
262 serverProcess
.destroy();
268 serverProcess
= builder
.start();
269 } catch (IOException e
) {
270 throw new RuntimeException("Unable to start the process", e
);
273 new Thread(new OutputPump(serverProcess
.getInputStream(),
274 new PrintWriter(System
.out
, true))).start();
275 new Thread(new OutputPump(serverProcess
.getErrorStream(),
276 new PrintWriter(System
.err
, true))).start();
279 serverProcess
.waitFor();
280 } catch (InterruptedException e
) {
283 serverProcess
.destroy();
284 serverProcess
= null;
287 private static String
extractValue(String argument
, String errorMessage
) {
288 int indexOfEqualSign
= argument
.indexOf('=');
289 if (indexOfEqualSign
== -1) {
290 throw new IllegalArgumentException(errorMessage
);
292 return argument
.substring(argument
.indexOf('=') + 1);
296 * Encapsulates the logic to determine the working directory that should be set for the dev
297 * appserver process. If one is explicitly specified it will be used. Otherwise the last
298 * command-line argument will be used.
300 * @param workingDirectoryArg An explicitly specified path. If not {@code null} then it will be
302 * @param args The command-line arguments. If {@code workingDirectory} is {@code null} then the
303 * last command-line argument will be used as the working directory.
304 * @return The working directory to use. If the path to an existing directory was not specified
305 * then we exist with a failure.
307 private static File
newWorkingDir(String workingDirectoryArg
, String
[] args
) {
309 if (workingDirectoryArg
!= null) {
310 workingDir
= new File(workingDirectoryArg
);
311 if (!workingDir
.isDirectory()) {
312 System
.err
.println(workingDirectoryArg
+ " is not an existing directory.");
316 if (args
.length
< 1 || args
[args
.length
- 1].startsWith("-")) {
317 new DevAppServerMain().printHelp(System
.out
);
320 workingDir
= new File(args
[args
.length
- 1]);
321 new DevAppServerMain().validateWarPath(workingDir
);
326 static boolean isVMRuntime(String appDir
) {
327 File f
= new File(appDir
, "WEB-INF/appengine-web.xml");
333 AppEngineWebXmlReader reader
= new AppEngineWebXmlReader(appDir
);
334 AppEngineWebXml appEngineWebXml
= reader
.readAppEngineWebXml();
335 return appEngineWebXml
.getUseVm();
336 } catch (AppEngineConfigException e
) {
337 System
.err
.println("Error reading: " + f
.getAbsolutePath());