2
# -*- coding: UTF-8 -*-
4
Yokadi daemon. Used to monitor due tasks and warn user.
6
@author: Sébastien Renard <sebastien.renard@digitalfox.org>
13
from datetime import datetime, timedelta
14
from signal import SIGTERM, SIGHUP, signal
15
from subprocess import Popen
16
from optparse import OptionParser
17
from commands import getoutput
19
from sqlobject import AND
23
# Force default encoding to prefered encoding
25
sys.setdefaultencoding(tui.ENCODING)
28
from db import Config, Project, Task, connectDatabase
31
from syslog import openlog, syslog, LOG_USER
36
# Daemon polling delay (in seconds)
39
# Event sender to main loop
43
"""Send ouput to syslog if available, else defaulting to /tmp/yokadid.log"""
47
openlog("yokadi", 0, LOG_USER)
51
self.logfile=open("/tmp/yokadid-%s.log" % os.getpid(), "w+")
55
def write(self, output):
57
if output=="\n": return
60
self.logfile.write(output)
66
# Python unix daemon trick from the Activestate recipe 66012
73
print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
76
# decouple from parent environment
77
os.chdir("/") #don't prevent unmounting
85
# exit from second parent, print eventual PID before
86
print "Forking background with PID %d" % pid
90
print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
93
sys.stdout = sys.stderr = Log()
94
print "Starting Yokadi daemon with pid %s" % os.getpid()
96
def sigTermHandler(signal, stack):
97
"""Handler when yokadid receive SIGTERM"""
98
print "Receive SIGTERM. Exiting"
99
print "End of yokadi Daemon"
103
def sigHupHandler(signal, stack):
104
"""Handler when yokadid receive SIGHUP"""
105
print "Receive SIGHUP. Reloading configuration"
111
"""Main event loop"""
112
delta=timedelta(hours=float(Config.byName("ALARM_DELAY").value))
113
suspend=timedelta(hours=float(Config.byName("ALARM_SUSPEND").value))
114
cmdDelayTemplate=Config.byName("ALARM_DELAY_CMD").value
115
cmdDueTemplate=Config.byName("ALARM_DUE_CMD").value
116
# For the two following dict, task id is key, and value is (duedate, triggerdate)
117
triggeredDelayTasks={}
119
activeTaskFilter=[Task.q.status!="done",
120
Task.q.projectID == Project.q.id,
121
Project.q.active == True]
124
now=datetime.today().replace(microsecond=0)
125
delayTasks=Task.select(AND(Task.q.dueDate < now+delta,
126
Task.q.dueDate > now,
128
dueTasks=Task.select(AND(Task.q.dueDate < now,
130
processTasks(delayTasks, triggeredDelayTasks, cmdDelayTemplate, suspend)
131
processTasks(dueTasks, triggeredDueTasks, cmdDueTemplate, suspend)
133
def processTasks(tasks, triggeredTasks, cmdTemplate, suspend):
134
"""Process a list of tasks and trigger action if needed
135
@param tasks: list of tasks
136
@param triggeredTasks: dict of tasks that has been triggered. Dict can be updated
137
@param cmdTemplate: command line template to execute if task trigger
138
@param suspend: timedelta beetween to task trigger"""
141
if triggeredTasks.has_key(task.id) and triggeredTasks[task.id][0]==task.dueDate:
142
# This task with the same dueDate has already been triggered
143
if now-triggeredTasks[task.id][1]<suspend:
144
# Task has been trigger recently, skip to next
146
print "Task %s is due soon" % task.title
147
cmd=cmdTemplate.replace("{ID}", str(task.id))
148
cmd=cmd.replace("{TITLE}", task.title.replace('"', '\"'))
149
cmd=cmd.replace("{DATE}", str(task.dueDate))
150
process=Popen(cmd, shell=True)
152
#TODO: redirect stdout/stderr properly to Log (not so easy...)
153
triggeredTasks[task.id]=(task.dueDate, datetime.now())
155
def killYokadid(dbName):
156
"""Kill Yokadi daemon
157
@param dbName: only kill Yokadid running for this database
160
for line in getoutput("ps -ef|grep python | grep [y]okadid.py ").split("\n"):
161
pid=int(line.split()[1])
165
print "Killing Yokadid with pid %s" % pid
166
os.kill(pid, SIGTERM)
169
#BUG: quite buggy. Killing foo database will also kill foobar.
170
# As we can have space in database path, it is not so easy to parse line...
171
print "Killing Yokadid with database %s and pid %s" % (dbName, pid)
172
os.kill(pid, SIGTERM)
175
parser = OptionParser()
177
parser.add_option("-d", "--db", dest="filename",
178
help="TODO database", metavar="FILE")
180
parser.add_option("-k", "--kill",
181
dest="kill", default=False, action="store_true",
182
help="Kill Yokadi Daemon (you can specify database with -db if you run multiple Yokadid")
184
parser.add_option("-f", "--foreground",
185
dest="foreground", default=False, action="store_true",
186
help="Don't fork background. Usefull for debug")
188
return parser.parse_args()
192
#TODO: check that yokadid is not already running for this database ? Not very harmful...
193
#TODO: change unix process name to "yokadid"
195
# Make the event list global to allow communication with main event loop
198
(options, args) = parseOptions()
201
killYokadid(options.filename)
204
signal(SIGTERM, sigTermHandler)
205
signal(SIGHUP, sigHupHandler)
208
if not options.foreground:
211
if not options.filename:
212
options.filename=os.path.join(os.path.expandvars("$HOME"), ".yokadi.db")
213
print "Using default database (%s)" % options.filename
215
connectDatabase(options.filename, createIfNeeded=False)
218
if not (Task.tableExists() and Config.tableExists()):
219
print "Your database seems broken or not initialised properly. Start yokadi command line tool to do it"
222
# Start the main event Loop
224
while event[1]!="SIGTERM":
227
except KeyboardInterrupt:
230
if __name__ == "__main__":