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; ...@@ -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.UserData;
import com.google.appinventor.server.storage.StoredData.UserFileData; import com.google.appinventor.server.storage.StoredData.UserFileData;
import com.google.appinventor.server.storage.StoredData.UserProjectData; 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.Motd;
import com.google.appinventor.shared.rpc.project.Project; import com.google.appinventor.shared.rpc.project.Project;
import com.google.appinventor.shared.rpc.project.ProjectSourceZip; import com.google.appinventor.shared.rpc.project.ProjectSourceZip;
...@@ -98,7 +99,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -98,7 +99,7 @@ public class ObjectifyStorageIo implements StorageIo {
private class Result<T> { private class Result<T> {
T t; T t;
} }
private FileService fileService; private FileService fileService;
static { static {
...@@ -109,13 +110,14 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -109,13 +110,14 @@ public class ObjectifyStorageIo implements StorageIo {
ObjectifyService.register(FileData.class); ObjectifyService.register(FileData.class);
ObjectifyService.register(UserFileData.class); ObjectifyService.register(UserFileData.class);
ObjectifyService.register(MotdData.class); ObjectifyService.register(MotdData.class);
ObjectifyService.register(RendezvousData.class);
} }
ObjectifyStorageIo() { ObjectifyStorageIo() {
fileService = FileServiceFactory.getFileService(); fileService = FileServiceFactory.getFileService();
initMotd(); initMotd();
} }
// for testing // for testing
ObjectifyStorageIo(FileService fileService) { ObjectifyStorageIo(FileService fileService) {
this.fileService = fileService; this.fileService = fileService;
...@@ -232,7 +234,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -232,7 +234,7 @@ public class ObjectifyStorageIo implements StorageIo {
UserData userData = datastore.find(userKey(userId)); UserData userData = datastore.find(userKey(userId));
if (userData != null) { if (userData != null) {
userData.settings = settings; 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); datastore.put(userData);
} }
} }
...@@ -311,7 +313,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -311,7 +313,7 @@ public class ObjectifyStorageIo implements StorageIo {
public void onNonFatalError() { public void onNonFatalError() {
rememberBlobsToDelete(); rememberBlobsToDelete();
} }
private void rememberBlobsToDelete() { private void rememberBlobsToDelete() {
for (FileData addedFile : addedFiles) { for (FileData addedFile : addedFiles) {
if (addedFile.isBlob && addedFile.blobstorePath != null) { if (addedFile.isBlob && addedFile.blobstorePath != null) {
...@@ -322,7 +324,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -322,7 +324,7 @@ public class ObjectifyStorageIo implements StorageIo {
addedFiles.clear(); addedFiles.clear();
} }
}); });
// second job is on the user entity // second job is on the user entity
runJobWithRetries(new JobRetryHelper() { runJobWithRetries(new JobRetryHelper() {
@Override @Override
...@@ -335,7 +337,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -335,7 +337,7 @@ public class ObjectifyStorageIo implements StorageIo {
datastore.put(upd); datastore.put(upd);
} }
}); });
} catch (ObjectifyException e) { } catch (ObjectifyException e) {
for (FileData addedFile : addedFiles) { for (FileData addedFile : addedFiles) {
if (addedFile.isBlob && addedFile.blobstorePath != null) { if (addedFile.isBlob && addedFile.blobstorePath != null) {
blobsToDelete.add(addedFile.blobstorePath); blobsToDelete.add(addedFile.blobstorePath);
...@@ -1059,7 +1061,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -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 { throws BlobWriteException, ObjectifyException {
// Create a new Blob file with generic mime-type "application/octet-stream" // Create a new Blob file with generic mime-type "application/octet-stream"
AppEngineFile blobstoreFile = null; AppEngineFile blobstoreFile = null;
...@@ -1223,10 +1225,10 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -1223,10 +1225,10 @@ public class ObjectifyStorageIo implements StorageIo {
final Result<String> projectName = new Result<String>(); final Result<String> projectName = new Result<String>();
projectName.t = null; projectName.t = null;
String fileName = null; String fileName = null;
ByteArrayOutputStream zipFile = new ByteArrayOutputStream(); ByteArrayOutputStream zipFile = new ByteArrayOutputStream();
final ZipOutputStream out = new ZipOutputStream(zipFile); final ZipOutputStream out = new ZipOutputStream(zipFile);
try { try {
runJobWithRetries(new JobRetryHelper() { runJobWithRetries(new JobRetryHelper() {
@Override @Override
...@@ -1316,7 +1318,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -1316,7 +1318,7 @@ public class ObjectifyStorageIo implements StorageIo {
} }
} catch (IOException e) { } catch (IOException e) {
throw CrashReport.createAndLogError(LOG, null, throw CrashReport.createAndLogError(LOG, null,
collectProjectErrorInfo(userId, projectId, collectProjectErrorInfo(userId, projectId,
StorageUtil.ANDROID_KEYSTORE_FILENAME), e); StorageUtil.ANDROID_KEYSTORE_FILENAME), e);
} }
} }
...@@ -1357,7 +1359,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -1357,7 +1359,7 @@ public class ObjectifyStorageIo implements StorageIo {
} }
return motd.t; return motd.t;
} }
@Override @Override
public String findUserByEmail(final String email) throws NoSuchElementException { public String findUserByEmail(final String email) throws NoSuchElementException {
Objectify datastore = ObjectifyService.begin(); Objectify datastore = ObjectifyService.begin();
...@@ -1370,6 +1372,46 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -1370,6 +1372,46 @@ public class ObjectifyStorageIo implements StorageIo {
return userData.id; 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() { private void initMotd() {
try { try {
runJobWithRetries(new JobRetryHelper() { runJobWithRetries(new JobRetryHelper() {
...@@ -1381,8 +1423,8 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -1381,8 +1423,8 @@ public class ObjectifyStorageIo implements StorageIo {
firstMotd.id = MOTD_ID; firstMotd.id = MOTD_ID;
firstMotd.caption = "Hello!"; firstMotd.caption = "Hello!";
firstMotd.content = "Welcome to the experimental App Inventor system from MIT. " + 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 " + "This is still a prototype. It would be a good idea to frequently back up " +
"your projects to local storage."; "your projects to local storage.";
datastore.put(firstMotd); datastore.put(firstMotd);
} }
} }
...@@ -1391,7 +1433,7 @@ public class ObjectifyStorageIo implements StorageIo { ...@@ -1391,7 +1433,7 @@ public class ObjectifyStorageIo implements StorageIo {
throw CrashReport.createAndLogError(LOG, null, "Initing MOTD", e); throw CrashReport.createAndLogError(LOG, null, "Initing MOTD", e);
} }
} }
// Create a name for a blob from a project id and file name. This is mostly // 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. // to help with debugging and viewing the blobstore via the admin console.
// We don't currently use these blob names anywhere else. // We don't currently use these blob names anywhere else.
......
...@@ -380,17 +380,41 @@ public interface StorageIo { ...@@ -380,17 +380,41 @@ public interface StorageIo {
boolean includeProjectHistory, boolean includeProjectHistory,
boolean includeAndroidKeystore, boolean includeAndroidKeystore,
@Nullable String zipName) throws IOException; @Nullable String zipName) throws IOException;
/** /**
* Find a user's id given their email address. Note that this query is case * Find a user's id given their email address. Note that this query is case
* sensitive! * sensitive!
* *
* @param email user's email address * @param email user's email address
* *
* @return the user's id if found * @return the user's id if found
* @throws NoSuchElementException if we can't find a user with that exact * @throws NoSuchElementException if we can't find a user with that exact
* email address * email address
*/ */
String findUserByEmail(String email) throws NoSuchElementException; 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 { ...@@ -166,4 +166,20 @@ public class StoredData {
// More MOTD detail, if any // More MOTD detail, if any
String content; 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 @@ ...@@ -10,7 +10,7 @@
<filter-name>odeAuthFilter</filter-name> <filter-name>odeAuthFilter</filter-name>
<filter-class>com.google.appinventor.server.OdeAuthFilter</filter-class> <filter-class>com.google.appinventor.server.OdeAuthFilter</filter-class>
</filter> </filter>
<!-- Filter for application statistics. See: <!-- Filter for application statistics. See:
http://code.google.com/appengine/docs/java/tools/appstats.html http://code.google.com/appengine/docs/java/tools/appstats.html
Note that all requests are logged, including appstats ones. Note that all requests are logged, including appstats ones.
...@@ -43,15 +43,16 @@ ...@@ -43,15 +43,16 @@
<role-name>*</role-name> <role-name>*</role-name>
</auth-constraint> </auth-constraint>
</security-constraint> </security-constraint>
<!-- Security constraint: no security should be used for these urls --> <!-- Security constraint: no security should be used for these urls -->
<security-constraint> <security-constraint>
<web-resource-collection> <web-resource-collection>
<url-pattern>/ode2/*</url-pattern> <url-pattern>/ode2/*</url-pattern>
<url-pattern>/docs/*</url-pattern> <url-pattern>/docs/*</url-pattern>
<url-pattern>/learn/*</url-pattern> <url-pattern>/learn/*</url-pattern>
<url-pattern>/about/*</url-pattern> <url-pattern>/about/*</url-pattern>
<url-pattern>/forum/*</url-pattern> <url-pattern>/forum/*</url-pattern>
<url-pattern>/rendezvous/*</url-pattern>
</web-resource-collection> </web-resource-collection>
</security-constraint> </security-constraint>
...@@ -67,6 +68,16 @@ ...@@ -67,6 +68,16 @@
<!-- Servlets --> <!-- 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 --> <!-- download -->
<servlet> <servlet>
<servlet-name>downloadServlet</servlet-name> <servlet-name>downloadServlet</servlet-name>
...@@ -147,7 +158,7 @@ ...@@ -147,7 +158,7 @@
<servlet-name>userInfoService</servlet-name> <servlet-name>userInfoService</servlet-name>
</filter-mapping> </filter-mapping>
<!-- webstartfile <!-- webstartfile
Note: this servlet does not require user authentication --> Note: this servlet does not require user authentication -->
<servlet> <servlet>
<servlet-name>webStartFileServlet</servlet-name> <servlet-name>webStartFileServlet</servlet-name>
...@@ -199,7 +210,7 @@ ...@@ -199,7 +210,7 @@
<filter-name>odeAuthFilter</filter-name> <filter-name>odeAuthFilter</filter-name>
<servlet-name>launchService</servlet-name> <servlet-name>launchService</servlet-name>
</filter-mapping> </filter-mapping>
<!-- accept tos --> <!-- accept tos -->
<servlet> <servlet>
<servlet-name>tosServlet</servlet-name> <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