Skip to content

Commit

Permalink
Merge branch 'master' into 0.11
Browse files Browse the repository at this point in the history
  • Loading branch information
kozec committed Apr 14, 2015
2 parents 38e3eeb + c6fd5d2 commit 89b2b94
Show file tree
Hide file tree
Showing 20 changed files with 363 additions and 89 deletions.
4 changes: 2 additions & 2 deletions app.glade
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@
<object class="GtkImageMenuItem" id="menu-daemon-output">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Display _Daemon Ouput</property>
<property name="label" translatable="yes">Display _Daemon Output</property>
<property name="use_underline">True</property>
<property name="always_show_image">True</property>
<property name="image">menu-daemon-output-image</property>
Expand Down Expand Up @@ -383,7 +383,7 @@
<attribute name="action">app.webui</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Display _Daemon Ouput</attribute>
<attribute name="label" translatable="yes">Display _Daemon Output</attribute>
<attribute name="action">app.daemon_output</attribute>
</item>
</section>
Expand Down
1 change: 1 addition & 0 deletions icons/syncthing-gtk.png
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def get_version():
('share/syncthing-gtk', glob.glob("scripts/syncthing-plugin-*.py") ),
('share/syncthing-gtk/icons', glob.glob("icons/*") ),
('share/pixmaps', glob.glob("icons/emblem-*.png") ),
('share/pixmaps', ["icons/syncthing-gtk.png"]),
('share/applications', ['syncthing-gtk.desktop'] ),
],
scripts = [ "scripts/syncthing-gtk" ],
Expand Down
2 changes: 0 additions & 2 deletions syncthing-gtk-full.nsis
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
## Initial stuff
!include MUI2.nsh
!define APP_NAME SyncthingGTK
!define LIBRARIES_FILE "syncthing-gtk-windows-libraries-0.5.7.zip"
!define LIBRARIES_URL "http://kozec.com/${LIBRARIES_FILE}"
!define MUI_FINISHPAGE_RUN "$INSTDIR\syncthing-gtk.exe"
!include "build\version.nsh"

Expand Down
2 changes: 1 addition & 1 deletion syncthing-gtk.desktop
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ GenericName=Syncthing GTK
Comment=GUI for Syncthing
Exec=/usr/bin/syncthing-gtk
Type=Application
Icon=/usr/share/syncthing-gtk/icons/st-logo-128.png
Icon=syncthing-gtk
Categories=Network
4 changes: 2 additions & 2 deletions syncthing-gtk.nsis
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Initial stuff
!include MUI2.nsh
!define APP_NAME SyncthingGTK
!define LIBRARIES_FILE "syncthing-gtk-windows-libraries-0.5.7.zip"
!define LIBRARIES_FILE "syncthing-gtk-windows-libraries-0.6.4.zip"
!define LIBRARIES_URL "http://kozec.com/${LIBRARIES_FILE}"
!define MUI_FINISHPAGE_RUN "$INSTDIR\syncthing-gtk.exe"
!include "build\version.nsh"
Expand Down Expand Up @@ -49,7 +49,7 @@ NotRunning:
File /r build\exe.win32-2.7\icons
# Check if random file that should be part of libraries zip exists
# and download&extract zip if not
IfFileExists $INSTDIR\libwebp-5.dll SkipDownload DoDownload
IfFileExists $INSTDIR\win32process.pyd SkipDownload DoDownload
DoDownload:
NSISdl::download ${LIBRARIES_URL} $TEMP\${LIBRARIES_FILE}
ZipDLL::extractall $TEMP\${LIBRARIES_FILE} $INSTDIR
Expand Down
3 changes: 2 additions & 1 deletion syncthing_gtk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from timermanager import TimerManager
from daemonprocess import DaemonProcess
from daemon import Daemon, InvalidConfigurationException, \
TLSUnsupportedException, ConnectionRestarted
TLSUnsupportedException, ConnectionRestarted, \
TLSErrorException
if not "GTK2APP" in os.environ:
# Condition above prevents __init__ from loading stuff that
# depends on GTK3-only features, allowing GTK2 apps to use
Expand Down
36 changes: 27 additions & 9 deletions syncthing_gtk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@ def setup_widgets(self):
submenu.add(menuitem)
self[limitmenu].show_all()

if not old_gtk:
if not Gtk.IconTheme.get_default().has_icon(self["edit-menu-icon"].get_icon_name()[0]):
# If requested icon is not found in default theme, replace it with emblem-system-symbolic
self["edit-menu-icon"].set_from_icon_name("emblem-system-symbolic", self["edit-menu-icon"].get_icon_name()[1])

# Set window title in way that even Gnome can understand
self["window"].set_title(_("Syncthing GTK"))
self["window"].set_wmclass("Syncthing GTK", "Syncthing GTK")
Expand All @@ -309,6 +314,10 @@ def setup_connection(self):
self.hide()
self.show_wizard()
return False
except TLSErrorException, e:
# This is pretty-much fatal. Display error message and bail out.
self.cb_syncthing_con_error(daemon, Daemon.UNKNOWN, str(e), e)
return False
# Enable filesystem watching and desktop notifications,
# if desired and possible
if HAS_INOTIFY:
Expand Down Expand Up @@ -375,7 +384,7 @@ def start_daemon(self):
if windows.is_shutting_down():
log.warning("Not starting daemon: System shutdown detected")
return
self.process = DaemonProcess([self.config["syncthing_binary"], "-no-browser"])
self.process = DaemonProcess([self.config["syncthing_binary"], "-no-browser"], self.config["daemon_priority"])
self.process.connect('failed', self.cb_daemon_startup_failed)
self.process.connect('exit', self.cb_daemon_exit)
self.process.start()
Expand Down Expand Up @@ -969,7 +978,12 @@ def update_folders(self):
folder.set_color_hex(COLOR_FOLDER_OFFLINE)
elif not online and folder.compare_color_hex(COLOR_FOLDER_IDLE):
# Folder is offline and in Idle state (not scanning)
folder.set_status(_("Offline"))
if len([ d for d in folder["devices"] if d["id"] != self.daemon.get_my_id()]) == 0:
# No device to share folder with
folder.set_status(_("Unshared"))
else:
# Folder is shared, but all devices are offline
folder.set_status(_("Offline"))
folder.set_color_hex(COLOR_FOLDER_OFFLINE)

def show_error_box(self, ribar, additional_data={}):
Expand Down Expand Up @@ -1195,6 +1209,7 @@ def show_folder(self, id, name, path, is_master, ignore_perms, rescan_interval,
self["folderlist"].pack_start(box, False, False, 3)
box.set_open(id in self.open_boxes or self.folders_never_loaded)
box.connect('right-click', self.cb_popup_menu_folder)
box.connect('doubleclick', self.cb_browse_folder)
box.connect('enter-notify-event', self.cb_box_mouse_enter)
box.connect('leave-notify-event', self.cb_box_mouse_leave)
self.folders[id] = box
Expand Down Expand Up @@ -1581,7 +1596,11 @@ def cb_menu_popup_edit_device(self, *a):

def cb_menu_popup_browse_folder(self, *a):
""" Handler for 'browse' folder context menu item """
path = os.path.expanduser(self.rightclick_box["path"])
self.cb_browse_folder(self.rightclick_box)

def cb_browse_folder(self, box, *a):
""" Handler for 'browse' action """
path = os.path.expanduser(box["path"])
if IS_WINDOWS:
# Don't attempt anything, use Windows Explorer on Windows
os.system('explorer "%s"' % (path,))
Expand Down Expand Up @@ -1617,16 +1636,15 @@ def check_delete(self, mode, id, name):
"""
Asks user if he really wants to do what he just asked to do
"""
msg = _("Do you really want to permanently stop synchronizing directory '%s'?")
if mode == "device":
msg = _("Do you really want remove device '%s' from Syncthing?")
d = Gtk.MessageDialog(
self["window"],
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.QUESTION,
Gtk.ButtonsType.YES_NO,
"%s %s\n'%s'?" % (
_("Do you really want do delete"),
_("directory") if mode == "folder" else _("device"),
name
)
msg % name
)
r = d.run()
d.hide()
Expand Down Expand Up @@ -1792,7 +1810,7 @@ def cb_daemon_exit(self, proc, error_code):
# New daemon version is downloaded and ready to use.
# Switch to this version before restarting
self.swap_updated_binary()
self.process = DaemonProcess([self.config["syncthing_binary"], "-no-browser"])
self.process = DaemonProcess([self.config["syncthing_binary"], "-no-browser"], self.config["daemon_priority"])
self.process.connect('failed', self.cb_daemon_startup_failed)
self.process.connect('exit', self.cb_daemon_exit)
self.process.start()
Expand Down
1 change: 1 addition & 0 deletions syncthing_gtk/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class _Configuration(object):
REQUIRED_KEYS = {
"autostart_daemon" : (int, 2), # 0 - wait for daemon, 1 - autostart, 2 - ask
"autokill_daemon" : (int, 2), # 0 - never kill, 1 - always kill, 2 - ask
"daemon_priority" : (int, 0), # uses nice values
"syncthing_binary" : (str, "/usr/bin/syncthing"),
"minimize_on_start" : (bool, False),
"folder_as_path" : (bool, True),
Expand Down
27 changes: 26 additions & 1 deletion syncthing_gtk/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,15 @@ def _read_config(self):
except Exception, e:
pass
self._tls = False
self._cert = None
if tls.lower() == "true":
self._tls = True
try:
self._cert = Gio.TlsCertificate.new_from_file(
os.path.join(get_config_dir(), "syncthing", "https-cert.pem"))
except Exception, e:
log.exception(e)
raise TLSErrorException("Failed to load daemon certificate")
try:
self._address = xml.getElementsByTagName("configuration")[0] \
.getElementsByTagName("gui")[0] \
Expand Down Expand Up @@ -372,7 +379,9 @@ def _rest_request(self, command, callback, error_callback=None, *callback_data):
callback(json_data, callback_data... )
error_callback(exception, command, callback_data... )
"""
sc = Gio.SocketClient(tls=self._tls, tls_validation_flags=0)
sc = Gio.SocketClient(tls=self._tls)
if self._tls:
GObject.Object.connect(sc, "event", self._rest_socket_event)
sc.connect_to_host_async(self._address, 0, None, self._rest_connected,
(command, self._epoch, callback, error_callback, callback_data))

Expand Down Expand Up @@ -475,9 +484,20 @@ def _rest_error(self, exception, epoch, command, callback, error_callback, callb
log.error("Request '%s' failed; Repeating...", command)
self.timer(None, 1, self._rest_request, command, callback, error_callback, *callback_data)

def _rest_socket_event(self, sc, event, connectable, con):
""" Setups TSL certificate if HTTPS is used """
if event == Gio.SocketClientEvent.TLS_HANDSHAKING:
con.connect("accept-certificate", self._rest_accept_certificate)

def _rest_accept_certificate(self, con, peer_cert, errors):
""" Check if server presents expected certificate and accept connection """
return peer_cert.is_same(self._cert)

def _rest_post(self, command, data, callback, error_callback=None, *callback_data):
""" POSTs data (formated with json) to daemon. Works like _rest_request """
sc = Gio.SocketClient(tls=self._tls)
if self._tls:
GObject.Object.connect(sc, "event", self._rest_socket_event)
sc.connect_to_host_async(self._address, 0, None, self._rest_post_connected,
(command, data, self._epoch, callback, error_callback, callback_data))

Expand Down Expand Up @@ -1176,6 +1196,10 @@ def syncing(self):
""" Returns true if any folder is being synchronized right now """
return len(self._syncing_folders) > 0

def get_api_key(self):
""" Returns API key used for communication with daemon. May return None """
return self._api_key

def get_min_version(self):
"""
Returns minimal syncthing daemon version that daemon instance
Expand Down Expand Up @@ -1256,6 +1280,7 @@ def set_refresh_interval(self, i):

class InvalidConfigurationException(RuntimeError): pass
class TLSUnsupportedException(RuntimeError): pass
class TLSErrorException(RuntimeError): pass

class HTTPError(RuntimeError):
def __init__(self, message, full_response):
Expand Down
25 changes: 20 additions & 5 deletions syncthing_gtk/daemonprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from subprocess import Popen, PIPE, STARTUPINFO, \
STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE, \
CREATE_NEW_PROCESS_GROUP
from syncthing_gtk.windows import WinPopenReader
from syncthing_gtk.windows import WinPopenReader, nice_to_priority_class
elif not HAS_SUBPROCESS:
# Gio.Subprocess is not available in Gio < 3.12
from subprocess import Popen, PIPE
Expand All @@ -33,11 +33,17 @@ class DaemonProcess(GObject.GObject):
b"failed" : (GObject.SIGNAL_RUN_FIRST, None, (object,)),
}
SCROLLBACK_SIZE = 500 # Maximum number of output lines stored in memory
PRIORITY_LOWEST = 19
PRIORITY_LOW = 10
PRIORITY_NORMAL = 0
PRIORITY_HIGH = -10
PRIORITY_HIGHEST = -20

def __init__(self, commandline):
def __init__(self, commandline, priority=PRIORITY_NORMAL):
""" commandline should be list of arguments """
GObject.GObject.__init__(self)
self.commandline = commandline
self.priority = priority
self._proc = None

def start(self):
Expand All @@ -50,20 +56,29 @@ def start(self):
sinfo = STARTUPINFO()
sinfo.dwFlags = STARTF_USESHOWWINDOW
sinfo.wShowWindow = 0
cflags = nice_to_priority_class(self.priority)
self._proc = Popen(self.commandline,
stdin=PIPE, stdout=PIPE, stderr=PIPE,
startupinfo=sinfo)
startupinfo=sinfo, creationflags=cflags)
self._stdout = WinPopenReader(self._proc)
self._check = GLib.timeout_add_seconds(1, self._cb_check_alive)
elif HAS_SUBPROCESS:
# New Gio
flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE
self._proc = Gio.Subprocess.new(self.commandline, flags)
if self.priority == 0:
self._proc = Gio.Subprocess.new(self.commandline, flags)
else:
# I just really do hope that there is no distro w/out nice command
self._proc = Gio.Subprocess.new([ "nice", "-%s" % self.priority ] + self.commandline, flags)
self._proc.wait_check_async(None, self._cb_finished)
self._stdout = self._proc.get_stdout_pipe()
else:
# Gio < 3.12 - Gio.Subprocess is missing :(
self._proc = Popen(self.commandline, stdout=PIPE)
if self.priority == 0:
self._proc = Popen(self.commandline, stdout=PIPE)
else:
# still hoping
self._proc = Popen([ "nice", "-%s" % self.priority ], stdout=PIPE)
self._stdout = Gio.UnixInputStream.new(self._proc.stdout.fileno(), False)
self._check = GLib.timeout_add_seconds(1, self._cb_check_alive)
except Exception, e:
Expand Down
2 changes: 1 addition & 1 deletion syncthing_gtk/editordialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def display_value(self, key, w):
val = self.get_value(strip_v(key))
m = w.get_model()
for i in xrange(0, len(m)):
if val == str(m[i][0]).strip():
if str(val) == str(m[i][0]).strip():
w.set_active(i)
break
elif isinstance(w, Gtk.CheckButton):
Expand Down
59 changes: 46 additions & 13 deletions syncthing_gtk/iddialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from __future__ import unicode_literals
from gi.repository import Gtk, Gdk, Gio, GLib, Pango
from tools import IS_WINDOWS
import os, tempfile
import urllib2, httplib, ssl
import os, tempfile, logging
log = logging.getLogger("IDDialog")
_ = lambda (a) : a

class IDDialog(object):
Expand Down Expand Up @@ -38,21 +40,29 @@ def setup_widgets(self):
self.builder.add_from_file(os.path.join(self.app.gladepath, "device-id.glade"))
self.builder.connect_signals(self)
self["vID"].set_text(self.device_id)

def load_data(self):
""" Loads QR code from Syncthing daemon """
uri = "%s/qr/?text=%s" % (self.app.daemon.get_webui_url(), self.device_id)
if IS_WINDOWS:
import urllib2
data = urllib2.urlopen(uri).read()
tf = tempfile.NamedTemporaryFile("wb", suffix=".png", delete=False)
tf.write(data)
tf.close()
self["vQR"].set_from_file(tf.name)
os.unlink(tf.name)
else:
io = Gio.file_new_for_uri(uri)
io.load_contents_async(None, self.cb_syncthing_qr, ())
return self.load_data_urllib()
uri = "%s/qr/?text=%s" % (self.app.daemon.get_webui_url(), self.device_id)
io = Gio.file_new_for_uri(uri)
io.load_contents_async(None, self.cb_syncthing_qr, ())

def load_data_urllib(self):
""" Loads QR code from Syncthing daemon """
uri = "%s/qr/?text=%s" % (self.app.daemon.get_webui_url(), self.device_id)
api_key = self.app.daemon.get_api_key()
opener = urllib2.build_opener(DummyHTTPSHandler())
if not api_key is None:
opener.addheaders = [("X-API-Key", api_key)]
a = opener.open(uri)
data = a.read()
tf = tempfile.NamedTemporaryFile("wb", suffix=".png", delete=False)
tf.write(data)
tf.close()
self["vQR"].set_from_file(tf.name)
os.unlink(tf.name)

def cb_btClose_clicked(self, *a):
self.close()
Expand All @@ -72,7 +82,30 @@ def cb_syncthing_qr(self, io, results, *a):
tf.close()
self["vQR"].set_from_file(tf.name)
os.unlink(tf.name)
except GLib.Error, e:
if e.code == 14:
# Unauthorized. Grab CSRF token from daemon and try again
log.warning("Failed to load image using glib. Retrying with urllib2.")
self.load_data_urllib()
except Exception, e:
log.exception(e)
return
finally:
del io

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
class DummyHTTPSHandler(urllib2.HTTPSHandler):
"""
Dummy HTTPS handler that ignores certificate errors. This in unsafe,
but used ONLY for QR code images.
"""
def __init__(self):
urllib2.HTTPSHandler.__init__(self)

def https_open(self, req):
return self.do_open(self.getConnection, req)

def getConnection(self, host, timeout=300):
return httplib.HTTPSConnection(host, context=ctx)
Loading

0 comments on commit 89b2b94

Please sign in to comment.