Commit aae9d54f authored by Jeffrey I. Schiller's avatar Jeffrey I. Schiller Committed by Gerrit Review System

Add RendezvousServlet to App Inventor Server. This is used to help the

Blocks Editor find the associated phone for debugging over wireless.

Change-Id: I8f825746b4d2df2a31ce6f6898bdc2009280ddd3
parent 36c393b2
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt
/*
* RendezvousServlet -- This servlet acts as the rendezvous point
* between the blocks editor and a phone being used for debugging
* with a WiFi connection. This was originally written in Python
* using the "Bottle" micro-framework. Here is that code:
*
* #!/usr/bin/python
* from bottle import run,route,app,request,response,template,default_app,Bottle,debug,abort
* from flup.server.fcgi import WSGIServer
* from cStringIO import StringIO
* import memcache
*
* app = Bottle()
* default_app.push(app)
*
* @route('/', method='POST')
* def store():
* c = memcache.Client(['127.0.0.1:11211',])
* key = request.POST.get('key')
* if not key:
* abort(404, 'No Key Specified')
* d = {}
* for k,v in request.POST.items():
* d[k] = v
* c.set('rr-%s' % key, d, 1800)
* return d
*
* @route('/<key>')
* def fetch(key):
* c = memcache.Client(['127.0.0.1:11211',])
* return c.get('rr-%s' % key)
*
* debug(True)
*
* ##run(host='127.0.0.1', port=8080)
* WSGIServer(app).run()
*
* # End of Python Code
*
* This code is a little bit more complicated. In part because it
* is written in Java and it is intended to be run within the
* Google App Engine. The App Engine can sometimes disable
* memcache, which this code uses both for speed and to avoid
* using the datastore for data which is valuable for typically
* 10 to 15 seconds!
*
* When memcache is disabled we use the datastore. However when
* memcache is available we do *NOT* use the datastore at
* all. This is a little different from the way most code uses
* memcache, literally as a cache in front of a real data
* store. Again, this is driven by the desire for speed and the
* short life of the data itself.
*
* Note: At the moment there is no code to cleaup entries left in
* the datastore. However each entry is marked with a used date,
* so it is pretty easy to write code at a later date to remove
* stale entries. Where stale can be defined to be data that is
* more then a few minutes old!
*
*/
package com.google.appinventor.server;
import com.google.appengine.api.capabilities.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.ServletException;
import java.io.PrintWriter;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.HashMap;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.memcache.Expiration;
import org.json.JSONObject;
import org.json.JSONException;
import com.google.appinventor.server.storage.StorageIo;
import com.google.appinventor.server.storage.StorageIoInstanceHolder;
@SuppressWarnings("unchecked")
public class RendezvousServlet extends HttpServlet {
private final MemcacheService memcache = MemcacheServiceFactory.getMemcacheService();
private final String rendezvousuuid = "c96d8ac6-e571-48bb-9e1f-58df18574e43"; // UUID Generated by JIS
private final StorageIo storageIo = StorageIoInstanceHolder.INSTANCE;
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String uriComponents[] = req.getRequestURI().split("/", 5);
String key = uriComponents[uriComponents.length-1];
resp.setContentType("text/plain");
PrintWriter out = resp.getWriter();
JSONObject jsonObject = new JSONObject();
if (memcacheNotAvailable()) {
// Don't have memcache at the moment, use the data store.
String ipAddress = storageIo.findIpAddressByKey(key);
if (ipAddress == null) {
// out.println("");
} else {
try {
jsonObject.put("key", key);
jsonObject.put("ipaddr", ipAddress);
} catch (JSONException e) {
e.printStackTrace();
}
out.println(jsonObject.toString());
}
return;
}
Object value = memcache.get(rendezvousuuid + key);
if (value == null) {
// out.println("");
return;
}
if (value instanceof Map) {
Map map = (Map<String, String>) value;
for (Object mkey : map.keySet()) {
try {
jsonObject.put((String) mkey, map.get(mkey));
} catch (JSONException e) {
e.printStackTrace();
}
}
out.println(jsonObject.toString());
} else
out.println("");
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
PrintWriter out = resp.getWriter();
BufferedReader input = new BufferedReader(new InputStreamReader(req.getInputStream()));
String queryString = input.readLine();
if (queryString == null) {
out.println("queryString is null");
return;
}
HashMap<String, String> params = getQueryMap(queryString);
String key = params.get("key");
if (key == null) {
out.println("no key");
return;
}
if (memcacheNotAvailable()) {
String ipAddress = params.get("ipaddr");
if (ipAddress == null) { // Malformed request
out.println("no ipaddress");
return;
}
storageIo.storeIpAddressByKey(key, ipAddress);
out.println("OK (Datastore)");
return;
}
memcache.put(rendezvousuuid + key, params, Expiration.byDeltaSeconds(300));
out.println("OK");
}
public void destroy() {
super.destroy();
}
private static HashMap<String, String> getQueryMap(String query) {
String[] params = query.split("&");
HashMap<String, String> map = new HashMap<String, String>();
for (String param : params) {
String name = param.split("=")[0];
String value = param.split("=")[1];
map.put(name, value);
}
return map;
}
private boolean memcacheNotAvailable() {
CapabilitiesService service = CapabilitiesServiceFactory.getCapabilitiesService();
CapabilityStatus status = service.getStatus(Capability.MEMCACHE).getStatus();
if (status == CapabilityStatus.DISABLED) {
return true;
} else {
return false;
}
}
}
......@@ -21,6 +21,7 @@ import com.google.appinventor.server.storage.StoredData.ProjectData;
import com.google.appinventor.server.storage.StoredData.UserData;
import com.google.appinventor.server.storage.StoredData.UserFileData;
import com.google.appinventor.server.storage.StoredData.UserProjectData;
import com.google.appinventor.server.storage.StoredData.RendezvousData;
import com.google.appinventor.shared.rpc.Motd;
import com.google.appinventor.shared.rpc.project.Project;
import com.google.appinventor.shared.rpc.project.ProjectSourceZip;
......@@ -98,7 +99,7 @@ public class ObjectifyStorageIo implements StorageIo {
private class Result<T> {
T t;
}
private FileService fileService;
static {
......@@ -109,13 +110,14 @@ public class ObjectifyStorageIo implements StorageIo {
ObjectifyService.register(FileData.class);
ObjectifyService.register(UserFileData.class);
ObjectifyService.register(MotdData.class);
ObjectifyService.register(RendezvousData.class);
}
ObjectifyStorageIo() {
fileService = FileServiceFactory.getFileService();
initMotd();
}
// for testing
ObjectifyStorageIo(FileService fileService) {
this.fileService = fileService;
......@@ -232,7 +234,7 @@ public class ObjectifyStorageIo implements StorageIo {
UserData userData = datastore.find(userKey(userId));
if (userData != null) {
userData.settings = settings;
userData.visited = new Date(); // Indicate that this person was active now
userData.visited = new Date(); // Indicate that this person was active now
datastore.put(userData);
}
}
......@@ -311,7 +313,7 @@ public class ObjectifyStorageIo implements StorageIo {
public void onNonFatalError() {
rememberBlobsToDelete();
}
private void rememberBlobsToDelete() {
for (FileData addedFile : addedFiles) {
if (addedFile.isBlob && addedFile.blobstorePath != null) {
......@@ -322,7 +324,7 @@ public class ObjectifyStorageIo implements StorageIo {
addedFiles.clear();
}
});
// second job is on the user entity
runJobWithRetries(new JobRetryHelper() {
@Override
......@@ -335,7 +337,7 @@ public class ObjectifyStorageIo implements StorageIo {
datastore.put(upd);
}
});
} catch (ObjectifyException e) {
} catch (ObjectifyException e) {
for (FileData addedFile : addedFiles) {
if (addedFile.isBlob && addedFile.blobstorePath != null) {
blobsToDelete.add(addedFile.blobstorePath);
......@@ -1059,7 +1061,7 @@ public class ObjectifyStorageIo implements StorageIo {
}
}
private String uploadToBlobstore(byte[] content, String name)
private String uploadToBlobstore(byte[] content, String name)
throws BlobWriteException, ObjectifyException {
// Create a new Blob file with generic mime-type "application/octet-stream"
AppEngineFile blobstoreFile = null;
......@@ -1223,10 +1225,10 @@ public class ObjectifyStorageIo implements StorageIo {
final Result<String> projectName = new Result<String>();
projectName.t = null;
String fileName = null;
ByteArrayOutputStream zipFile = new ByteArrayOutputStream();
final ZipOutputStream out = new ZipOutputStream(zipFile);
try {
runJobWithRetries(new JobRetryHelper() {
@Override
......@@ -1316,7 +1318,7 @@ public class ObjectifyStorageIo implements StorageIo {
}
} catch (IOException e) {
throw CrashReport.createAndLogError(LOG, null,
collectProjectErrorInfo(userId, projectId,
collectProjectErrorInfo(userId, projectId,
StorageUtil.ANDROID_KEYSTORE_FILENAME), e);
}
}
......@@ -1357,7 +1359,7 @@ public class ObjectifyStorageIo implements StorageIo {
}
return motd.t;
}
@Override
public String findUserByEmail(final String email) throws NoSuchElementException {
Objectify datastore = ObjectifyService.begin();
......@@ -1370,6 +1372,46 @@ public class ObjectifyStorageIo implements StorageIo {
return userData.id;
}
@Override
public String findIpAddressByKey(final String key) {
Objectify datastore = ObjectifyService.begin();
RendezvousData data = datastore.query(RendezvousData.class).filter("key", key).get();
if (data == null) {
return null;
} else {
return data.ipAddress;
}
}
@Override
public void storeIpAddressByKey(final String key, final String ipAddress) {
Objectify datastore = ObjectifyService.begin();
final RendezvousData data = datastore.query(RendezvousData.class).filter("key", key).get();
try {
runJobWithRetries(new JobRetryHelper() {
@Override
public void run(Objectify datastore) {
RendezvousData new_data = null;
if (data == null) {
new_data = new RendezvousData();
new_data.id = null;
new_data.key = key;
new_data.ipAddress = ipAddress;
new_data.used = new Date(); // So we can cleanup old entries
datastore.put(new_data);
} else {
new_data = data;
new_data.ipAddress = ipAddress;
new_data.used = new Date();
datastore.put(new_data);
}
}
});
} catch (ObjectifyException e) {
throw CrashReport.createAndLogError(LOG, null, null, e);
}
}
private void initMotd() {
try {
runJobWithRetries(new JobRetryHelper() {
......@@ -1381,8 +1423,8 @@ public class ObjectifyStorageIo implements StorageIo {
firstMotd.id = MOTD_ID;
firstMotd.caption = "Hello!";
firstMotd.content = "Welcome to the experimental App Inventor system from MIT. " +
"This is still a prototype. It would be a good idea to frequently back up " +
"your projects to local storage.";
"This is still a prototype. It would be a good idea to frequently back up " +
"your projects to local storage.";
datastore.put(firstMotd);
}
}
......@@ -1391,7 +1433,7 @@ public class ObjectifyStorageIo implements StorageIo {
throw CrashReport.createAndLogError(LOG, null, "Initing MOTD", e);
}
}
// Create a name for a blob from a project id and file name. This is mostly
// to help with debugging and viewing the blobstore via the admin console.
// We don't currently use these blob names anywhere else.
......
......@@ -380,17 +380,41 @@ public interface StorageIo {
boolean includeProjectHistory,
boolean includeAndroidKeystore,
@Nullable String zipName) throws IOException;
/**
* Find a user's id given their email address. Note that this query is case
* sensitive!
*
*
* @param email user's email address
*
*
* @return the user's id if found
* @throws NoSuchElementException if we can't find a user with that exact
* email address
*/
String findUserByEmail(String email) throws NoSuchElementException;
/**
* Find a phone's IP address given the six character key. Used by the
* RendezvousServlet. This is used only when memcache is unavailable.
*
* @param key the six character key
* @return Ip Address as string or null if not found
*
*/
String findIpAddressByKey(String key);
/**
* Store a phone's IP address indexed by six character key. Used by the
* RendezvousServlet. This is used only when memcache is unavailable.
*
* Note: Nothing currently cleans up these entries, but we have a
* timestamp field which we update so a later process can recognize
* and remove stale entries.
*
* @param key the six character key
* @param ipAddress the IP Address of the phone
*
*/
void storeIpAddressByKey(String key, String ipAddress);
}
......@@ -166,4 +166,20 @@ public class StoredData {
// More MOTD detail, if any
String content;
}
// Rendezvous Data -- Only used when memcache is unavailable
@Unindexed
static final class RendezvousData {
@Id Long id;
// Six character key entered by user (or scanned).
@Indexed public String key;
// Ip Address of phone
public String ipAddress;
public Date used; // Used during (manual) cleanup to determine if this entry can be pruned
}
}
......@@ -10,7 +10,7 @@
<filter-name>odeAuthFilter</filter-name>
<filter-class>com.google.appinventor.server.OdeAuthFilter</filter-class>
</filter>
<!-- Filter for application statistics. See:
http://code.google.com/appengine/docs/java/tools/appstats.html
Note that all requests are logged, including appstats ones.
......@@ -43,15 +43,16 @@
<role-name>*</role-name>
</auth-constraint>
</security-constraint>
<!-- Security constraint: no security should be used for these urls -->
<security-constraint>
<web-resource-collection>
<url-pattern>/ode2/*</url-pattern>
<url-pattern>/docs/*</url-pattern>
<url-pattern>/learn/*</url-pattern>
<url-pattern>/learn/*</url-pattern>
<url-pattern>/about/*</url-pattern>
<url-pattern>/forum/*</url-pattern>
<url-pattern>/rendezvous/*</url-pattern>
</web-resource-collection>
</security-constraint>
......@@ -67,6 +68,16 @@
<!-- Servlets -->
<!-- rendezvious -->
<servlet>
<servlet-name>rendezvousServlet</servlet-name>
<servlet-class>com.google.appinventor.server.RendezvousServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>rendezvousServlet</servlet-name>
<url-pattern>/rendezvous/*</url-pattern>
</servlet-mapping>
<!-- download -->
<servlet>
<servlet-name>downloadServlet</servlet-name>
......@@ -147,7 +158,7 @@
<servlet-name>userInfoService</servlet-name>
</filter-mapping>
<!-- webstartfile
<!-- webstartfile
Note: this servlet does not require user authentication -->
<servlet>
<servlet-name>webStartFileServlet</servlet-name>
......@@ -199,7 +210,7 @@
<filter-name>odeAuthFilter</filter-name>
<servlet-name>launchService</servlet-name>
</filter-mapping>
<!-- accept tos -->
<servlet>
<servlet-name>tosServlet</servlet-name>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment