/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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 com.google.apphosting.vmruntime;
import static com.google.appengine.repackaged.com.google.common.base.MoreObjects.firstNonNull;
import com.google.appengine.api.utils.SystemProperty;
import com.google.apphosting.base.AppId;
import com.google.apphosting.utils.config.AppEngineWebXml;
import com.google.apphosting.utils.http.HttpRequest;
import com.google.apphosting.utils.http.HttpResponse;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Constants and utility functions shared by the Jetty 6 and Jetty 9 VmRuntimeWebAppContext.
*
*/
public class VmRuntimeUtils {
// This should be kept in sync with HTTPProto::X_GOOGLE_INTERNAL_SKIPADMINCHECK.
private static final String X_GOOGLE_INTERNAL_SKIPADMINCHECK = "X-Google-Internal-SkipAdminCheck";
// This should be kept in sync with HTTPProto::X_APPENGINE_QUEUENAME.
private static final String X_APPENGINE_QUEUENAME = "X-AppEngine-QueueName";
// Keep in sync with com.google.apphosting.utils.jetty[8].AppEngineAuthentication.
private static final String SKIP_ADMIN_CHECK_ATTR =
"com.google.apphosting.internal.SkipAdminCheck";
private static final String VM_API_PROXY_HOST = "appengine.googleapis.internal";
private static final int VM_API_PROXY_PORT = 10001;
public static final long ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
public static final long MAX_REQUEST_THREAD_INTERRUPT_WAIT_TIME_MS = 10;
public static final long MAX_REQUEST_THREAD_API_CALL_WAIT_MS = 30 * 1000;
public static final long MAX_USER_API_CALL_WAIT_MS = 60 * 1000;
// Keep in sync with apphosting/base/http_proto.cc
public static final String LOG_FLUSH_COUNTER_HEADER = "X-AppEngine-Log-Flush-Count";
public static final String ASYNC_API_WAIT_HEADER = "X-AppEngine-Async-Api-Wait";
private static final String MINOR_VERSION_PATTERN = "/home/vmagent/.+_%s-([0-9]+)/root";
/**
* Reads the AppEngine release version from the package specification in the jar file.
*
* @return The AppEngine version (for example 1.7.4), or "unknown" if the class is loaded outside
* appengine-vm-runtime.jar (for example during tests).
*/
private static String getAppEngineRelease() {
Package vmruntimePackage = VmRuntimeUtils.class.getPackage();
if (vmruntimePackage == null || vmruntimePackage.getSpecificationVersion() == null) {
return "unknown";
}
return vmruntimePackage.getSpecificationVersion();
}
/**
* Extract the minor version based on the canonical path of the app.
*
* @param majorVersion The current major version.
* @param path The current resource base.
* @return The minor version.
*/
public static String getMinorVersionFromPath(String majorVersion, String path) {
Pattern pattern = Pattern.compile(String.format(MINOR_VERSION_PATTERN, majorVersion));
Matcher matcher = pattern.matcher(path);
if (matcher.matches()) {
return matcher.group(1);
}
return "0";
}
/**
* Creates the server info string that should be returned by ServletContext.getServerInfo().
*
* See //java/com/google/apphosting/runtime/JavaRuntime.java:runtimeVersion
*/
public static String getServerInfo() {
return "Google App Engine/" + getAppEngineRelease();
}
/**
* Initiates a synchronous log flush and inserts the flush count header into the response. The
* header is added so that the appserver can know when all log API calls are completed and can
* delay the request accordingly.
*
* @param response The response to add the flush count header to.
* @param requestSpecificEnvironment The environment used by the request.
*/
public static void flushLogsAndAddHeader(
HttpResponse response, VmApiProxyEnvironment requestSpecificEnvironment) {
int flushCount = requestSpecificEnvironment.flushLogs();
response.setHeader(VmRuntimeUtils.LOG_FLUSH_COUNTER_HEADER, Integer.toString(flushCount));
}
/**
* Check if the request has the internal "skip admin check" header or comes from a task queue, if
* so set a request attribute so this information can be used by the security handler.
*
* @param request The request to inspect and modify.
*/
public static void handleSkipAdminCheck(HttpRequest request) {
if (request.getHeader(VmRuntimeUtils.X_GOOGLE_INTERNAL_SKIPADMINCHECK) != null
|| request.getHeader(VmRuntimeUtils.X_APPENGINE_QUEUENAME) != null) {
request.setAttribute(VmRuntimeUtils.SKIP_ADMIN_CHECK_ATTR, Boolean.TRUE);
}
}
/**
* Set the Environment system property to Development or Production.
*/
static void setEnvironmentSystemProperty(String partition) {
if ("dev".equals(partition)) {
System.setProperty(
SystemProperty.environment.key(), SystemProperty.Environment.Value.Development.name());
} else {
System.setProperty(
SystemProperty.environment.key(), SystemProperty.Environment.Value.Production.name());
}
}
/**
* Install system properties based on the provided VmApiProxyEnvironment.
*/
public static void installSystemProperties(
VmApiProxyEnvironment environment, AppEngineWebXml appEngineWebXml) {
setEnvironmentSystemProperty(environment.getPartition());
System.setProperty(SystemProperty.version.key(), getServerInfo());
System.setProperty(
SystemProperty.applicationId.key(), AppId.parse(environment.getAppId()).getLongAppId());
System.setProperty(SystemProperty.applicationVersion.key(), environment.getVersionId());
System.setProperty("appengine.jetty.also_log_to_apiproxy", "true");
if (appEngineWebXml!=null) {
for (Map.Entry<String, String> entry : appEngineWebXml.getSystemProperties().entrySet()) {
System.setProperty(entry.getKey(), entry.getValue());
}
}
}
/**
* Interrupt any request threads created by the current request.
*
* @param requestEnvironment A request specific environment containing the thread factory that
* created the threads.
* @param millis Wait up to @code{millis} milliseconds for all request threads to die.
*/
public static void interruptRequestThreads(
VmApiProxyEnvironment requestEnvironment, long millis) {
Object threadFactory =
requestEnvironment.getAttributes().get(VmApiProxyEnvironment.REQUEST_THREAD_FACTORY_ATTR);
if (threadFactory != null && threadFactory instanceof VmRequestThreadFactory) {
VmRequestThreadFactory vmRequestThreadFactory = (VmRequestThreadFactory) threadFactory;
vmRequestThreadFactory.interruptRequestThreads();
vmRequestThreadFactory.join(millis);
}
}
/**
* Waits for all Async API calls made with the provided environment to complete and injects the
* number of milliseconds it took into a header of the response.
*
* @param requestEnvironment The request specific API environment.
* @param response The response to add header to.
* @return True if all calls completed before the timeout fired or the thread was interrupted.
* False otherwise.
*/
public static boolean waitForAsyncApiCalls(
VmApiProxyEnvironment requestEnvironment, HttpResponse response) {
long startTime = System.currentTimeMillis();
boolean success =
requestEnvironment.waitForAllApiCallsToComplete(MAX_REQUEST_THREAD_API_CALL_WAIT_MS);
long elapsed = System.currentTimeMillis() - startTime;
response.setHeader(ASYNC_API_WAIT_HEADER, Long.toString(elapsed));
return success;
}
/**
* Returns the host:port of the API server.
*
* @return If environment variables API_HOST or API_PORT port are set the host and/or port is
* calculated from them. Otherwise the default host:port is used.
*/
public static String getApiServerAddress(){
String server = firstNonNull(System.getenv("API_HOST"), VM_API_PROXY_HOST);
String port = firstNonNull(System.getenv("API_PORT"), "" + VM_API_PROXY_PORT);
return server + ":" + port;
}
}