package openeye.logic; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.lang.Thread.UncaughtExceptionHandler; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import openeye.Log; import openeye.net.ReportSender; import openeye.notes.NoteCollector; import openeye.protocol.FileSignature; import openeye.protocol.ITypedStruct; import openeye.protocol.reports.ReportCrash; import openeye.protocol.reports.ReportPing; import openeye.responses.IExecutableResponse; import openeye.storage.IDataSource; import openeye.storage.IQueryableStorage; import openeye.struct.TypedCollections.ReportsList; import openeye.struct.TypedCollections.ResponseList; public class SenderWorker implements Runnable { private static final String API_HOST = "openeye.openmods.info"; private static final String API_PATH = "/api/v1/data"; // private static final String API_PATH = "/dummy"; private final Future<ModMetaCollector> collector; private final ModState state; private final CountDownLatch firstMessageReceived = new CountDownLatch(1); private final Set<String> dangerousSignatures = Sets.newHashSet(); public SenderWorker(Future<ModMetaCollector> collector, ModState state) { this.collector = collector; this.state = state; } private static void logException(Throwable throwable, String msg, Object... args) { ThrowableLogger.processThrowable(throwable, "openeye"); Log.warn(throwable, msg, args); } private static void store(Object report, String name) { try { IDataSource<Object> list = Storages.instance().sessionArchive.createNew(name); list.store(report); } catch (Exception e) { Log.warn(e, "Failed to store " + name); } } private static void filterStructs(Collection<? extends ITypedStruct> structs, Set<String> blacklist) { Iterator<? extends ITypedStruct> it = structs.iterator(); while (it.hasNext()) { final ITypedStruct struct = it.next(); final String type = struct.getType(); if (blacklist.contains(type)) { Log.debug("Filtered type %s(%s) from list, since it's on blacklist", type, struct); it.remove(); } } } private ReportsList executeResponses(ModMetaCollector collector, ResponseList requests) { Preconditions.checkState(!requests.isEmpty()); final ReportContext context = new ReportContext(collector); for (IExecutableResponse request : requests) request.execute(context); dangerousSignatures.addAll(context.dangerousSignatures()); return context.reports(); } private static <T> SortedMap<String, T> retrieveAllSources(IQueryableStorage<T> storage) { final ImmutableSortedMap.Builder<String, T> result = ImmutableSortedMap.naturalOrder(); for (IDataSource<T> source : storage.listAll()) { try { result.put(source.getId(), source.retrieve()); } catch (Throwable t) { Log.warn(t, "Failed to read entry %s, removing", source.getId()); source.delete(); } } return result.build(); } private static <T> void removeSources(IQueryableStorage<T> storage, Set<String> ids) { for (String id : ids) { final IDataSource<T> entry = storage.getById(id); if (entry != null) entry.delete(); } } private static Collection<ReportCrash> removePendingCrashDuplicates(Map<String, ReportCrash> crashes) { final Map<CrashId, ReportCrash> result = Maps.newHashMap(); for (Map.Entry<String, ReportCrash> e : crashes.entrySet()) { ReportCrash crash = e.getValue(); if (crash != null) { ReportCrash prev = result.put(new CrashId(crash.timestamp, crash.random), crash); if (prev != null) Log.warn("Found duplicated crash report %s", e.getKey()); } } return ImmutableList.copyOf(crashes.values()); } private static SortedMap<String, ReportCrash> selectCrashes(Map<String, ReportCrash> pendingCrashes) { if (!Config.sendCrashes) return ImmutableSortedMap.of(); if (Config.sentCrashReportsLimitTotal >= 0 && Config.sentCrashReportsLimitTotal < pendingCrashes.size()) { final ImmutableSortedMap.Builder<String, ReportCrash> result = ImmutableSortedMap.naturalOrder(); int count = Config.sentCrashReportsLimitTotal; for (Map.Entry<String, ReportCrash> e : pendingCrashes.entrySet()) { if (--count < 0) break; result.put(e); } return result.build(); } return ImmutableSortedMap.copyOf(pendingCrashes); } protected ReportsList createInitialReport(ModMetaCollector collector, Collection<ReportCrash> crashes) { final ReportsList result = new ReportsList(); try { if (Config.sendModList) createAnalyticsReport(collector, result); if (Config.pingOnInitialReport) result.add(new ReportPing()); result.addAll(crashes); } catch (Exception e) { logException(e, "Failed to create initial report"); } return result; } protected void createAnalyticsReport(ModMetaCollector collector, final ReportsList result) { try { if (Config.scanOnly) { result.add(ReportBuilders.buildKnownFilesReport(collector)); } else { result.add(ReportBuilders.buildAnalyticsReport(collector, state.installedMods)); } } catch (Exception e) { logException(e, "Failed to create analytics report"); } } private void sendReports(ModMetaCollector collector) { final SortedMap<String, ReportCrash> pendingCrashes = retrieveAllSources(Storages.instance().pendingCrashes); final SortedMap<String, ReportCrash> selectedPendingCrashes = selectCrashes(pendingCrashes); final Collection<ReportCrash> pendingUniqueCrashes = removePendingCrashDuplicates(selectedPendingCrashes); ReportsList currentReports = createInitialReport(collector, pendingUniqueCrashes); try { ReportSender sender = new ReportSender(API_HOST, API_PATH); while (!currentReports.isEmpty()) { filterStructs(currentReports, Config.reportsBlacklist); store(currentReports, "request"); ResponseList response = Config.dontSend? null : sender.sendAndReceive(currentReports); if (response == null || response.isEmpty()) break; filterStructs(response, Config.responseBlacklist); store(response, "response"); currentReports.clear(); try { currentReports = executeResponses(collector, response); } catch (Exception e) { logException(e, "Failed to execute responses"); break; } firstMessageReceived.countDown(); // early release - notes send in next packets are ignored } removeSources(Storages.instance().pendingCrashes, selectedPendingCrashes.keySet()); NoteCollector.INSTANCE.addNote(sender.getEncryptionState()); } catch (Exception e) { Log.warn(e, "Failed to send report to " + API_HOST + API_PATH); } } @Override public void run() { try { final ModMetaCollector collector = this.collector.get(); sendReports(collector); // only update state after mods were successfully sent StateHolder.state().installedMods = collector.getAllSignatures(); StateHolder.save(); } catch (Throwable t) { logException(t, "Failed to send data to server OpenEye"); } finally { firstMessageReceived.countDown(); // can't do much more, releasing lock } } public void start() { Thread senderThread = new Thread(this); senderThread.setName("OpenEye sender thread"); senderThread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { logException(e, "Uncaught exception in data collector thread, report will not be send"); firstMessageReceived.countDown(); // oh well, better luck next time } }); senderThread.start(); } public void waitForFirstMsg() { try { if (!firstMessageReceived.await(30, TimeUnit.SECONDS)) Log.warn("OpenEye timeouted while waiting for worker thread, data will be incomplete"); } catch (InterruptedException e) { Log.warn("Thread interrupted while waiting for msg"); } } public Collection<FileSignature> listDangerousFiles() { List<FileSignature> result = Lists.newArrayList(); try { ModMetaCollector collector = this.collector.get(); for (String signature : dangerousSignatures) { FileSignature file = collector.getFileForSignature(signature); if (signature != null) result.add(file); } } catch (Throwable t) { Log.warn(t, "Failed to list dangerous files"); } return result; } }