├── LICENSE
├── README.md
├── icons
├── syncthing-client-down.svg
├── syncthing-client-error.svg
├── syncthing-client-idle.svg
├── syncthing-client-paused.svg
├── syncthing-client-scanning.svg
├── syncthing-client-up.svg
├── syncthing-client-updating.svg
└── syncthing-client-updown.svg
├── start-syncthing.sh
├── syncthing-ubuntu-indicator.py
└── testserver.py
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | syncthing-ubuntu-indicator
2 | ==========================
3 |
4 | A [Syncthing] status menu for systems that support AppIndicator,
5 | using Syncthing's event interface to display information about what's going on.
6 |
7 | Syncthing v0.11.0 and higher are supported.
8 |
9 | This is a fork from Stuart Langridge's [syncthing-ubuntu-indicator].
10 |
11 | The project is in an early stage and contributions are welcome.
12 |
13 | dependencies
14 | ==========================
15 |
16 | * python2 AppIndicator3
17 | * python2 gtk
18 | * python-dateutil (python2 version)
19 | * python-tz (python2 version)
20 | * requests-futures (python2 version)
21 |
22 |
23 | (I'm running Arch Linux so somebody maybe can fill the dependencies for other distributions, if there are some?)
24 |
25 | installation
26 | ==========================
27 | Tldr:
28 | in a directory of your choice:
29 |
30 | git clone https://github.com/icaruseffect/syncthing-ubuntu-indicator.git
31 |
32 | cd syncthing-ubuntu-indicator
33 |
34 | python2 ./syncthing-ubuntu-indicator.py
35 |
36 | When you are using GNOME3 you can move the icon with the [gnome-shell-extension-appindicator] in the upper notification bar.
37 |
38 |
39 | _On Ubuntu 14.04:_
40 |
41 | sudo apt-get install python-pip python-tz
42 |
43 | sudo pip install python-dateutil requests-futures
44 |
45 | this should result in the installation of the following packages:
46 | * python-colorama
47 | * python-distlib
48 | * python-html5lib
49 | * python-pip
50 | * python-requests
51 | * python-setuptools
52 | * python-urllib3
53 | * python-dateutil
54 |
55 | then go to [gnome-extensions-appindicator] and install "AppIndicator support"
56 |
57 |
58 |
59 |
60 |
61 | [Syncthing]: https://github.com/syncthing/syncthing
62 |
63 | [syncthing-ubuntu-indicator]: https://github.com/stuartlangridge/syncthing-ubuntu-indicator
64 |
65 | [gnome-shell-extension-appindicator]: https://github.com/rgcjonas/gnome-shell-extension-appindicator
66 |
67 | [gnome-extensions-appindicator]: https://extensions.gnome.org/extension/615/appindicator-support/
68 |
69 |
--------------------------------------------------------------------------------
/icons/syncthing-client-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/icons/syncthing-client-error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/icons/syncthing-client-idle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/icons/syncthing-client-paused.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/icons/syncthing-client-scanning.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/icons/syncthing-client-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/icons/syncthing-client-updating.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/icons/syncthing-client-updown.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/start-syncthing.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ## Uncomment one of the commands below and edit it with your Syncthing install path.
4 | ## For more information try:
5 | ## syncthing -help
6 | ## https://github.com/syncthing/syncthing/wiki
7 | ## https://forum.syncthing.net/c/howto
8 |
9 | ## Example commands:
10 | # /path/to/syncthing -no-browser &
11 | # nice -n 19 ionice -c3 /path/to/syncthing -no-browser &
12 | # GOMAXPROCS=1 nice -n 19 ionice -c3 /path/to/syncthing -no-browser &
13 |
--------------------------------------------------------------------------------
/syncthing-ubuntu-indicator.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # -*- coding: utf-8 -*-
3 |
4 | import argparse
5 | import datetime
6 | import dateutil.parser
7 | import json
8 | import logging as log
9 | import os
10 | import subprocess
11 | import sys
12 | import urlparse
13 | import webbrowser
14 |
15 | import pytz
16 | import requests # used only to catch exceptions
17 | import socket # used only to catch exceptions
18 | from requests_futures.sessions import FuturesSession
19 | from gi.repository import Gtk, Gio, GLib
20 | from gi.repository import AppIndicator3 as appindicator
21 | from xml.dom import minidom
22 |
23 | VERSION = 'v0.3.1'
24 |
25 |
26 | def shorten_path(text, maxlength=80):
27 | if len(text) <= maxlength:
28 | return text
29 | head, tail = os.path.split(text)
30 | if len(tail) > maxlength:
31 | return tail[:maxlength] # TODO: separate file extension
32 | while len(head) + len(tail) > maxlength:
33 | head = '/'.join(head.split('/')[:-1])
34 | if head == '':
35 | return '.../' + tail
36 | return head + '/.../' + tail
37 |
38 |
39 | class Main(object):
40 | def __init__(self, args):
41 | log.info('Started main procedure')
42 | self.args = args
43 | self.wd = os.path.normpath(os.path.abspath(os.path.split(__file__)[0]))
44 | self.icon_path = os.path.join(self.wd, 'icons')
45 | self.ind = appindicator.Indicator.new_with_path(
46 | 'syncthing-indicator',
47 | 'syncthing-client-idle',
48 | appindicator.IndicatorCategory.APPLICATION_STATUS,
49 | self.icon_path)
50 | self.ind.set_status(appindicator.IndicatorStatus.ACTIVE)
51 |
52 | self.state = {'update_folders': True,
53 | 'update_devices': True,
54 | 'update_files': True,
55 | 'update_st_running': False,
56 | 'set_icon': 'paused'}
57 | self.set_icon()
58 | self.create_menu()
59 |
60 | self.downloading_files = []
61 | self.recent_files = []
62 | self.folders = []
63 | self.devices = []
64 | self.errors = []
65 |
66 | self.last_ping = None
67 | self.system_data = {}
68 | self.syncthing_base = 'http://localhost:8080'
69 | self.syncthing_version = ''
70 | self.device_name = ''
71 | self.last_seen_id = 0
72 | self.timeout_counter = 0
73 | self.count_connection_error = 0
74 | self.session = FuturesSession()
75 |
76 | GLib.idle_add(self.load_config_begin)
77 |
78 | def create_menu(self):
79 | self.menu = Gtk.Menu()
80 |
81 | self.title_menu = Gtk.MenuItem('Syncthing')
82 | self.title_menu.show()
83 | self.title_menu.set_sensitive(False)
84 | self.menu.append(self.title_menu)
85 |
86 | self.syncthing_upgrade_menu = Gtk.MenuItem('Upgrade check')
87 | self.syncthing_upgrade_menu.connect('activate', self.open_releases_page)
88 | self.menu.append(self.syncthing_upgrade_menu)
89 |
90 | self.mi_errors = Gtk.MenuItem('Errors: open web interface')
91 | self.mi_errors.connect('activate', self.open_web_ui)
92 | self.menu.append(self.mi_errors)
93 |
94 | sep = Gtk.SeparatorMenuItem()
95 | sep.show()
96 | self.menu.append(sep)
97 |
98 | self.devices_menu = Gtk.MenuItem('Devices')
99 | self.devices_menu.show()
100 | self.devices_menu.set_sensitive(False)
101 | self.menu.append(self.devices_menu)
102 | self.devices_submenu = Gtk.Menu()
103 | self.devices_menu.set_submenu(self.devices_submenu)
104 |
105 | self.folder_menu = Gtk.MenuItem('Folders')
106 | self.folder_menu.show()
107 | self.folder_menu.set_sensitive(False)
108 | self.menu.append(self.folder_menu)
109 | self.folder_menu_submenu = Gtk.Menu()
110 | self.folder_menu.set_submenu(self.folder_menu_submenu)
111 |
112 | sep = Gtk.SeparatorMenuItem()
113 | sep.show()
114 | self.menu.append(sep)
115 |
116 | self.current_files_menu = Gtk.MenuItem('Downloading files')
117 | self.current_files_menu.show()
118 | self.current_files_menu.set_sensitive(False)
119 | self.menu.append(self.current_files_menu)
120 | self.current_files_submenu = Gtk.Menu()
121 | self.current_files_menu.set_submenu(self.current_files_submenu)
122 |
123 | self.recent_files_menu = Gtk.MenuItem('Recently updated')
124 | self.recent_files_menu.show()
125 | self.recent_files_menu.set_sensitive(False)
126 | self.menu.append(self.recent_files_menu)
127 | self.recent_files_submenu = Gtk.Menu()
128 | self.recent_files_menu.set_submenu(self.recent_files_submenu)
129 |
130 | sep = Gtk.SeparatorMenuItem()
131 | sep.show()
132 | self.menu.append(sep)
133 |
134 | open_web_ui = Gtk.MenuItem('Open web interface')
135 | open_web_ui.connect('activate', self.open_web_ui)
136 | open_web_ui.show()
137 | self.menu.append(open_web_ui)
138 |
139 | self.more_menu = Gtk.MenuItem('More')
140 | self.more_menu.show()
141 | self.menu.append(self.more_menu)
142 |
143 | self.more_submenu = Gtk.Menu()
144 | self.more_menu.set_submenu(self.more_submenu)
145 |
146 | self.mi_start_syncthing = Gtk.MenuItem('Start Syncthing')
147 | self.mi_start_syncthing.connect('activate', self.syncthing_start)
148 | self.mi_start_syncthing.set_sensitive(False)
149 | self.more_submenu.append(self.mi_start_syncthing)
150 |
151 | self.mi_restart_syncthing = Gtk.MenuItem('Restart Syncthing')
152 | self.mi_restart_syncthing.connect('activate', self.syncthing_restart)
153 | self.mi_restart_syncthing.set_sensitive(False)
154 | self.more_submenu.append(self.mi_restart_syncthing)
155 |
156 | self.mi_shutdown_syncthing = Gtk.MenuItem('Shutdown Syncthing')
157 | self.mi_shutdown_syncthing.connect('activate', self.syncthing_shutdown)
158 | self.mi_shutdown_syncthing.set_sensitive(False)
159 | self.more_submenu.append(self.mi_shutdown_syncthing)
160 |
161 | sep = Gtk.SeparatorMenuItem()
162 | self.more_submenu.append(sep)
163 |
164 | if not self.args.no_shutdown:
165 | self.mi_start_syncthing.show()
166 | self.mi_restart_syncthing.show()
167 | self.mi_shutdown_syncthing.show()
168 | sep.show()
169 |
170 | self.about_menu = Gtk.MenuItem('About Indicator')
171 | self.about_menu.connect('activate', self.show_about)
172 | self.about_menu.show()
173 | self.more_submenu.append(self.about_menu)
174 |
175 | self.quit_button = Gtk.MenuItem('Quit Indicator')
176 | self.quit_button.connect('activate', self.leave)
177 | self.quit_button.show()
178 | self.more_submenu.append(self.quit_button)
179 |
180 | self.ind.set_menu(self.menu)
181 |
182 | def load_config_begin(self):
183 | ''' Read needed values from config file '''
184 | confdir = GLib.get_user_config_dir()
185 | if not confdir:
186 | confdir = os.path.expanduser('~/.config')
187 | conffile = os.path.join(confdir, 'syncthing', 'config.xml')
188 | if not os.path.isfile(conffile):
189 | log.error("load_config_begin: Couldn't find config file {}".format(
190 | conffile))
191 | f = Gio.file_new_for_path(conffile)
192 | f.load_contents_async(None, self.load_config_finish)
193 | return False
194 |
195 | def load_config_finish(self, fp, async_result):
196 | try:
197 | success, data, etag = fp.load_contents_finish(async_result)
198 |
199 | dom = minidom.parseString(data)
200 |
201 | conf = dom.getElementsByTagName('configuration')
202 | if not conf:
203 | raise Exception('No configuration element in config')
204 |
205 | gui = conf[0].getElementsByTagName('gui')
206 | if not gui:
207 | raise Exception('No gui element in config')
208 |
209 | # Find the local syncthing address
210 | address = gui[0].getElementsByTagName('address')
211 | if not address:
212 | raise Exception('No address element in config')
213 | if not address[0].hasChildNodes():
214 | raise Exception('No address specified in config')
215 |
216 | self.syncthing_base = 'http://%s' % address[0].firstChild.nodeValue
217 |
218 | # Find and fetch the api key
219 | api_key = gui[0].getElementsByTagName('apikey')
220 | if not api_key:
221 | raise Exception('No api-key element in config')
222 | if not api_key[0].hasChildNodes():
223 | raise Exception('No api-key specified in config, please create one via the web interface')
224 | self.api_key = api_key[0].firstChild.nodeValue
225 |
226 | # Read folders and devices from config
227 | for elem in conf[0].childNodes:
228 | if elem.nodeType != minidom.Node.ELEMENT_NODE:
229 | continue
230 | if elem.tagName == 'device':
231 | self.devices.append({
232 | 'id': elem.getAttribute('id'),
233 | 'name': elem.getAttribute('name'),
234 | 'state': '',
235 | 'connected': False
236 | })
237 | elif elem.tagName == 'folder':
238 | self.folders.append({
239 | 'id': elem.getAttribute('id'),
240 | 'path': elem.getAttribute('path'),
241 | 'state': 'unknown',
242 | })
243 | if not self.devices:
244 | raise Exception('No devices in config')
245 | if not self.folders:
246 | raise Exception('No folders in config')
247 | except Exception as e:
248 | log.error('Error parsing config file: {}'.format(e))
249 | self.leave()
250 |
251 | # Start processes
252 | GLib.idle_add(self.rest_get, '/rest/system/version')
253 | GLib.idle_add(self.rest_get, '/rest/system/connections')
254 | GLib.idle_add(self.rest_get, '/rest/system/status')
255 | GLib.idle_add(self.rest_get, '/rest/system/upgrade')
256 | GLib.idle_add(self.rest_get, '/rest/system/error')
257 | GLib.idle_add(self.rest_get, '/rest/events')
258 | GLib.timeout_add_seconds(self.args.timeout_gui, self.update)
259 | GLib.timeout_add_seconds(self.args.timeout_rest, self.timeout_rest)
260 | GLib.timeout_add_seconds(self.args.timeout_event, self.timeout_events)
261 |
262 | def syncthing_url(self, url):
263 | ''' Creates a url from given values and the address read from file '''
264 | return urlparse.urljoin(self.syncthing_base, url)
265 |
266 | def open_web_ui(self, *args):
267 | webbrowser.open(self.syncthing_url(''))
268 |
269 | def open_releases_page(self, *args):
270 | webbrowser.open('https://github.com/syncthing/syncthing/releases')
271 |
272 | def rest_post(self, rest_path):
273 | log.debug('rest_post {}'.format(rest_path))
274 | headers = {'X-API-Key': self.api_key}
275 | if rest_path in ['/rest/system/restart', '/rest/system/shutdown']:
276 | f = self.session.post(
277 | self.syncthing_url(rest_path), headers=headers)
278 | return False
279 |
280 | def rest_get(self, rest_path):
281 | params = ''
282 | if rest_path == '/rest/events':
283 | params = {'since': self.last_seen_id}
284 |
285 | log.info('rest_get {} {}'.format(rest_path, params))
286 | # url for the included testserver: http://localhost:5115
287 | headers = {'X-API-Key': self.api_key}
288 | f = self.session.get(self.syncthing_url(rest_path),
289 | params=params,
290 | headers=headers,
291 | timeout=9)
292 | f.add_done_callback(self.rest_receive_data)
293 | return False
294 |
295 | def rest_receive_data(self, future):
296 | try:
297 | r = future.result()
298 | except requests.exceptions.ConnectionError:
299 | log.error(
300 | "Couldn't connect to Syncthing REST interface at {}".format(
301 | self.syncthing_base))
302 | self.count_connection_error += 1
303 | log.info('count_connection_error: {}'.format(self.count_connection_error))
304 | if self.count_connection_error > 1:
305 | self.state['update_st_running'] = True
306 | self.set_state('paused')
307 | return
308 | except (requests.exceptions.Timeout, socket.timeout):
309 | log.debug('Timeout')
310 | # Timeout may be because Syncthing restarted and event ID reset.
311 | GLib.idle_add(self.rest_get, '/rest/system/status')
312 | return
313 | except Exception as e:
314 | log.error('exception: {}'.format(e))
315 | return
316 |
317 | rest_path = urlparse.urlparse(r.url).path
318 | rest_query = urlparse.urlparse(r.url).query
319 | if r.status_code != 200:
320 | log.warning('rest_receive_data: {0} failed ({1})'.format(
321 | rest_path, r.status_code))
322 | if rest_path == '/rest/system/upgrade':
323 | # Debian/Ubuntu Syncthing packages disable upgrade check
324 | pass
325 | else:
326 | self.set_state('error')
327 | if rest_path == '/rest/system/ping':
328 | # Basic version check: try the old REST path
329 | GLib.idle_add(self.rest_get, '/rest/ping')
330 | return
331 |
332 | try:
333 | json_data = r.json()
334 | except:
335 | log.warning('rest_receive_data: Cannot process REST data')
336 | self.set_state('error')
337 | return
338 |
339 | # Receiving data appears to have succeeded
340 | self.count_connection_error = 0
341 | self.set_state('idle') # TODO: fix this
342 | log.debug('rest_receive_data: {} {}'.format(rest_path, rest_query))
343 | if rest_path == '/rest/events':
344 | try:
345 | for qitem in json_data:
346 | self.process_event(qitem)
347 | except Exception as e:
348 | log.warning(
349 | 'rest_receive_data: error processing event ({})'.format(e))
350 | log.debug(qitem)
351 | self.set_state('error')
352 | else:
353 | fn = getattr(
354 | self,
355 | 'process_{}'.format(rest_path.strip('/').replace('/', '_'))
356 | )(json_data)
357 |
358 | # processing of the events coming from the event interface
359 | def process_event(self, event):
360 | t = event.get('type').lower()
361 | if hasattr(self, 'event_{}'.format(t)):
362 | log.debug('received event: {} {}'.format(
363 | event.get('id'), event.get('type')))
364 | pass
365 | else:
366 | log.debug('ignoring event: {} {}'.format(
367 | event.get('id'), event.get('type')))
368 |
369 | #log.debug(json.dumps(event, indent=4))
370 | fn = getattr(self, 'event_{}'.format(t), self.event_unknown_event)(event)
371 | self.update_last_seen_id(event.get('id', 0))
372 |
373 | def event_unknown_event(self, event):
374 | pass
375 |
376 | def event_statechanged(self, event):
377 | for elem in self.folders:
378 | if elem['id'] == event['data']['folder']:
379 | elem['state'] = event['data']['to']
380 | self.state['update_folders'] = True
381 | self.set_state()
382 |
383 | def event_foldersummary(self, event):
384 | for elem in self.folders:
385 | if elem['id'] == event['data']['folder']:
386 | elem.update(event['data']['summary'])
387 | self.state['update_folders'] = True
388 |
389 | def event_foldercompletion(self, event):
390 | for dev in self.devices:
391 | if dev['id'] == event['data']['device']:
392 | if event['data']['completion'] < 100:
393 | dev['state'] = 'syncing'
394 | else:
395 | dev['state'] = ''
396 | self.state['update_devices'] = True
397 |
398 | def event_starting(self, event):
399 | self.set_state('paused')
400 | log.info('Received that Syncthing was starting at %s' % event['time'])
401 | # Check for added/removed devices or folders.
402 | GLib.idle_add(self.rest_get, '/rest/system/config')
403 | GLib.idle_add(self.rest_get, '/rest/system/version')
404 |
405 | def event_startupcomplete(self, event):
406 | self.set_state('idle')
407 | log.info('Syncthing startup complete at %s' %
408 | self.convert_time(event['time']))
409 |
410 | def event_ping(self, event):
411 | self.last_ping = dateutil.parser.parse(event['time'])
412 |
413 | def event_devicediscovered(self, event):
414 | found = False
415 | for elm in self.devices:
416 | if elm['id'] == event['data']['device']:
417 | elm['state'] = 'discovered'
418 | found = True
419 | if not found:
420 | log.warn('unknown device discovered')
421 | self.devices.append({
422 | 'id': event['data']['device'],
423 | 'name': 'new unknown device',
424 | 'address': event['data']['addrs'],
425 | 'state': 'unknown',
426 | })
427 | self.state['update_devices'] = True
428 |
429 | def event_deviceconnected(self, event):
430 | for dev in self.devices:
431 | if event['data']['id'] == dev['id']:
432 | dev['connected'] = True
433 | log.info('Device connected: %s' % dev['name'])
434 | self.state['update_devices'] = True
435 |
436 | def event_devicedisconnected(self, event):
437 | for dev in self.devices:
438 | if event['data']['id'] == dev['id']:
439 | dev['connected'] = False
440 | log.info('Device disconnected: %s' % dev['name'])
441 | self.state['update_devices'] = True
442 |
443 | def event_itemstarted(self, event):
444 | log.debug(u'item started: {}'.format(event['data']['item']))
445 | file_details = {'folder': event['data']['folder'],
446 | 'file': event['data']['item'],
447 | 'type': event['data']['type'],
448 | 'direction': 'down'}
449 | if file_details not in self.downloading_files:
450 | self.downloading_files.append(file_details)
451 | for elm in self.folders:
452 | if elm['id'] == event['data']['folder']:
453 | elm['state'] = 'syncing'
454 | self.set_state()
455 | self.state['update_files'] = True
456 |
457 | def event_itemfinished(self, event):
458 | # TODO: test whether 'error' is null
459 | log.debug(u'item finished: {}'.format(event['data']['item']))
460 | file_details = {'folder': event['data']['folder'],
461 | 'file': event['data']['item'],
462 | 'type': event['data']['type'],
463 | 'direction': 'down'}
464 | try:
465 | self.downloading_files.remove(file_details)
466 | log.debug('file locally updated: %s' % file_details['file'])
467 | except ValueError:
468 | log.debug('Completed a file we didn\'t know about: {}'.format(
469 | event['data']['item']))
470 | file_details['time'] = event['time']
471 | file_details['action'] = event['data']['action']
472 | self.recent_files.insert(0, file_details)
473 | self.recent_files = self.recent_files[:20]
474 | self.state['update_files'] = True
475 | # end of the event processing dings
476 |
477 | # begin REST processing functions
478 | def process_rest_system_connections(self, data):
479 | for elem in data['connections']:
480 | for dev in self.devices:
481 | if dev['id'] == elem:
482 | dev['connected'] = True
483 | self.state['update_devices'] = True
484 |
485 | def process_rest_system_config(self, data):
486 | log.info('Processing /rest/system/config')
487 | self.api_key = data['gui']['apiKey']
488 |
489 | newfolders = []
490 | for elem in data['folders']:
491 | newfolders.append({
492 | 'id': elem['id'],
493 | 'path': elem['path'],
494 | 'state': 'unknown',
495 | })
496 |
497 | newdevices = []
498 | for elem in data['devices']:
499 | newdevices.append({
500 | 'id': elem['deviceID'],
501 | 'name': elem['name'],
502 | 'state': '',
503 | 'connected': False,
504 | })
505 |
506 | self.folders = newfolders
507 | self.devices = newdevices
508 |
509 | def process_rest_system_status(self, data):
510 | if data['uptime'] < self.system_data.get('uptime', 0):
511 | # Means that Syncthing restarted
512 | self.last_seen_id = 0
513 | GLib.idle_add(self.rest_get, '/rest/system/version')
514 | self.system_data = data
515 | # TODO: check status of global announce
516 | self.state['update_st_running'] = True
517 |
518 | def process_rest_system_upgrade(self, data):
519 | self.syncthing_version = data['running']
520 | if data['newer']:
521 | self.syncthing_upgrade_menu.set_label(
522 | 'New version available: %s' % data['latest'])
523 | self.syncthing_upgrade_menu.show()
524 | else:
525 | self.syncthing_upgrade_menu.hide()
526 | self.state['update_st_running'] = True
527 |
528 | def process_rest_system_version(self, data):
529 | self.syncthing_version = data['version']
530 | self.state['update_st_running'] = True
531 |
532 | def process_rest_system_ping(self, data):
533 | if data['ping'] == 'pong':
534 | log.info('Connected to Syncthing REST interface at {}'.format(
535 | self.syncthing_url('')))
536 |
537 | def process_rest_ping(self, data):
538 | if data['ping'] == 'pong':
539 | # Basic version check
540 | log.error('Detected running Syncthing version < v0.11')
541 | log.error('Syncthing v0.11 (or higher) required. Exiting.')
542 | self.leave()
543 |
544 | def process_rest_system_error(self, data):
545 | self.errors = data['errors']
546 | if self.errors:
547 | log.info('{}'.format(data['errors']))
548 | self.mi_errors.show()
549 | self.set_state('error')
550 | else:
551 | self.mi_errors.hide()
552 | # end of the REST processing functions
553 |
554 | def update(self):
555 | for func in self.state:
556 | if self.state[func]:
557 | log.debug('self.update {}'.format(func))
558 | start = getattr(self, '%s' % func)()
559 | return True
560 |
561 | def update_last_checked(self, isotime):
562 | #dt = dateutil.parser.parse(isotime)
563 | #self.last_checked_menu.set_label('Last checked: %s' % (dt.strftime('%H:%M'),))
564 | pass
565 |
566 | def update_last_seen_id(self, lsi):
567 | if lsi > self.last_seen_id:
568 | self.last_seen_id = lsi
569 |
570 | def update_devices(self):
571 | if self.devices:
572 | # TODO: set icon if zero devices are connected
573 | self.devices_menu.set_label('Devices ({}/{})'.format(
574 | self.count_connected(), len(self.devices) - 1))
575 | self.devices_menu.set_sensitive(True)
576 |
577 | if len(self.devices_submenu) == len(self.devices) - 1:
578 | # Update the devices menu
579 | for mi in self.devices_submenu:
580 | for elm in self.devices:
581 | if mi.get_label().split(' ')[0] == elm['name']:
582 | mi.set_label(elm['name'])
583 | mi.set_sensitive(elm['connected'])
584 | else:
585 | # Repopulate the devices menu
586 | for child in self.devices_submenu.get_children():
587 | self.devices_submenu.remove(child)
588 |
589 | for elm in sorted(self.devices, key=lambda elm: elm['name']):
590 | if elm['id'] == self.system_data.get('myID', None):
591 | self.device_name = elm['name']
592 | self.state['update_st_running'] = True
593 | else:
594 | mi = Gtk.MenuItem(elm['name'])
595 | mi.set_sensitive(elm['connected'])
596 | self.devices_submenu.append(mi)
597 | mi.show()
598 | else:
599 | self.devices_menu.set_label('No devices')
600 | self.devices_menu.set_sensitive(False)
601 | self.state['update_devices'] = False
602 |
603 | def update_files(self):
604 | self.current_files_menu.set_label(u'Downloading %s files' % (
605 | len(self.downloading_files)))
606 |
607 | if not self.downloading_files:
608 | self.current_files_menu.set_sensitive(False)
609 | #self.set_state('idle')
610 | else:
611 | # Repopulate the current files menu
612 | self.current_files_menu.set_sensitive(True)
613 | self.set_state('syncing')
614 | for child in self.current_files_submenu.get_children():
615 | self.current_files_submenu.remove(child)
616 | for f in self.downloading_files:
617 | mi = Gtk.MenuItem(u'\u2193 [{}] {}'.format(
618 | f['folder'],
619 | shorten_path(f['file'])))
620 | self.current_files_submenu.append(mi)
621 | mi.connect(
622 | 'activate',
623 | self.open_file_browser,
624 | os.path.split(
625 | self.get_full_path(f['folder'], f['file']))[0])
626 | mi.show()
627 | self.current_files_menu.show()
628 |
629 | # Repopulate the recent files menu
630 | if not self.recent_files:
631 | self.recent_files_menu.set_sensitive(False)
632 | else:
633 | self.recent_files_menu.set_sensitive(True)
634 | for child in self.recent_files_submenu.get_children():
635 | self.recent_files_submenu.remove(child)
636 | icons = {'delete': u'\u2612', # [x]
637 | 'update': u'\u2193', # down arrow
638 | 'dir': u'\U0001f4c1', # folder
639 | 'file': u'\U0001f4c4', # file
640 | }
641 | for f in self.recent_files:
642 | mi = Gtk.MenuItem(
643 | u'{icon} {time} [{folder}] {item}'.format(
644 | icon=icons.get(f['action'], 'unknown'),
645 | folder=f['folder'],
646 | item=shorten_path(f['file']),
647 | time=self.convert_time(f['time'])
648 | )
649 | )
650 | self.recent_files_submenu.append(mi)
651 | mi.connect(
652 | 'activate',
653 | self.open_file_browser,
654 | os.path.split(
655 | self.get_full_path(f['folder'], f['file']))[0])
656 | mi.show()
657 | self.recent_files_menu.show()
658 | self.state['update_files'] = False
659 |
660 | def update_folders(self):
661 | if self.folders:
662 | self.folder_menu.set_sensitive(True)
663 | folder_maxlength = 0
664 | if len(self.folders) == len(self.folder_menu_submenu):
665 | for mi in self.folder_menu_submenu:
666 | for elm in self.folders:
667 | folder_maxlength = max(folder_maxlength, len(elm['id']))
668 | if str(mi.get_label()).split(' ', 1)[0] == elm['id']:
669 | if elm['state'] == 'scanning':
670 | mi.set_label('{} (scanning)'.format(elm['id']))
671 | elif elm['state'] == 'syncing':
672 | if elm.get('needFiles') > 1:
673 | lbltext = '{fid} (syncing {num} files)'
674 | elif elm.get('needFiles') == 1:
675 | lbltext = '{fid} (syncing {num} file)'
676 | else:
677 | lbltext = '{fid} (syncing)'
678 | mi.set_label(lbltext.format(
679 | fid=elm['id'], num=elm.get('needFiles')))
680 | else:
681 | mi.set_label(elm['id'].ljust(folder_maxlength + 20))
682 | else:
683 | for child in self.folder_menu_submenu.get_children():
684 | self.folder_menu_submenu.remove(child)
685 | for elm in self.folders:
686 | folder_maxlength = max(folder_maxlength, len(elm['id']))
687 | mi = Gtk.MenuItem(elm['id'].ljust(folder_maxlength + 20))
688 | mi.connect('activate', self.open_file_browser, elm['path'])
689 | self.folder_menu_submenu.append(mi)
690 | mi.show()
691 | else:
692 | self.folder_menu.set_sensitive(False)
693 | self.state['update_folders'] = False
694 |
695 | def update_st_running(self):
696 | if self.count_connection_error <= 1:
697 | if self.syncthing_version and self.device_name:
698 | self.title_menu.set_label(u'Syncthing {0} \u2022 {1}'.format(
699 | self.syncthing_version, self.device_name))
700 | else:
701 | self.title_menu.set_label(u'Syncthing')
702 | self.mi_start_syncthing.set_sensitive(False)
703 | self.mi_restart_syncthing.set_sensitive(True)
704 | self.mi_shutdown_syncthing.set_sensitive(True)
705 | else:
706 | self.title_menu.set_label('Could not connect to Syncthing')
707 | for dev in self.devices:
708 | dev['connected'] = False
709 | self.state['update_devices'] = True
710 | for f in self.folders:
711 | f['state'] = 'unknown'
712 | self.state['update_folders'] = True
713 | self.errors = []
714 | self.mi_errors.hide()
715 | self.set_state()
716 | self.mi_start_syncthing.set_sensitive(True)
717 | self.mi_restart_syncthing.set_sensitive(False)
718 | self.mi_shutdown_syncthing.set_sensitive(False)
719 |
720 | def count_connected(self):
721 | return len([e for e in self.devices if e['connected']])
722 |
723 | def syncthing_start(self, *args):
724 | cmd = [os.path.join(self.wd, 'start-syncthing.sh')]
725 | log.info('Starting {}'.format(cmd))
726 | try:
727 | proc = subprocess.Popen(cmd)
728 | except Exception as e:
729 | log.error("Couldn't run {}: {}".format(cmd, e))
730 | return
731 | GLib.idle_add(self.rest_get, '/rest/system/status')
732 | GLib.idle_add(self.rest_get, '/rest/system/version')
733 | self.state['update_st_running'] = True
734 |
735 | def syncthing_restart(self, *args):
736 | self.rest_post('/rest/system/restart')
737 | GLib.idle_add(self.rest_get, '/rest/system/status')
738 |
739 | def syncthing_shutdown(self, *args):
740 | self.rest_post('/rest/system/shutdown')
741 | GLib.idle_add(self.rest_get, '/rest/system/status')
742 |
743 | def convert_time(self, time):
744 | return dateutil.parser.parse(time).strftime('%x %X')
745 |
746 | def calc_speed(self, old, new):
747 | return old / (new * 10)
748 |
749 | def license(self):
750 | with open(os.path.join(self.wd, 'LICENSE'), 'r') as f:
751 | lic = f.read()
752 | return lic
753 |
754 | def show_about(self, widget):
755 | dialog = Gtk.AboutDialog()
756 | dialog.set_default_icon_from_file(
757 | os.path.join(self.icon_path, 'syncthing-client-idle.svg'))
758 | dialog.set_logo(None)
759 | dialog.set_program_name('Syncthing Ubuntu Indicator')
760 | dialog.set_version(VERSION)
761 | dialog.set_website('https://github.com/icaruseffect/syncthing-ubuntu-indicator')
762 | dialog.set_comments('This menu applet for systems supporting AppIndicator'
763 | '\ncan show the status of a Syncthing instance')
764 | dialog.set_license(self.license())
765 | dialog.run()
766 | dialog.destroy()
767 |
768 | def set_state(self, s=None):
769 | if not s:
770 | s = self.state['set_icon']
771 |
772 | if (s == 'error') or self.errors:
773 | self.state['set_icon'] = 'error'
774 | elif self.count_connection_error > 1:
775 | self.state['set_icon'] = 'paused'
776 | else:
777 | self.state['set_icon'] = self.folder_check_state()
778 |
779 | def folder_check_state(self):
780 | state = {'syncing': 0, 'idle': 0, 'cleaning': 0, 'scanning': 0,
781 | 'unknown': 0}
782 | for elem in self.folders:
783 | if elem['state'] in state:
784 | state[elem['state']] += 1
785 |
786 | if state['syncing'] > 0:
787 | return 'syncing'
788 | elif state['scanning'] > 0 or state['cleaning'] > 0:
789 | return 'scanning'
790 | else:
791 | return 'idle'
792 |
793 | def set_icon(self):
794 | icon = {
795 | 'updating': {'name': 'syncthing-client-updating', 'descr': 'Updating'},
796 | 'idle': {'name': 'syncthing-client-idle', 'descr': 'Nothing to do'},
797 | 'syncing': {'name': 'syncthing-client-updown', 'descr': 'Transferring Data'},
798 | 'error': {'name': 'syncthing-client-error', 'descr': 'Scotty, We Have A Problem!'},
799 | 'paused': {'name': 'syncthing-client-paused', 'descr': 'Paused'},
800 | 'scanning': {'name': 'syncthing-client-scanning', 'descr': 'Scanning Directories'},
801 | 'cleaning': {'name': 'syncthing-client-scanning', 'descr': 'Cleaning Directories'},
802 | }
803 |
804 | self.ind.set_attention_icon(icon[self.state['set_icon']]['name'])
805 | self.ind.set_icon_full(icon[self.state['set_icon']]['name'],
806 | icon[self.state['set_icon']]['descr'])
807 |
808 | def leave(self, widget):
809 | Gtk.main_quit()
810 |
811 | def timeout_rest(self):
812 | self.timeout_counter = (self.timeout_counter + 1) % 10
813 | if self.count_connection_error == 0:
814 | GLib.idle_add(self.rest_get, '/rest/system/connections')
815 | GLib.idle_add(self.rest_get, '/rest/system/status')
816 | GLib.idle_add(self.rest_get, '/rest/system/error')
817 | if self.timeout_counter == 0 or not self.syncthing_version:
818 | GLib.idle_add(self.rest_get, '/rest/system/upgrade')
819 | GLib.idle_add(self.rest_get, '/rest/system/version')
820 | else:
821 | GLib.idle_add(self.rest_get, '/rest/system/status')
822 | return True
823 |
824 | def timeout_events(self):
825 | if self.count_connection_error == 0:
826 | GLib.idle_add(self.rest_get, '/rest/events')
827 | return True
828 |
829 | def open_file_browser(self, menuitem, path):
830 | if not os.path.isdir(path):
831 | log.debug('Not a directory, or does not exist: {}'.format(path))
832 | return
833 | try:
834 | proc = subprocess.Popen(['xdg-open', path])
835 | except Exception as e:
836 | log.error("Couldn't open file browser for {} ({})".format(path, e))
837 |
838 | def get_full_path(self, folder, item):
839 | for elem in self.folders:
840 | if elem['id'] == folder:
841 | a = elem['path']
842 | return os.path.join(a, item)
843 |
844 |
845 | if __name__ == '__main__':
846 | import signal
847 | signal.signal(signal.SIGINT, signal.SIG_DFL)
848 |
849 | parser = argparse.ArgumentParser()
850 | parser.add_argument('--loglevel',
851 | choices=['debug', 'info', 'warning', 'error'], default='info')
852 | parser.add_argument('--timeout-event', type=int, default=10, metavar='N',
853 | help='Interval for polling event interface, in seconds. Default: %(default)s')
854 | parser.add_argument('--timeout-rest', type=int, default=30, metavar='N',
855 | help='Interval for polling REST interface, in seconds. Default: %(default)s')
856 | parser.add_argument('--timeout-gui', type=int, default=5, metavar='N',
857 | help='Interval for refreshing GUI, in seconds. Default: %(default)s')
858 | parser.add_argument('--no-shutdown', action='store_true',
859 | help='Hide Start, Restart, and Shutdown Syncthing menus')
860 |
861 | args = parser.parse_args()
862 | for arg in [args.timeout_event, args.timeout_rest, args.timeout_gui]:
863 | if arg < 1:
864 | sys.exit('Timeouts must be integers greater than 0')
865 |
866 | loglevels = {'debug': log.DEBUG, 'info': log.INFO,
867 | 'warning': log.WARNING, 'error': log.ERROR}
868 | log.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
869 | level=loglevels[args.loglevel])
870 | requests_log = log.getLogger('urllib3.connectionpool')
871 | requests_log.setLevel(log.WARNING)
872 | requests_log.propagate = True
873 |
874 | app = Main(args)
875 | Gtk.main()
876 |
--------------------------------------------------------------------------------
/testserver.py:
--------------------------------------------------------------------------------
1 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
2 | from SocketServer import ThreadingMixIn
3 | import threading
4 | import sys, tty, termios, time, random, string, json
5 | from datetime import tzinfo, timedelta, datetime
6 |
7 | class TZ(tzinfo):
8 | def utcoffset(self, dt): return timedelta(minutes=60)
9 |
10 | class Handler(BaseHTTPRequestHandler):
11 |
12 | def log_request(self, *args):
13 | pass
14 |
15 | def do_GET(self):
16 | global QUEUE
17 | self.send_response(200)
18 | self.end_headers()
19 | now = time.time()
20 | foundevents = False
21 | while 1:
22 | if QUEUE:
23 | self.wfile.write(json.dumps(QUEUE))
24 | QUEUE = []
25 | foundevents = True
26 | break
27 | elif time.time() - now > 10:
28 | break
29 | else:
30 | time.sleep(1)
31 | if not foundevents:
32 | self.wfile.write(json.dumps([make_action(ACTIONS[0], -1)]))
33 | self.wfile.write('\n')
34 | return
35 |
36 | class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
37 | """Handle requests in a separate thread."""
38 | timeout = 6
39 |
40 | def printtime():
41 | while 1:
42 | if not TYPING:
43 | print time.asctime(), TYPING, "\r",
44 |
45 | def randomLetters():
46 | return "".join([random.choice(string.uppercase) for x in range(10)])
47 |
48 | CREATED = {}
49 | def create(thing):
50 | if thing not in CREATED: CREATED[thing] = []
51 | val = randomLetters()
52 | CREATED[thing].append(val)
53 | return val
54 | def use(thing):
55 | if thing not in CREATED: return randomLetters()
56 | if not CREATED[thing]: return randomLetters()
57 | return CREATED[thing].pop()
58 |
59 | ACTIONS = [
60 | {"type": "TIMEOUT", "params": lambda: {}},
61 | {"type": "NODE_CONNECTED", "params": lambda: {"node": create("node")}},
62 | {"type": "NODE_DISCONNECTED", "params": lambda: {"node": use("node")}},
63 | {"type": "PULL_START", "params": lambda: {
64 | "repo": 'reponame', "file": create("file") + ".txt",
65 | "size": random.randint(1,10000), "modified": time.time(), "flags": ""}},
66 | {"type": "PULL_COMPLETE", "params": lambda: {
67 | "repo": 'reponame', "file": use("file") + ".txt"}},
68 | {"type": "PULL_ERROR", "params": lambda: {
69 | "repo": 'reponame', "file": use("file") + ".txt", "error": "MESSAGE"}}
70 | ]
71 |
72 | QUEUE = []
73 |
74 | def menu():
75 | for i in range(len(ACTIONS)):
76 | print "%s: %s" % (i, ACTIONS[i]["type"])
77 | print "q. quit"
78 |
79 | def make_action(action_template, action_id):
80 | ts = datetime.now()
81 | ts = ts.replace(tzinfo=TZ())
82 | action = {
83 | "type": action_template["type"],
84 | "params": action_template["params"](),
85 | "id": action_id,
86 | "timestamp": ts.isoformat()
87 | }
88 | return action
89 |
90 | if __name__ == '__main__':
91 | server = ThreadedHTTPServer(('localhost', 5115), Handler)
92 | print 'Starting server, use to stop'
93 | server = threading.Thread(target=server.serve_forever)
94 | server.daemon = True
95 | server.start()
96 | fd = sys.stdin.fileno()
97 | menu()
98 | acount = 1
99 | while 1:
100 | old_settings = termios.tcgetattr(fd)
101 | try:
102 | tty.setraw(sys.stdin.fileno())
103 | ch = sys.stdin.read(1)
104 | finally:
105 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
106 | if ch == "q": break
107 | try:
108 | action_template = ACTIONS[int(ch)]
109 | except:
110 | continue
111 | action = make_action(action_template, acount)
112 | print action
113 | QUEUE.append(action)
114 | acount += 1
115 | if acount % 20 == 0:
116 | menu()
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------