5 Commits

Author SHA1 Message Date
zzz
953533c6e3 0.12.0 2014-11-13 20:15:02 +00:00
zzz
2e0f7ae4bf Release all resources when shut down (requires 0.9.16-6 or higher)
log tweaks
2014-11-13 16:01:15 +00:00
zzz
17e5595dd0 Don't overwrite index.html and robots.txt in the update
More build and clean fixes
2014-11-12 23:10:19 +00:00
zzz
8350d009d4 Add zzzot.config file to set interval 2014-11-12 15:19:13 +00:00
zzz
a7225a2379 Fix parameter decoding for scrape also
Add caching for info hashes and peer ids; move from ByteArray to SDS
Stop cleaner when plugin stops
Move to the ClientApp interface, remove all static refs
Attempt to fix crash after update
2014-11-12 14:48:51 +00:00
14 changed files with 388 additions and 90 deletions

View File

@@ -1,3 +1,14 @@
0.12.0
2014-11-13
Fix parameter decoding for scrape also
Add caching for info hashes and peer ids
Stop cleaner when plugin stops
Move to the ClientApp interface, remove all static refs
Attempt to fix crash after update
Add zzzot.config file to set interval
Don't overwrite index.html and robots.txt in the update
Release all resources when shut down (requires 0.9.16-6 or higher)
0.11.0
2014-11-11
Critical fix for announce parameter decoding, triggered by recent Jetty versions

View File

@@ -8,10 +8,14 @@
</target>
<target name="plugin" depends="war">
<delete file="plugin/i2ptunnel.config" />
<delete>
<!-- in installer but not update -->
<fileset dir="plugin/" includes="i2ptunnel.config zzzot.config eepsite/docroot/index.html eepsite/docroot/robots.txt" />
</delete>
<delete dir="plugin/eepsite/docroot/torrents/" />
<!-- get version number -->
<buildnumber file="scripts/build.number" />
<property name="release.number" value="0.11.0" />
<property name="release.number" value="0.12.0" />
<!-- make the update xpi2p -->
<!-- this contains everything except i2ptunnel.config -->
@@ -48,8 +52,13 @@
<move file="zzzot.su3" tofile="zzzot-update.su3" overwrite="true" />
<!-- make the install xpi2p -->
<copy file="scripts/i2ptunnel.config" todir="plugin/" overwrite="true" />
<copy file="scripts/plugin.config" todir="plugin/" overwrite="true" />
<!-- Files in installer but not update. Be sure to Add to delete fileset above and clean target below -->
<copy file="scripts/i2ptunnel.config" todir="plugin/" overwrite="true" />
<copy file="scripts/zzzot.config" todir="plugin/" overwrite="true" />
<copy file="scripts/index.html" todir="plugin/eepsite/docroot/" overwrite="true" />
<copy file="scripts/robots.txt" todir="plugin/eepsite/docroot/" overwrite="true" />
<mkdir dir="plugin/eepsite/docroot/torrents/" />
<exec executable="echo" osfamily="unix" failonerror="true" output="plugin/plugin.config" append="true">
<arg value="version=${release.number}-b${build.number}" />
</exec>
@@ -64,6 +73,9 @@
<ant dir="src" target="clean" />
<delete file="plugin/i2ptunnel.config" />
<delete file="plugin/plugin.config" />
<delete file="plugin/zzzot.config" />
<delete file="plugin/eepsite/docroot/index.html" />
<delete file="plugin/eepsite/docroot/robots.txt" />
<delete file="plugin/lib/zzzot.jar.pack" />
<delete file="plugin/eepsite/webapps/tracker.war.pack" />
<delete file="plugin/LICENSE.txt" />
@@ -73,6 +85,7 @@
<delete file="zzzot.su3" />
<delete file="zzzot-update.su3" />
<delete file="plugin.zip" />
<delete dir="plugin/eepsite/docroot/torrents/" />
</target>
</project>

3
scripts/zzzot.config Normal file
View File

@@ -0,0 +1,3 @@
# announce interval in seconds
# minimum 900 (15 minutes), maximum 21600 (6 hours)
interval=1620

View File

@@ -16,22 +16,20 @@ package net.i2p.zzzot;
*
*/
import java.io.UnsupportedEncodingException;
import net.i2p.data.ByteArray;
import net.i2p.data.SimpleDataStructure;
/**
* A 20-byte SHA1 info hash
*/
public class InfoHash extends ByteArray {
public class InfoHash extends SimpleDataStructure {
public InfoHash(String data) throws UnsupportedEncodingException {
this(data.getBytes("ISO-8859-1"));
}
public static final int LENGTH = 20;
public InfoHash(byte[] data) {
super(data);
if (data.length != 20)
throw new IllegalArgumentException("Bad infohash length: " + data.length);
}
public int length() {
return LENGTH;
}
}

View File

@@ -16,22 +16,20 @@ package net.i2p.zzzot;
*
*/
import java.io.UnsupportedEncodingException;
import net.i2p.data.ByteArray;
import net.i2p.data.SimpleDataStructure;
/**
* A 20-byte peer ID
*/
public class PID extends ByteArray {
public class PID extends SimpleDataStructure {
public PID(String data) throws UnsupportedEncodingException {
this(data.getBytes("ISO-8859-1"));
}
public static final int LENGTH = 20;
public PID(byte[] data) {
super(data);
if (data.length != 20)
throw new IllegalArgumentException("Bad peer ID length: " + data.length);
}
public int length() {
return LENGTH;
}
}

View File

@@ -18,13 +18,30 @@ package net.i2p.zzzot;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.CoreVersion;
import net.i2p.data.DataHelper;
import net.i2p.data.SDSCache;
import net.i2p.util.VersionComparator;
/**
* All the torrents
*/
public class Torrents extends ConcurrentHashMap<InfoHash, Peers> {
public Torrents() {
private static final int CACHE_SIZE = 2048;
private final SDSCache<InfoHash> _hashCache;
private final SDSCache<PID> _pidCache;
private final Integer _interval;
/**
* @param interval in seconds
*/
public Torrents(int interval) {
super();
_hashCache = new SDSCache<InfoHash>(InfoHash.class, InfoHash.LENGTH, CACHE_SIZE);
_pidCache = new SDSCache<PID>(PID.class, PID.LENGTH, CACHE_SIZE);
_interval = Integer.valueOf(interval);
}
public int countPeers() {
@@ -34,4 +51,60 @@ public class Torrents extends ConcurrentHashMap<InfoHash, Peers> {
}
return rv;
}
/**
* @return in seconds
* @since 0.12.0
*/
public Integer getInterval() {
return _interval;
}
/**
* Pull from cache or return new
*
* @throws IllegalArgumentException if data is not the correct number of bytes
* @since 0.12.0
*/
public InfoHash createInfoHash(String data) throws IllegalArgumentException {
byte[] d = DataHelper.getASCII(data);
if (d.length != InfoHash.LENGTH)
throw new IllegalArgumentException("bad infohash length " + d.length);
return _hashCache.get(d);
}
/**
* Pull from cache or return new
*
* @throws IllegalArgumentException if data is not the correct number of bytes
* @since 0.12.0
*/
public PID createPID(String data) throws IllegalArgumentException {
byte[] d = DataHelper.getASCII(data);
if (d.length != PID.LENGTH)
throw new IllegalArgumentException("bad peer id length " + d.length);
return _pidCache.get(d);
}
/**
* @since 0.12.0
*/
@Override
public void clear() {
super.clear();
clearCaches();
}
/**
* @since 0.12.0
*/
private void clearCaches() {
// not available until 0.9.17
if (VersionComparator.comp(CoreVersion.VERSION, "0.9.17") >= 0) {
try {
_hashCache.clear();
_pidCache.clear();
} catch (Throwable t) {}
}
}
}

View File

@@ -17,34 +17,62 @@ package net.i2p.zzzot;
*/
import java.util.Iterator;
import java.util.Properties;
import net.i2p.util.SimpleScheduler;
import net.i2p.util.SimpleTimer;
import net.i2p.I2PAppContext;
import net.i2p.util.SimpleTimer2;
/**
* Instantiate this to fire it up
*/
class ZzzOT {
private Torrents _torrents;
private static final long CLEAN_TIME = 4*60*1000;
private static final long EXPIRE_TIME = 60*60*1000;
private final Torrents _torrents;
private final Cleaner _cleaner;
private final long EXPIRE_TIME;
ZzzOT() {
_torrents = new Torrents();
SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME);
private static final String PROP_INTERVAL = "interval";
private static final long CLEAN_TIME = 4*60*1000;
private static final int DEFAULT_INTERVAL = 27*60;
private static final int MIN_INTERVAL = 15*60;
private static final int MAX_INTERVAL = 6*60*60;
ZzzOT(I2PAppContext ctx, Properties p) {
String intv = p.getProperty(PROP_INTERVAL);
int interval = DEFAULT_INTERVAL;
if (intv != null) {
try {
interval = Integer.parseInt(intv);
if (interval < MIN_INTERVAL)
interval = MIN_INTERVAL;
else if (interval > MAX_INTERVAL)
interval = MAX_INTERVAL;
} catch (NumberFormatException nfe) {}
}
_torrents = new Torrents(interval);
EXPIRE_TIME = 1000 * (interval + interval / 2);
_cleaner = new Cleaner(ctx);
}
Torrents getTorrents() {
return _torrents;
}
void stop() {
_torrents.clear();
// no way to stop the cleaner
void start() {
_cleaner.forceReschedule(CLEAN_TIME);
}
private class Cleaner implements SimpleTimer.TimedEvent {
void stop() {
_cleaner.cancel();
_torrents.clear();
}
private class Cleaner extends SimpleTimer2.TimedEvent {
/** must schedule later */
public Cleaner(I2PAppContext ctx) {
super(ctx.simpleTimer2());
}
public void timeReached() {
long now = System.currentTimeMillis();
@@ -61,6 +89,7 @@ class ZzzOT {
if (recent <= 0)
iter.remove();
}
schedule(CLEAN_TIME);
}
}
}

View File

@@ -22,18 +22,25 @@ import java.io.IOException;
import java.util.List;
import java.util.Properties;
import net.i2p.CoreVersion;
import net.i2p.I2PAppContext;
import net.i2p.app.ClientApp;
import net.i2p.app.ClientAppManager;
import net.i2p.app.ClientAppState;
import static net.i2p.app.ClientAppState.*;
import net.i2p.apps.systray.UrlLauncher;
import net.i2p.data.Base32;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.PrivateKeyFile;
import net.i2p.i2ptunnel.TunnelController;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
import net.i2p.i2ptunnel.TunnelController;
import net.i2p.apps.systray.UrlLauncher;
import net.i2p.util.VersionComparator;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.xml.XmlConfiguration;
/**
@@ -47,39 +54,77 @@ import org.eclipse.jetty.xml.XmlConfiguration;
*
* @author zzz
*/
public class ZzzOTController {
private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(ZzzOTController.class);
private static Server _server;
private static TunnelController _tunnel;
private static ZzzOT _zzzot;
private static Object _lock = new Object();
public class ZzzOTController implements ClientApp {
private final I2PAppContext _context;
private final Log _log;
private final String[] _args;
private final ClientAppManager _mgr;
private Server _server;
private TunnelController _tunnel;
private final ZzzOT _zzzot;
/** only for main() */
private static volatile ZzzOTController _controller;
private ClientAppState _state = UNINITIALIZED;
private static final String NAME = "ZzzOT";
private static final String CONFIG_FILE = "zzzot.config";
private static final String BACKUP_SUFFIX = ".jetty6";
private static final String[] xmlFiles = {
"jetty.xml", "contexts/base-context.xml", "contexts/cgi-context.xml",
"etc/realm.properties", "etc/webdefault.xml" };
public static void main(String args[]) {
if (args.length != 3 || (!"-d".equals(args[0])))
throw new IllegalArgumentException("Usage: PluginController -d $PLUGIN [start|stop]");
if ("start".equals(args[2]))
start(args);
else if ("stop".equals(args[2]))
stop();
else
throw new IllegalArgumentException("Usage: PluginController -d $PLUGIN [start|stop]");
}
public static Torrents getTorrents() {
synchronized(_lock) {
if (_zzzot == null)
_zzzot = new ZzzOT();
/**
* @since 0.12.0
*/
public ZzzOTController(I2PAppContext ctx, ClientAppManager mgr, String args[]) {
_context = ctx;
_log = ctx.logManager().getLog(ZzzOTController.class);
_mgr = mgr;
_args = args;
File cfile = new File(_context.getAppDir(), "plugins/zzzot/" + CONFIG_FILE);
Properties props = new Properties();
if (cfile.exists()) {
try {
DataHelper.loadProps(props, cfile);
} catch (IOException ioe) {
_log.error("Failed loading zzzot config from " + cfile, ioe);
}
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("No config file " + cfile);
}
return _zzzot.getTorrents();
_zzzot = new ZzzOT(ctx, props);
_state = INITIALIZED;
}
private static void start(String args[]) {
File pluginDir = new File(args[1]);
/**
* No longer supported, as we now need the ClientAppManager for the webapp to find us
*/
public synchronized static void main(String args[]) {
throw new UnsupportedOperationException("Must use ClientApp interface");
}
/**
* @return null if not running
*/
public static Torrents getTorrents() {
ClientAppManager mgr = I2PAppContext.getGlobalContext().clientAppManager();
if (mgr == null)
return null;
ClientApp z = mgr.getRegisteredApp(NAME);
if (z == null)
return null;
ZzzOTController ctrlr = (ZzzOTController) z;
return ctrlr._zzzot.getTorrents();
}
/**
* @param args ignored
*/
private void start(String args[]) {
//File pluginDir = new File(args[1]);
File pluginDir = new File(_context.getAppDir(), "plugins/zzzot");
if (!pluginDir.exists())
throw new IllegalArgumentException("Plugin directory " + pluginDir.getAbsolutePath() + " does not exist");
@@ -105,11 +150,12 @@ public class ZzzOTController {
}
startJetty(pluginDir, dest);
startI2PTunnel(pluginDir, dest);
_zzzot.start();
// SeedlessAnnouncer.announce(_tunnel);
}
private static void startI2PTunnel(File pluginDir, Destination dest) {
private void startI2PTunnel(File pluginDir, Destination dest) {
File i2ptunnelConfig = new File(pluginDir, "i2ptunnel.config");
Properties i2ptunnelProps = new Properties();
try {
@@ -131,15 +177,15 @@ public class ZzzOTController {
_tunnel = tun;
}
private static void startJetty(File pluginDir, Destination dest) {
private void startJetty(File pluginDir, Destination dest) {
if (_server != null)
throw new IllegalArgumentException("Jetty already running!");
migrateJettyXML(pluginDir);
I2PAppContext context = I2PAppContext.getGlobalContext();
File tmpdir = new File(context.getTempDir().getAbsolutePath(), "/zzzot-work");
File tmpdir = new File(_context.getTempDir().getAbsolutePath(), "/zzzot-work");
tmpdir.mkdir();
File jettyXml = new File(pluginDir, "jetty.xml");
try {
Resource.setDefaultUseCaches(false);
XmlConfiguration xmlc = new XmlConfiguration(jettyXml.toURI().toURL());
Server serv = (Server) xmlc.configure();
//HttpContext[] hcs = serv.getContexts();
@@ -155,18 +201,29 @@ public class ZzzOTController {
launchHelp(pluginDir, dest);
}
private static void stop() {
private void stop() {
stopI2PTunnel();
stopJetty();
if (_zzzot != null)
_zzzot.stop();
_zzzot.stop();
}
private static void stopI2PTunnel() {
private void stopI2PTunnel() {
if (_tunnel == null)
return;
try {
_tunnel.stopTunnel();
// destroyTunnel() not available until 0.9.17, but we put 0.9.16 here for now
// so we get testing in 0.9.16-6 or later dev builds.
// No access to RouterVersion here.
// TODO change to 0.9.17
if (VersionComparator.comp(CoreVersion.VERSION, "0.9.16") >= 0) {
try {
_tunnel.destroyTunnel();
} catch (Throwable t) {
_tunnel.stopTunnel();
}
} else {
_tunnel.stopTunnel();
}
} catch (Throwable t) {
_log.error("ZzzOT tunnel stop failed", t);
throw new IllegalArgumentException("Tunnel stop failed " + t);
@@ -174,7 +231,7 @@ public class ZzzOTController {
_tunnel = null;
}
private static void stopJetty() {
private void stopJetty() {
if (_server == null)
return;
try {
@@ -190,7 +247,7 @@ public class ZzzOTController {
* Migate the jetty configuration files.
* Save old jetty.xml if moving from jetty 5 to jetty 6
*/
private static void migrateJettyXML(File pluginDir) {
private void migrateJettyXML(File pluginDir) {
// contexts dir does not exist in Jetty 5
File file = new File(pluginDir, "contexts");
file.mkdir();
@@ -220,7 +277,7 @@ public class ZzzOTController {
* @return success
* @since Jetty 7
*/
private static boolean backupAndMigrateFile(File toDir, String filename) {
private boolean backupAndMigrateFile(File toDir, String filename) {
File to = new File(toDir, filename);
boolean rv = backupFile(to);
boolean rv2 = migrateJettyFile(toDir, filename);
@@ -249,7 +306,7 @@ public class ZzzOTController {
/**
* Migate a single jetty config file, replacing $PLUGIN as we copy it.
*/
private static boolean migrateJettyFile(File pluginDir, String name) {
private boolean migrateJettyFile(File pluginDir, String name) {
File templateDir = new File(pluginDir, "templates");
File fileTmpl = new File(templateDir, name);
File outFile = new File(pluginDir, name);
@@ -271,7 +328,7 @@ public class ZzzOTController {
}
/** put the directory, base32, and base64 info in the help.html file and launch a browser window to display it */
private static void launchHelp(File pluginDir, Destination dest) {
private void launchHelp(File pluginDir, Destination dest) {
File fileTmpl = new File(pluginDir, "templates/help.html");
File outFile = new File(pluginDir, "eepsite/docroot/help.html");
String b32 = Base32.encode(dest.calculateHash().getData()) + ".b32.i2p";
@@ -298,4 +355,83 @@ public class ZzzOTController {
UrlLauncher.main(new String[] { "http://127.0.0.1:7662/help.html" } );
}
}
/////// ClientApp methods
/** @since 0.12.0 */
public synchronized void startup() {
if (_mgr != null) {
// this is really ugly, but thru 0.9.16,
// stopping a ClientApp plugin with $PLUGIN in the args fails,
// and it tries to start a second one instead.
// Find the first one and stop it.
ClientApp z = _mgr.getRegisteredApp(NAME);
if (z != null) {
if (VersionComparator.comp(CoreVersion.VERSION, "0.9.17") < 0) {
ZzzOTController ctrlr = (ZzzOTController) z;
_log.warn("Got start when another zzzot running, stopping him instead");
ctrlr.shutdown(null);
} else {
_log.error("ZzzOT already running");
}
changeState(START_FAILED);
return;
}
}
if (_state != STOPPED && _state != INITIALIZED && _state != START_FAILED) {
_log.error("Start while state = " + _state);
return;
}
changeState(STARTING);
try {
start(_args);
changeState(RUNNING);
if (_mgr != null)
_mgr.register(this);
} catch (Exception e) {
changeState(START_FAILED, "Start failed", e);
}
}
/** @since 0.12.0 */
public synchronized void shutdown(String[] args) {
if (_state == STOPPED)
return;
changeState(STOPPING);
if (_mgr != null)
_mgr.unregister(this);
stop();
changeState(STOPPED);
}
/** @since 0.12.0 */
public ClientAppState getState() {
return _state;
}
/** @since 0.12.0 */
public String getName() {
return NAME;
}
/** @since 0.12.0 */
public String getDisplayName() {
return NAME;
}
/////// end ClientApp methods
/** @since 0.12.0 */
private synchronized void changeState(ClientAppState state) {
_state = state;
if (_mgr != null)
_mgr.notify(this, state, null, null);
}
/** @since 0.12.0 */
private synchronized void changeState(ClientAppState state, String msg, Exception e) {
_state = state;
if (_mgr != null)
_mgr.notify(this, state, msg, e);
}
}

View File

@@ -27,7 +27,6 @@
*/
// would be nice to make these configurable
final int MAX_RESPONSES = 25;
final int INTERVAL = 27*60;
final boolean ALLOW_IP_MISMATCH = false;
final boolean ALLOW_COMPACT_RESPONSE = true;
@@ -73,6 +72,11 @@
msg = "no info hash";
}
if (info_hash.length() != 20 && !fail) {
fail = true;
msg = "bad info hash length " + info_hash.length();
}
if (ip == null && !fail) {
fail = true;
msg = "no ip (dest)";
@@ -83,11 +87,22 @@
msg = "no peer id";
}
if (peer_id.length() != 20 && !fail) {
fail = true;
msg = "bad peer id length " + peer_id.length();
}
Torrents torrents = ZzzOTController.getTorrents();
if (torrents == null && !fail) {
fail = true;
msg = "tracker is down";
}
InfoHash ih = null;
if (!fail) {
try {
ih = new InfoHash(info_hash);
} catch (Exception e) {
ih = torrents.createInfoHash(info_hash);
} catch (IllegalArgumentException e) {
fail = true;
msg = "bad infohash " + e;
}
@@ -111,8 +126,8 @@
PID pid = null;
if (!fail) {
try {
pid = new PID(peer_id);
} catch (Exception e) {
pid = torrents.createPID(peer_id);
} catch (IllegalArgumentException e) {
fail = true;
msg = "bad peer id " + e;
}
@@ -162,15 +177,14 @@
} catch (NumberFormatException nfe) {};
}
Torrents torrents = ZzzOTController.getTorrents();
Map<String, Object> m = new HashMap();
Map<String, Object> m = new HashMap(8);
if (fail) {
m.put("failure reason", msg);
} else if ("stopped".equals(event)) {
Peers peers = torrents.get(ih);
if (matchIP && peers != null)
peers.remove(pid);
m.put("interval", Integer.valueOf(INTERVAL));
m.put("interval", torrents.getInterval());
} else {
Peers peers = torrents.get(ih);
if (peers == null) {
@@ -195,7 +209,7 @@
if (matchIP)
p.setLeft(left);
m.put("interval", Integer.valueOf(INTERVAL));
m.put("interval", torrents.getInterval());
int size = peers.size();
int seeds = peers.countSeeds();
m.put("complete", Integer.valueOf(seeds));

View File

@@ -1,4 +1,4 @@
<%@page import="net.i2p.zzzot.ZzzOTController" %>
<%@page import="net.i2p.zzzot.ZzzOTController,net.i2p.zzzot.Torrents" %>
<html>
<head>
<title>ZzzOT</title>
@@ -6,9 +6,20 @@
<p>
zzzot
<p>
<%
Torrents torrents = ZzzOTController.getTorrents();
if (torrents != null) {
%>
<table cellspacing="8">
<tr><td>Torrents:<td align="right"><%=ZzzOTController.getTorrents().size()%>
<tr><td>Peers:<td align="right"><%=ZzzOTController.getTorrents().countPeers()%>
<tr><td>Torrents:<td align="right"><%=torrents.size()%>
<tr><td>Peers:<td align="right"><%=torrents.countPeers()%>
</table>
<%
} else {
%>
ZzzOT is not running
<%
}
%>
</body>
</html>

View File

@@ -27,6 +27,10 @@
*/
// so the chars will turn into bytes correctly
request.setCharacterEncoding("ISO-8859-1");
// above doesn't work for the query string
// https://wiki.eclipse.org/Jetty/Howto/International_Characters
// we could also do ((org.eclipse.jetty.server.Request) request).setQueryEncoding("ISO-8859-1")
request.setAttribute("org.eclipse.jetty.server.Request.queryEncoding", "ISO-8859-1");
java.io.OutputStream cout = response.getOutputStream();
response.setCharacterEncoding("ISO-8859-1");
response.setContentType("text/plain");
@@ -46,20 +50,24 @@
boolean all = info_hash == null;
Torrents torrents = ZzzOTController.getTorrents();
if (torrents == null && !fail) {
fail = true;
msg = "tracker is down";
}
InfoHash ih = null;
if ((!all) && !fail) {
try {
ih = new InfoHash(info_hash);
ih = torrents.createInfoHash(info_hash);
} catch (Exception e) {
fail = true;
msg = "bad infohash " + e;
}
}
Torrents torrents = ZzzOTController.getTorrents();
// build 3-level dictionary
Map<String, Object> m = new HashMap();
Map<String, Object> m = new HashMap(4);
if (fail) {
m.put("failure reason", msg);
} else {

View File

@@ -62,6 +62,10 @@
} else if (req.startsWith("locate dG9ycmVud")) { // locate b64(torrent)
// all the peers
Torrents torrents = ZzzOTController.getTorrents();
if (torrents == null) {
response.setStatus(503, "Down");
return;
}
for (InfoHash ihash : torrents.keySet()) {
Peers peers = torrents.get(ihash);
if (peers == null)