From db543c290174caaeb3d0b740066f68d7b76e8090 Mon Sep 17 00:00:00 2001
From: Jonathan Harris <jonathan@marginal.org.uk>
Date: Tue, 6 Sep 2016 01:54:13 +0100
Subject: [PATCH] Option to delay sending body data until docked

---
 EDMarketConnector.py |  7 ++++++
 README.md            |  1 +
 config.py            |  1 +
 eddn.py              | 57 ++++++++++++++++++++++++++++++++++++++++----
 prefs.py             |  6 +++++
 5 files changed, 68 insertions(+), 4 deletions(-)

diff --git a/EDMarketConnector.py b/EDMarketConnector.py
index 9e3c7a43..b647addf 100755
--- a/EDMarketConnector.py
+++ b/EDMarketConnector.py
@@ -309,6 +309,12 @@ class AppWindow:
             if __debug__: print_exc()
             self.status['text'] = unicode(e)
 
+        # Try to obtain exclusive lock on journal cache, even if we don't need it yet
+        try:
+            eddn.load()
+        except Exception as e:
+            self.status['text'] = unicode(e)
+
         if not getattr(sys, 'frozen', False):
             self.updater.checkForUpdates()	# Sparkle / WinSparkle does this automatically for packaged apps
 
@@ -620,6 +626,7 @@ class AppWindow:
         if platform!='darwin' or self.w.winfo_rooty()>0:	# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
             config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+')))
         config.close()
+        eddn.close()
         self.updater.close()
         self.session.close()
         self.w.destroy()
diff --git a/README.md b/README.md
index 2da16df2..ca32ec90 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,7 @@ Some options work by reading the Elite: Dangerous game's “journal” files. If
   * Sends station commodity market, outfitting and shipyard data to “[EDDN](http://eddn-gateway.elite-markets.net/)” from where you and others can use it via online trading tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc.
 * System and scan data
   * Sends general system information and the results of your detailed planet scans to “[EDDN](http://eddn-gateway.elite-markets.net/)” from where you and others can use it via online prospecting tools such as [eddb](http://eddb.io/), [Inara](http://inara.cz), etc.
+  * You can choose to delay sending this information to EDDN until you're next safely docked at a station. Otherwise the information is sent as soon as you enter a system or perform a scan.
 
 ### EDSM
 
diff --git a/config.py b/config.py
index b51ca4f6..b5d60652 100644
--- a/config.py
+++ b/config.py
@@ -95,6 +95,7 @@ class Config:
     # OUT_SYS_AUTO    = 512	# Now always automatic
     OUT_MKT_MANUAL    = 1024
     OUT_SYS_EDDN      = 2048
+    OUT_SYS_DELAY     = 4096
 
     if platform=='darwin':
 
diff --git a/eddn.py b/eddn.py
index a986d1d4..21d29caa 100644
--- a/eddn.py
+++ b/eddn.py
@@ -4,12 +4,20 @@ from collections import OrderedDict
 import hashlib
 import json
 import numbers
+from os import SEEK_SET, SEEK_CUR, SEEK_END
+from os.path import exists, join
 from platform import system
 import re
 import requests
 from sys import platform
 import time
 
+if platform != 'win32':
+    from fcntl import lockf, LOCK_EX, LOCK_NB
+
+if __debug__:
+    from traceback import print_exc
+
 from config import applongname, appversion, config
 from companion import category_map
 
@@ -25,6 +33,38 @@ class _EDDN:
 
     def __init__(self):
         self.session = requests.Session()
+        self.replayfile = None	# For delayed messages
+
+    def load(self):
+        # Try to obtain exclusive access to the journal cache
+        filename = join(config.app_dir, 'replay.jsonl')
+        try:
+            try:
+                # Try to open existing file
+                self.replayfile = open(filename, 'r+')
+            except:
+                if exists(filename):
+                    raise	# Couldn't open existing file
+                else:
+                    self.replayfile = open(filename, 'w+')	# Create file
+            if platform != 'win32':	# open for writing is automatically exclusive on Windows
+                lockf(self.replayfile, LOCK_EX|LOCK_NB)
+        except:
+            if __debug__: print_exc()
+            if self.replayfile:
+                self.replayfile.close()
+            self.replayfile = None
+            raise Exception("Error: Is another copy of this app already running?")	# Shouldn't happen - don't bother localizing
+
+    def flush(self):
+        self.replayfile.seek(0, SEEK_SET)
+        for line in self.replayfile:
+            self.send(*json.loads(line, object_pairs_hook=OrderedDict))
+        self.replayfile.truncate(0)
+
+    def close(self):
+        if self.replayfile:
+            self.replayfile.close()
 
     def send(self, cmdr, msg):
         msg['header'] = {
@@ -96,10 +136,19 @@ class _EDDN:
             })
 
     def export_journal_entry(self, cmdr, is_beta, entry):
-        self.send(cmdr, {
-            '$schemaRef' : 'http://schemas.elite-markets.net/eddn/journal/1' + (is_beta and '/test' or ''),
-            'message'    : entry
-        })
+        if config.getint('output') & config.OUT_SYS_DELAY and self.replayfile and entry['event'] != 'Docked':
+            self.replayfile.seek(0, SEEK_END)
+            self.replayfile.write('%s\n' % json.dumps([cmdr.encode('utf-8'), {
+                '$schemaRef' : 'http://schemas.elite-markets.net/eddn/journal/1' + (is_beta and '/test' or ''),
+                'message'    : entry
+            }]))
+            self.replayfile.flush()
+        else:
+            self.flush()
+            self.send(cmdr, {
+                '$schemaRef' : 'http://schemas.elite-markets.net/eddn/journal/1' + (is_beta and '/test' or ''),
+                'message'    : entry
+            })
 
 # singleton
 eddn = _EDDN()
diff --git a/prefs.py b/prefs.py
index 5606e610..6beef4f8 100644
--- a/prefs.py
+++ b/prefs.py
@@ -11,6 +11,7 @@ from ttkHyperlinkLabel import HyperlinkLabel
 import myNotebook as nb
 
 from config import applongname, config
+from eddn import eddn
 from hotkey import hotkeymgr
 from l10n import Translations
 from monitor import monitor
@@ -157,6 +158,9 @@ class PreferencesDialog(tk.Toplevel):
         self.eddn_system = tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1)
         self.eddn_system_button = nb.Checkbutton(eddnframe, text=_('Send system and scan data to the Elite Dangerous Data Network'), variable=self.eddn_system, command=self.outvarchanged)	# Output setting new in E:D 2.2
         self.eddn_system_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W)
+        self.eddn_delay= tk.IntVar(value = (output & config.OUT_SYS_DELAY) and 1)
+        self.eddn_delay_button = nb.Checkbutton(eddnframe, text=_('Delay sending until docked'), variable=self.eddn_delay, command=self.outvarchanged)	# Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2
+        self.eddn_delay_button.grid(padx=BUTTONX, sticky=tk.W)
 
         notebook.add(eddnframe, text='EDDN')		# Not translated
 
@@ -318,6 +322,7 @@ class PreferencesDialog(tk.Toplevel):
 
         self.eddn_auto_button['state']  = self.eddn_station.get() and logvalid and tk.NORMAL or tk.DISABLED
         self.eddn_system_button['state']= logvalid and tk.NORMAL or tk.DISABLED
+        self.eddn_delay_button['state'] = logvalid and eddn.replayfile and self.eddn_system.get() and tk.NORMAL or tk.DISABLED
 
         self.edsm_log_button['state']   = logvalid and tk.NORMAL or tk.DISABLED
         edsm_state = logvalid and self.edsm_log.get() and tk.NORMAL or tk.DISABLED
@@ -449,6 +454,7 @@ class PreferencesDialog(tk.Toplevel):
                    (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) +
                    (self.eddn_station.get()      and config.OUT_MKT_EDDN) +
                    (self.eddn_system.get()       and config.OUT_SYS_EDDN) +
+                   (self.eddn_delay.get()        and config.OUT_SYS_DELAY) +
                    (self.edsm_log.get()          and config.OUT_SYS_EDSM))
         config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get())