1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Getting Things Gnome! - a personal organizer for the GNOME desktop
4
# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
6
# This program is free software: you can redistribute it and/or modify it under
7
# the terms of the GNU General Public License as published by the Free Software
8
# Foundation, either version 3 of the License, or (at your option) any later
11
# This program is distributed in the hope that it will be useful, but WITHOUT
12
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16
# You should have received a copy of the GNU General Public License along with
17
# this program. If not, see <http://www.gnu.org/licenses/>.
18
# -----------------------------------------------------------------------------
21
Contains the Backend class for both Tomboy and Gnote
23
#Note: To introspect tomboy, execute:
24
# qdbus org.gnome.Tomboy /org/gnome/Tomboy/RemoteControl
33
from GTG.tools.testingmode import TestingMode
34
from GTG.tools.borg import Borg
35
from GTG.backends.genericbackend import GenericBackend
36
from GTG.backends.backendsignals import BackendSignals
37
from GTG.backends.syncengine import SyncEngine, SyncMeme
38
from GTG.tools.logger import Log
39
from GTG.tools.watchdog import Watchdog
40
from GTG.tools.interruptible import interruptible
41
from GTG.tools.tags import extract_tags_from_text
45
class GenericTomboy(GenericBackend):
46
'''Backend class for Tomboy/Gnote'''
49
###############################################################################
50
### Backend standard methods ##################################################
51
###############################################################################
53
def __init__(self, parameters):
55
See GenericBackend for an explanation of this function.
57
super(GenericTomboy, self).__init__(parameters)
58
#loading the saved state of the synchronization, if any
59
self.data_path = os.path.join('backends/tomboy/', \
60
"sync_engine-" + self.get_id())
61
self.sync_engine = self._load_pickled_file(self.data_path, \
63
#if the backend is being tested, we connect to a different DBus
64
# interface to avoid clashing with a running instance of Tomboy
65
if TestingMode().get_testing_mode():
66
#just used for testing purposes
68
self._parameters["use this fake connection instead"]
70
self.BUS_ADDRESS = self._BUS_ADDRESS
71
#we let some time pass before considering a tomboy task for importing,
72
# as the user may still be editing it. Here, we store the Timer objects
73
# that will execute after some time after each tomboy signal.
74
#NOTE: I'm not sure if this is the case anymore (but it shouldn't hurt
75
# anyway). (invernizzi)
76
self._tomboy_setting_timers = {}
80
See GenericBackend for an explanation of this function.
81
Connects to the session bus and sets the callbacks for bus signals
83
super(GenericTomboy, self).initialize()
84
with self.DbusWatchdog(self):
85
bus = dbus.SessionBus()
86
bus.add_signal_receiver(self.on_note_saved,
87
dbus_interface = self.BUS_ADDRESS[2],
88
signal_name = "NoteSaved")
89
bus.add_signal_receiver(self.on_note_deleted,
90
dbus_interface = self.BUS_ADDRESS[2],
91
signal_name = "NoteDeleted")
94
def start_get_tasks(self):
96
See GenericBackend for an explanation of this function.
97
Gets all the notes from Tomboy and sees if they must be added in GTG
98
(and, if so, it adds them).
100
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
101
with self.DbusWatchdog(self):
102
tomboy_notes = [note_id for note_id in \
103
tomboy.ListAllNotes()]
105
for note in tomboy_notes:
106
self.cancellation_point()
107
self._process_tomboy_note(note)
108
#checking if some notes have been deleted while GTG was not running
109
stored_notes_ids = self.sync_engine.get_all_remote()
110
for note in set(stored_notes_ids).difference(set(tomboy_notes)):
111
self.on_note_deleted(note, None)
113
def save_state(self):
114
'''Saves the state of the synchronization'''
115
self._store_pickled_file(self.data_path, self.sync_engine)
117
def quit(self, disable = False):
119
See GenericBackend for an explanation of this function.
125
self._tomboy_setting_timers.iteritems().next()
126
except StopIteration:
129
del self._tomboy_setting_timers[key]
130
threading.Thread(target = quit_thread).start()
131
super(GenericTomboy, self).quit(disable)
133
###############################################################################
134
### Something got removed #####################################################
135
###############################################################################
138
def on_note_deleted(self, note, something):
140
Callback, executed when a tomboy note is deleted.
141
Deletes the related GTG task.
143
@param note: the id of the Tomboy note
144
@param something: not used, here for signal callback compatibility
146
with self.datastore.get_backend_mutex():
147
self.cancellation_point()
149
tid = self.sync_engine.get_local_id(note)
152
if self.datastore.has_task(tid):
153
self.datastore.request_task_deletion(tid)
154
self.break_relationship(remote_id = note)
157
def remove_task(self, tid):
159
See GenericBackend for an explanation of this function.
161
with self.datastore.get_backend_mutex():
162
self.cancellation_point()
164
note = self.sync_engine.get_remote_id(tid)
167
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
168
with self.DbusWatchdog(self):
169
if tomboy.NoteExists(note):
170
tomboy.DeleteNote(note)
171
self.break_relationship(local_id = tid)
173
def _exec_lost_syncability(self, tid, note):
175
Executed when a relationship between tasks loses its syncability
176
property. See SyncEngine for an explanation of that.
177
This function finds out which object (task/note) is the original one
178
and which is the copy, and deletes the copy.
180
@param tid: a GTG task tid
181
@param note: a tomboy note id
183
self.cancellation_point()
184
meme = self.sync_engine.get_meme_from_remote_id(note)
185
#First of all, the relationship is lost
186
self.sync_engine.break_relationship(remote_id = note)
187
if meme.get_origin() == "GTG":
188
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
189
with self.DbusWatchdog(self):
190
tomboy.DeleteNote(note)
192
self.datastore.request_task_deletion(tid)
194
###############################################################################
195
### Process tasks #############################################################
196
###############################################################################
198
def _process_tomboy_note(self, note):
200
Given a tomboy note, finds out if it must be synced to a GTG note and,
201
if so, it carries out the synchronization (by creating or updating a GTG
202
task, or deleting itself if the related task has been deleted)
204
@param note: a Tomboy note id
206
with self.datastore.get_backend_mutex():
207
self.cancellation_point()
208
is_syncable = self._tomboy_note_is_syncable(note)
209
with self.DbusWatchdog(self):
210
action, tid = self.sync_engine.analyze_remote_id(note, \
211
self.datastore.has_task, \
212
self._tomboy_note_exists, is_syncable)
213
Log.debug("processing tomboy (%s, %s)" % (action, is_syncable))
215
if action == SyncEngine.ADD:
216
tid = str(uuid.uuid4())
217
task = self.datastore.task_factory(tid)
218
self._populate_task(task, note)
219
self.record_relationship(local_id = tid,\
221
meme = SyncMeme(task.get_modified(),
222
self.get_modified_for_note(note),
224
self.datastore.push_task(task)
226
elif action == SyncEngine.UPDATE:
227
task = self.datastore.get_task(tid)
228
meme = self.sync_engine.get_meme_from_remote_id(note)
229
newest = meme.which_is_newest(task.get_modified(),
230
self.get_modified_for_note(note))
231
if newest == "remote":
232
self._populate_task(task, note)
233
meme.set_local_last_modified(task.get_modified())
234
meme.set_remote_last_modified(\
235
self.get_modified_for_note(note))
238
elif action == SyncEngine.REMOVE:
239
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
240
with self.DbusWatchdog(self):
241
tomboy.DeleteNote(note)
243
self.sync_engine.break_relationship(remote_id = note)
247
elif action == SyncEngine.LOST_SYNCABILITY:
248
self._exec_lost_syncability(tid, note)
251
def set_task(self, task):
253
See GenericBackend for an explanation of this function.
255
self.cancellation_point()
256
is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
258
with self.datastore.get_backend_mutex():
259
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
260
with self.DbusWatchdog(self):
261
action, note = self.sync_engine.analyze_local_id(tid, \
262
self.datastore.has_task, tomboy.NoteExists, \
264
Log.debug("processing gtg (%s, %d)" % (action, is_syncable))
266
if action == SyncEngine.ADD:
267
#GTG allows multiple tasks with the same name,
268
#Tomboy doesn't. we need to handle the renaming
270
title = task.get_title()
271
duplicate_counter = 1
272
with self.DbusWatchdog(self):
273
note = tomboy.CreateNamedNote(title)
275
duplicate_counter += 1
276
note = tomboy.CreateNamedNote(title + "(%d)" %
278
if duplicate_counter != 1:
279
#if we needed to rename, we have to rename also
281
task.set_title(title + " (%d)" % duplicate_counter)
283
self._populate_note(note, task)
284
self.record_relationship( \
285
local_id = tid, remote_id = note, \
286
meme = SyncMeme(task.get_modified(),
287
self.get_modified_for_note(note),
290
elif action == SyncEngine.UPDATE:
291
meme = self.sync_engine.get_meme_from_local_id(\
293
newest = meme.which_is_newest(task.get_modified(),
294
self.get_modified_for_note(note))
295
if newest == "local":
296
self._populate_note(note, task)
297
meme.set_local_last_modified(task.get_modified())
298
meme.set_remote_last_modified(\
299
self.get_modified_for_note(note))
302
elif action == SyncEngine.REMOVE:
303
self.datastore.request_task_deletion(tid)
305
self.sync_engine.break_relationship(local_id = tid)
310
elif action == SyncEngine.LOST_SYNCABILITY:
311
self._exec_lost_syncability(tid, note)
313
###############################################################################
314
### Helper methods ############################################################
315
###############################################################################
318
def on_note_saved(self, note):
320
Callback, executed when a tomboy note is saved by Tomboy itself.
321
Updates the related GTG task (or creates one, if necessary).
323
@param note: the id of the Tomboy note
325
self.cancellation_point()
326
#NOTE: we let some seconds pass before executing the real callback, as
327
# the editing of the Tomboy note may still be in progress
329
def _execute_on_note_saved(self, note):
330
self.cancellation_point()
332
del self._tomboy_setting_timers[note]
335
self._process_tomboy_note(note)
339
self._tomboy_setting_timers[note].cancel()
343
timer =threading.Timer(5, _execute_on_note_saved,
345
self._tomboy_setting_timers[note] = timer
348
def _tomboy_note_is_syncable(self, note):
350
Returns True if this tomboy note should be synced into GTG tasks.
352
@param note: the note id
355
attached_tags = self.get_attached_tags()
356
if GenericBackend.ALLTASKS_TAG in attached_tags:
358
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
359
with self.DbusWatchdog(self):
360
content = tomboy.GetNoteContents(note)
362
for tag in attached_tags:
371
def _tomboy_note_exists(self, note):
373
Returns True if a tomboy note exists with the given id.
375
@param note: the note id
378
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
379
with self.DbusWatchdog(self):
380
return tomboy.NoteExists(note)
382
def get_modified_for_note(self, note):
384
Returns the modification time for the given note id.
386
@param note: the note id
387
@returns datetime.datetime
389
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
390
with self.DbusWatchdog(self):
391
return datetime.datetime.fromtimestamp( \
392
tomboy.GetNoteChangeDate(note))
394
def _tomboy_split_title_and_text(self, content):
396
Tomboy does not have a "getTitle" and "getText" functions to get the
397
title and the text of a note separately. Instead, it has a getContent
398
function, that returns both of them.
399
This function splits up the output of getContent into a title string and
402
@param content: a string, the result of a getContent call
403
@returns list: a list composed by [title, text]
406
end_of_title = content.index('\n')
408
return content, unicode("")
409
title = content[: end_of_title]
410
if len(content) > end_of_title:
411
return title, content[end_of_title +1 :]
413
return title, unicode("")
415
def _populate_task(self, task, note):
417
Copies the content of a Tomboy note into a task.
419
@param task: a GTG Task
420
@param note: a Tomboy note
422
#add tags objects (it's not enough to have @tag in the text to add a
424
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
425
with self.DbusWatchdog(self):
426
content = tomboy.GetNoteContents(note)
427
#update the tags list
428
task.set_only_these_tags(extract_tags_from_text(content))
429
#extract title and text
430
[title, text] = self._tomboy_split_title_and_text(unicode(content))
431
#Tomboy speaks unicode, we don't
432
title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore')
433
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore')
434
task.set_title(title)
436
task.add_remote_id(self.get_id(), note)
438
def _populate_note(self, note, task):
440
Copies the content of a task into a Tomboy note.
442
@param note: a Tomboy note
443
@param task: a GTG Task
445
title = task.get_title()
447
duplicate_counter = 1
448
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
449
with self.DbusWatchdog(self):
450
tomboy.SetNoteContents(note, title + '\n' + \
451
task.get_excerpt(strip_tags = False))
453
def break_relationship(self, *args, **kwargs):
455
Proxy method for SyncEngine.break_relationship, which also saves the
456
state of the synchronization.
458
#tomboy passes Dbus.String objects, which are not pickable. We convert
460
kwargs["remote_id"] = unicode(kwargs["remote_id"])
462
self.sync_engine.break_relationship(*args, **kwargs)
463
#we try to save the state at each change in the sync_engine:
464
#it's slower, but it should avoid widespread task
470
def record_relationship(self, *args, **kwargs):
472
Proxy method for SyncEngine.break_relationship, which also saves the
473
state of the synchronization.
475
#tomboy passes Dbus.String objects, which are not pickable. We convert
477
kwargs["remote_id"] = unicode(kwargs["remote_id"])
479
self.sync_engine.record_relationship(*args, **kwargs)
480
#we try to save the state at each change in the sync_engine:
481
#it's slower, but it should avoid widespread task
485
###############################################################################
486
### Connection handling #######################################################
487
###############################################################################
491
class TomboyConnection(Borg):
493
TomboyConnection creates a connection to TOMBOY via DBUS and
494
handles all the possible exceptions.
495
It is a class that can be used with a with statement.
497
with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
502
def __init__(self, backend, bus_name, bus_path, bus_interface):
504
Sees if a TomboyConnection object already exists. If so, since we
505
are inheriting from a Borg object, the initialization already took
507
If not, it tries to connect to Tomboy via Dbus. If the connection
508
is not possible, the user is notified about it.
510
@param backend: a reference to a Backend
511
@param bus_name: the DBUS address of Tomboy
512
@param bus_path: the DBUS path of Tomboy RemoteControl
513
@param bus_interface: the DBUS address of Tomboy RemoteControl
515
super(GenericTomboy.TomboyConnection, self).__init__()
516
if hasattr(self, "tomboy_connection_is_ok") and \
517
self.tomboy_connection_is_ok:
519
self.backend = backend
520
with GenericTomboy.DbusWatchdog(backend):
521
bus = dbus.SessionBus()
522
obj = bus.get_object(bus_name, bus_path)
523
self.tomboy = dbus.Interface(obj, bus_interface)
524
self.tomboy_connection_is_ok = True
528
Returns the Tomboy connection
530
@returns dbus.Interface
534
def __exit__(self, exception_type, value, traceback):
536
Checks the state of the connection.
537
If something went wrong for the connection, notifies the user.
539
@param exception_type: the type of exception that occurred, or
541
@param value: the instance of the exception occurred, or None
542
@param traceback: the traceback of the error
543
@returns: False if some exception must be re-raised.
545
if isinstance(value, dbus.DBusException):
546
self.tomboy_connection_is_ok = False
547
self.backend.quit(disable = True)
548
BackendSignals().backend_failed(self.backend.get_id(), \
549
BackendSignals.ERRNO_DBUS)
556
class DbusWatchdog(Watchdog):
558
A simple watchdog to detect stale dbus connections
562
def __init__(self, backend):
564
Simple constructor, which sets _when_taking_too_long as the function
565
to run when the connection is taking too long.
567
@param backend: a Backend object
569
self.backend = backend
570
super(GenericTomboy.DbusWatchdog, self).__init__(3, \
571
self._when_taking_too_long)
573
def _when_taking_too_long(self):
575
Function that is executed when the Dbus connection seems to be
576
hanging. It disables the backend and signals the error to the user.
578
Log.error("Dbus connection is taking too long for the Tomboy/Gnote"
580
self.backend.quit(disable = True)
581
BackendSignals().backend_failed(self.backend.get_id(), \
582
BackendSignals.ERRNO_DBUS)