Advertisement
Guest User

Metrics.java

a guest
Apr 14th, 2014
42
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.74 KB | None | 0 0
  1. /*
  2. * Copyright 2011-2013 Tyler Blair. All rights reserved.
  3. *
  4. * Redistribution and use in source and binary forms, with or without modification, are
  5. * permitted provided that the following conditions are met:
  6. *
  7. * 1. Redistributions of source code must retain the above copyright notice, this list of
  8. * conditions and the following disclaimer.
  9. *
  10. * 2. Redistributions in binary form must reproduce the above copyright notice, this list
  11. * of conditions and the following disclaimer in the documentation and/or other materials
  12. * provided with the distribution.
  13. *
  14. * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
  15. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  16. * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
  17. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  18. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  19. * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  20. * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  21. * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  22. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  23. *
  24. * The views and conclusions contained in the software and documentation are those of the
  25. * authors and contributors and should not be interpreted as representing official policies,
  26. * either expressed or implied, of anybody else.
  27. */
  28. package com.github.Gamecube762.IsMinecraftDown;
  29.  
  30. import org.bukkit.Bukkit;
  31. import org.bukkit.configuration.InvalidConfigurationException;
  32. import org.bukkit.configuration.file.YamlConfiguration;
  33. import org.bukkit.plugin.Plugin;
  34. import org.bukkit.plugin.PluginDescriptionFile;
  35. import org.bukkit.scheduler.BukkitTask;
  36.  
  37. import java.io.*;
  38. import java.net.Proxy;
  39. import java.net.URL;
  40. import java.net.URLConnection;
  41. import java.net.URLEncoder;
  42. import java.util.*;
  43. import java.util.logging.Level;
  44. import java.util.zip.GZIPOutputStream;
  45.  
  46. public class Metrics {
  47.  
  48. /**
  49. * The current revision number
  50. */
  51. private final static int REVISION = 7;
  52.  
  53. /**
  54. * The base url of the metrics domain
  55. */
  56. private static final String BASE_URL = "http://report.com.github.Gamecube762.IsMinecraftDown.mcstats.org";
  57.  
  58. /**
  59. * The url used to report a server's status
  60. */
  61. private static final String REPORT_URL = "/plugin/%s";
  62.  
  63. /**
  64. * Interval of time to ping (in minutes)
  65. */
  66. private static final int PING_INTERVAL = 15;
  67.  
  68. /**
  69. * The plugin this metrics submits for
  70. */
  71. private final Plugin plugin;
  72.  
  73. /**
  74. * All of the custom graphs to submit to metrics
  75. */
  76. private final Set<Graph> graphs = Collections.synchronizedSet(new HashSet<Graph>());
  77.  
  78. /**
  79. * The plugin configuration file
  80. */
  81. private final YamlConfiguration configuration;
  82.  
  83. /**
  84. * The plugin configuration file
  85. */
  86. private final File configurationFile;
  87.  
  88. /**
  89. * Unique server id
  90. */
  91. private final String guid;
  92.  
  93. /**
  94. * Debug mode
  95. */
  96. private final boolean debug;
  97.  
  98. /**
  99. * Lock for synchronization
  100. */
  101. private final Object optOutLock = new Object();
  102.  
  103. /**
  104. * The scheduled task
  105. */
  106. private volatile BukkitTask task = null;
  107.  
  108. public Metrics(final Plugin plugin) throws IOException {
  109. if (plugin == null) {
  110. throw new IllegalArgumentException("Plugin cannot be null");
  111. }
  112.  
  113. this.plugin = plugin;
  114.  
  115. // load the config
  116. configurationFile = getConfigFile();
  117. configuration = YamlConfiguration.loadConfiguration(configurationFile);
  118.  
  119. // add some defaults
  120. configuration.addDefault("opt-out", false);
  121. configuration.addDefault("guid", UUID.randomUUID().toString());
  122. configuration.addDefault("debug", false);
  123.  
  124. // Do we need to create the file?
  125. if (configuration.get("guid", null) == null) {
  126. configuration.options().header("http://com.github.Gamecube762.IsMinecraftDown.mcstats.org").copyDefaults(true);
  127. configuration.save(configurationFile);
  128. }
  129.  
  130. // Load the guid then
  131. guid = configuration.getString("guid");
  132. debug = configuration.getBoolean("debug", false);
  133. }
  134.  
  135. /**
  136. * Construct and create a Graph that can be used to separate specific plotters to their own graphs on the metrics
  137. * website. Plotters can be added to the graph object returned.
  138. *
  139. * @param name The name of the graph
  140. * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given
  141. */
  142. public Graph createGraph(final String name) {
  143. if (name == null) {
  144. throw new IllegalArgumentException("Graph name cannot be null");
  145. }
  146.  
  147. // Construct the graph object
  148. final Graph graph = new Graph(name);
  149.  
  150. // Now we can add our graph
  151. graphs.add(graph);
  152.  
  153. // and return back
  154. return graph;
  155. }
  156.  
  157. /**
  158. * Add a Graph object to BukkitMetrics that represents data for the plugin that should be sent to the backend
  159. *
  160. * @param graph The name of the graph
  161. */
  162. public void addGraph(final Graph graph) {
  163. if (graph == null) {
  164. throw new IllegalArgumentException("Graph cannot be null");
  165. }
  166.  
  167. graphs.add(graph);
  168. }
  169.  
  170. /**
  171. * Start measuring statistics. This will immediately create an async repeating task as the plugin and send the
  172. * initial data to the metrics backend, and then after that it will post in increments of PING_INTERVAL * 1200
  173. * ticks.
  174. *
  175. * @return True if statistics measuring is running, otherwise false.
  176. */
  177. public boolean start() {
  178. synchronized (optOutLock) {
  179. // Did we opt out?
  180. if (isOptOut()) {
  181. return false;
  182. }
  183.  
  184. // Is metrics already running?
  185. if (task != null) {
  186. return true;
  187. }
  188.  
  189. // Begin hitting the server with glorious data
  190. task = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() {
  191.  
  192. private boolean firstPost = true;
  193.  
  194. public void run() {
  195. try {
  196. // This has to be synchronized or it can collide with the disable method.
  197. synchronized (optOutLock) {
  198. // Disable Task, if it is running and the server owner decided to opt-out
  199. if (isOptOut() && task != null) {
  200. task.cancel();
  201. task = null;
  202. // Tell all plotters to stop gathering information.
  203. for (Graph graph : graphs) {
  204. graph.onOptOut();
  205. }
  206. }
  207. }
  208.  
  209. // We use the inverse of firstPost because if it is the first time we are posting,
  210. // it is not a interval ping, so it evaluates to FALSE
  211. // Each time thereafter it will evaluate to TRUE, i.e PING!
  212. postPlugin(!firstPost);
  213.  
  214. // After the first post we set firstPost to false
  215. // Each post thereafter will be a ping
  216. firstPost = false;
  217. } catch (IOException e) {
  218. if (debug) {
  219. Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage());
  220. }
  221. }
  222. }
  223. }, 0, PING_INTERVAL * 1200);
  224.  
  225. return true;
  226. }
  227. }
  228.  
  229. /**
  230. * Has the server owner denied plugin metrics?
  231. *
  232. * @return true if metrics should be opted out of it
  233. */
  234. public boolean isOptOut() {
  235. synchronized (optOutLock) {
  236. try {
  237. // Reload the metrics file
  238. configuration.load(getConfigFile());
  239. } catch (IOException ex) {
  240. if (debug) {
  241. Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
  242. }
  243. return true;
  244. } catch (InvalidConfigurationException ex) {
  245. if (debug) {
  246. Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
  247. }
  248. return true;
  249. }
  250. return configuration.getBoolean("opt-out", false);
  251. }
  252. }
  253.  
  254. /**
  255. * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task.
  256. *
  257. * @throws java.io.IOException
  258. */
  259. public void enable() throws IOException {
  260. // This has to be synchronized or it can collide with the check in the task.
  261. synchronized (optOutLock) {
  262. // Check if the server owner has already set opt-out, if not, set it.
  263. if (isOptOut()) {
  264. configuration.set("opt-out", false);
  265. configuration.save(configurationFile);
  266. }
  267.  
  268. // Enable Task, if it is not running
  269. if (task == null) {
  270. start();
  271. }
  272. }
  273. }
  274.  
  275. /**
  276. * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task.
  277. *
  278. * @throws java.io.IOException
  279. */
  280. public void disable() throws IOException {
  281. // This has to be synchronized or it can collide with the check in the task.
  282. synchronized (optOutLock) {
  283. // Check if the server owner has already set opt-out, if not, set it.
  284. if (!isOptOut()) {
  285. configuration.set("opt-out", true);
  286. configuration.save(configurationFile);
  287. }
  288.  
  289. // Disable Task, if it is running
  290. if (task != null) {
  291. task.cancel();
  292. task = null;
  293. }
  294. }
  295. }
  296.  
  297. /**
  298. * Gets the File object of the config file that should be used to store data such as the GUID and opt-out status
  299. *
  300. * @return the File object for the config file
  301. */
  302. public File getConfigFile() {
  303. // I believe the easiest way to get the base folder (e.g craftbukkit set via -P) for plugins to use
  304. // is to abuse the plugin object we already have
  305. // plugin.getDataFolder() => base/plugins/PluginA/
  306. // pluginsFolder => base/plugins/
  307. // The base is not necessarily relative to the startup directory.
  308. File pluginsFolder = plugin.getDataFolder().getParentFile();
  309.  
  310. // return => base/plugins/PluginMetrics/config.yml
  311. return new File(new File(pluginsFolder, "PluginMetrics"), "config.yml");
  312. }
  313.  
  314. /**
  315. * Generic method that posts a plugin to the metrics website
  316. */
  317. private void postPlugin(final boolean isPing) throws IOException {
  318. // Server software specific section
  319. PluginDescriptionFile description = plugin.getDescription();
  320. String pluginName = description.getName();
  321. boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE if online mode is enabled
  322. String pluginVersion = description.getVersion();
  323. String serverVersion = Bukkit.getVersion();
  324. int playersOnline = Bukkit.getServer().getOnlinePlayers().length;
  325.  
  326. // END server software specific section -- all code below does not use any code outside of this class / Java
  327.  
  328. // Construct the post data
  329. StringBuilder json = new StringBuilder(1024);
  330. json.append('{');
  331.  
  332. // The plugin's description file containg all of the plugin data such as name, version, author, etc
  333. appendJSONPair(json, "guid", guid);
  334. appendJSONPair(json, "plugin_version", pluginVersion);
  335. appendJSONPair(json, "server_version", serverVersion);
  336. appendJSONPair(json, "players_online", Integer.toString(playersOnline));
  337.  
  338. // New data as of R6
  339. String osname = System.getProperty("os.name");
  340. String osarch = System.getProperty("os.arch");
  341. String osversion = System.getProperty("os.version");
  342. String java_version = System.getProperty("java.version");
  343. int coreCount = Runtime.getRuntime().availableProcessors();
  344.  
  345. // normalize os arch .. amd64 -> x86_64
  346. if (osarch.equals("amd64")) {
  347. osarch = "x86_64";
  348. }
  349.  
  350. appendJSONPair(json, "osname", osname);
  351. appendJSONPair(json, "osarch", osarch);
  352. appendJSONPair(json, "osversion", osversion);
  353. appendJSONPair(json, "cores", Integer.toString(coreCount));
  354. appendJSONPair(json, "auth_mode", onlineMode ? "1" : "0");
  355. appendJSONPair(json, "java_version", java_version);
  356.  
  357. // If we're pinging, append it
  358. if (isPing) {
  359. appendJSONPair(json, "ping", "1");
  360. }
  361.  
  362. if (graphs.size() > 0) {
  363. synchronized (graphs) {
  364. json.append(',');
  365. json.append('"');
  366. json.append("graphs");
  367. json.append('"');
  368. json.append(':');
  369. json.append('{');
  370.  
  371. boolean firstGraph = true;
  372.  
  373. final Iterator<Graph> iter = graphs.iterator();
  374.  
  375. while (iter.hasNext()) {
  376. Graph graph = iter.next();
  377.  
  378. StringBuilder graphJson = new StringBuilder();
  379. graphJson.append('{');
  380.  
  381. for (Plotter plotter : graph.getPlotters()) {
  382. appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue()));
  383. }
  384.  
  385. graphJson.append('}');
  386.  
  387. if (!firstGraph) {
  388. json.append(',');
  389. }
  390.  
  391. json.append(escapeJSON(graph.getName()));
  392. json.append(':');
  393. json.append(graphJson);
  394.  
  395. firstGraph = false;
  396. }
  397.  
  398. json.append('}');
  399. }
  400. }
  401.  
  402. // close json
  403. json.append('}');
  404.  
  405. // Create the url
  406. URL url = new URL(BASE_URL + String.format(REPORT_URL, urlEncode(pluginName)));
  407.  
  408. // Connect to the website
  409. URLConnection connection;
  410.  
  411. // Mineshafter creates a socks proxy, so we can safely bypass it
  412. // It does not reroute POST requests so we need to go around it
  413. if (isMineshafterPresent()) {
  414. connection = url.openConnection(Proxy.NO_PROXY);
  415. } else {
  416. connection = url.openConnection();
  417. }
  418.  
  419.  
  420. byte[] uncompressed = json.toString().getBytes();
  421. byte[] compressed = gzip(json.toString());
  422.  
  423. // Headers
  424. connection.addRequestProperty("User-Agent", "MCStats/" + REVISION);
  425. connection.addRequestProperty("Content-Type", "application/json");
  426. connection.addRequestProperty("Content-Encoding", "gzip");
  427. connection.addRequestProperty("Content-Length", Integer.toString(compressed.length));
  428. connection.addRequestProperty("Accept", "application/json");
  429. connection.addRequestProperty("Connection", "close");
  430.  
  431. connection.setDoOutput(true);
  432.  
  433. if (debug) {
  434. System.out.println("[Metrics] Prepared request for " + pluginName + " uncompressed=" + uncompressed.length + " compressed=" + compressed.length);
  435. }
  436.  
  437. // Write the data
  438. OutputStream os = connection.getOutputStream();
  439. os.write(compressed);
  440. os.flush();
  441.  
  442. // Now read the response
  443. final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
  444. String response = reader.readLine();
  445.  
  446. // close resources
  447. os.close();
  448. reader.close();
  449.  
  450. if (response == null || response.startsWith("ERR") || response.startsWith("7")) {
  451. if (response == null) {
  452. response = "null";
  453. } else if (response.startsWith("7")) {
  454. response = response.substring(response.startsWith("7,") ? 2 : 1);
  455. }
  456.  
  457. throw new IOException(response);
  458. } else {
  459. // Is this the first update this hour?
  460. if (response.equals("1") || response.contains("This is your first update this hour")) {
  461. synchronized (graphs) {
  462. final Iterator<Graph> iter = graphs.iterator();
  463.  
  464. while (iter.hasNext()) {
  465. final Graph graph = iter.next();
  466.  
  467. for (Plotter plotter : graph.getPlotters()) {
  468. plotter.reset();
  469. }
  470. }
  471. }
  472. }
  473. }
  474. }
  475.  
  476. /**
  477. * GZip compress a string of bytes
  478. *
  479. * @param input
  480. * @return
  481. */
  482. public static byte[] gzip(String input) {
  483. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  484. GZIPOutputStream gzos = null;
  485.  
  486. try {
  487. gzos = new GZIPOutputStream(baos);
  488. gzos.write(input.getBytes("UTF-8"));
  489. } catch (IOException e) {
  490. e.printStackTrace();
  491. } finally {
  492. if (gzos != null) try {
  493. gzos.close();
  494. } catch (IOException ignore) {
  495. }
  496. }
  497.  
  498. return baos.toByteArray();
  499. }
  500.  
  501. /**
  502. * Check if mineshafter is present. If it is, we need to bypass it to send POST requests
  503. *
  504. * @return true if mineshafter is installed on the server
  505. */
  506. private boolean isMineshafterPresent() {
  507. try {
  508. Class.forName("mineshafter.MineServer");
  509. return true;
  510. } catch (Exception e) {
  511. return false;
  512. }
  513. }
  514.  
  515. /**
  516. * Appends a json encoded key/value pair to the given string builder.
  517. *
  518. * @param json
  519. * @param key
  520. * @param value
  521. * @throws java.io.UnsupportedEncodingException
  522. */
  523. private static void appendJSONPair(StringBuilder json, String key, String value) throws UnsupportedEncodingException {
  524. boolean isValueNumeric = false;
  525.  
  526. try {
  527. if (value.equals("0") || !value.endsWith("0")) {
  528. Double.parseDouble(value);
  529. isValueNumeric = true;
  530. }
  531. } catch (NumberFormatException e) {
  532. isValueNumeric = false;
  533. }
  534.  
  535. if (json.charAt(json.length() - 1) != '{') {
  536. json.append(',');
  537. }
  538.  
  539. json.append(escapeJSON(key));
  540. json.append(':');
  541.  
  542. if (isValueNumeric) {
  543. json.append(value);
  544. } else {
  545. json.append(escapeJSON(value));
  546. }
  547. }
  548.  
  549. /**
  550. * Escape a string to create a valid JSON string
  551. *
  552. * @param text
  553. * @return
  554. */
  555. private static String escapeJSON(String text) {
  556. StringBuilder builder = new StringBuilder();
  557.  
  558. builder.append('"');
  559. for (int index = 0; index < text.length(); index++) {
  560. char chr = text.charAt(index);
  561.  
  562. switch (chr) {
  563. case '"':
  564. case '\\':
  565. builder.append('\\');
  566. builder.append(chr);
  567. break;
  568. case '\b':
  569. builder.append("\\b");
  570. break;
  571. case '\t':
  572. builder.append("\\t");
  573. break;
  574. case '\n':
  575. builder.append("\\n");
  576. break;
  577. case '\r':
  578. builder.append("\\r");
  579. break;
  580. default:
  581. if (chr < ' ') {
  582. String t = "000" + Integer.toHexString(chr);
  583. builder.append("\\u" + t.substring(t.length() - 4));
  584. } else {
  585. builder.append(chr);
  586. }
  587. break;
  588. }
  589. }
  590. builder.append('"');
  591.  
  592. return builder.toString();
  593. }
  594.  
  595. /**
  596. * Encode text as UTF-8
  597. *
  598. * @param text the text to encode
  599. * @return the encoded text, as UTF-8
  600. */
  601. private static String urlEncode(final String text) throws UnsupportedEncodingException {
  602. return URLEncoder.encode(text, "UTF-8");
  603. }
  604.  
  605. /**
  606. * Represents a custom graph on the website
  607. */
  608. public static class Graph {
  609.  
  610. /**
  611. * The graph's name, alphanumeric and spaces only :) If it does not comply to the above when submitted, it is
  612. * rejected
  613. */
  614. private final String name;
  615.  
  616. /**
  617. * The set of plotters that are contained within this graph
  618. */
  619. private final Set<Plotter> plotters = new LinkedHashSet<Plotter>();
  620.  
  621. private Graph(final String name) {
  622. this.name = name;
  623. }
  624.  
  625. /**
  626. * Gets the graph's name
  627. *
  628. * @return the Graph's name
  629. */
  630. public String getName() {
  631. return name;
  632. }
  633.  
  634. /**
  635. * Add a plotter to the graph, which will be used to plot entries
  636. *
  637. * @param plotter the plotter to add to the graph
  638. */
  639. public void addPlotter(final Plotter plotter) {
  640. plotters.add(plotter);
  641. }
  642.  
  643. /**
  644. * Remove a plotter from the graph
  645. *
  646. * @param plotter the plotter to remove from the graph
  647. */
  648. public void removePlotter(final Plotter plotter) {
  649. plotters.remove(plotter);
  650. }
  651.  
  652. /**
  653. * Gets an <b>unmodifiable</b> set of the plotter objects in the graph
  654. *
  655. * @return an unmodifiable {@link java.util.Set} of the plotter objects
  656. */
  657. public Set<Plotter> getPlotters() {
  658. return Collections.unmodifiableSet(plotters);
  659. }
  660.  
  661. @Override
  662. public int hashCode() {
  663. return name.hashCode();
  664. }
  665.  
  666. @Override
  667. public boolean equals(final Object object) {
  668. if (!(object instanceof Graph)) {
  669. return false;
  670. }
  671.  
  672. final Graph graph = (Graph) object;
  673. return graph.name.equals(name);
  674. }
  675.  
  676. /**
  677. * Called when the server owner decides to opt-out of BukkitMetrics while the server is running.
  678. */
  679. protected void onOptOut() {
  680. }
  681. }
  682.  
  683. /**
  684. * Interface used to collect custom data for a plugin
  685. */
  686. public static abstract class Plotter {
  687.  
  688. /**
  689. * The plot's name
  690. */
  691. private final String name;
  692.  
  693. /**
  694. * Construct a plotter with the default plot name
  695. */
  696. public Plotter() {
  697. this("Default");
  698. }
  699.  
  700. /**
  701. * Construct a plotter with a specific plot name
  702. *
  703. * @param name the name of the plotter to use, which will show up on the website
  704. */
  705. public Plotter(final String name) {
  706. this.name = name;
  707. }
  708.  
  709. /**
  710. * Get the current value for the plotted point. Since this function defers to an external function it may or may
  711. * not return immediately thus cannot be guaranteed to be thread friendly or safe. This function can be called
  712. * from any thread so care should be taken when accessing resources that need to be synchronized.
  713. *
  714. * @return the current value for the point to be plotted.
  715. */
  716. public abstract int getValue();
  717.  
  718. /**
  719. * Get the column name for the plotted point
  720. *
  721. * @return the plotted point's column name
  722. */
  723. public String getColumnName() {
  724. return name;
  725. }
  726.  
  727. /**
  728. * Called after the website graphs have been updated
  729. */
  730. public void reset() {
  731. }
  732.  
  733. @Override
  734. public int hashCode() {
  735. return getColumnName().hashCode();
  736. }
  737.  
  738. @Override
  739. public boolean equals(final Object object) {
  740. if (!(object instanceof Plotter)) {
  741. return false;
  742. }
  743.  
  744. final Plotter plotter = (Plotter) object;
  745. return plotter.name.equals(name) && plotter.getValue() == getValue();
  746. }
  747. }
  748. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement