~ubuntu-branches/ubuntu/maverick/yokadi/maverick

« back to all changes in this revision

Viewing changes to src/yokadi/yokadid.py

  • Committer: Bazaar Package Importer
  • Author(s): Harald Sitter
  • Date: 2009-07-19 13:01:35 UTC
  • Revision ID: james.westby@ubuntu.com-20090719130135-eonczddb1s21ux1v
Tags: upstream-0.10.0
Import upstream version 0.10.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# -*- coding: UTF-8 -*-
 
3
"""
 
4
Yokadi daemon. Used to monitor due tasks and warn user.
 
5
 
 
6
@author: Sébastien Renard <sebastien.renard@digitalfox.org>
 
7
@license: GPLv3
 
8
"""
 
9
 
 
10
import sys
 
11
import os
 
12
import time
 
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
 
18
 
 
19
from sqlobject import AND
 
20
 
 
21
import tui
 
22
 
 
23
# Force default encoding to prefered encoding
 
24
reload(sys)
 
25
sys.setdefaultencoding(tui.ENCODING)
 
26
 
 
27
 
 
28
from db import Config, Project, Task, connectDatabase
 
29
 
 
30
try:
 
31
    from syslog import openlog, syslog, LOG_USER
 
32
    SYSLOG=True
 
33
except ImportError:
 
34
    SYSLOG=False
 
35
 
 
36
# Daemon polling delay (in seconds)
 
37
DELAY=5
 
38
 
 
39
# Event sender to main loop
 
40
event=[True, ""]
 
41
    
 
42
class Log:
 
43
    """Send ouput to syslog if available, else defaulting to /tmp/yokadid.log"""
 
44
    def __init__(self):
 
45
        self.logfile=None
 
46
        if SYSLOG:
 
47
            openlog("yokadi", 0, LOG_USER)
 
48
            syslog("init")
 
49
        else:
 
50
            try:
 
51
                self.logfile=open("/tmp/yokadid-%s.log" % os.getpid(), "w+")
 
52
            except:
 
53
                self.logfile=None
 
54
 
 
55
    def write(self, output):
 
56
        if SYSLOG:
 
57
            if output=="\n": return
 
58
            syslog(output)
 
59
        elif self.logfile:
 
60
            self.logfile.write(output)
 
61
        else:
 
62
            sys.stdout(output)
 
63
            sys.stdout.flush()
 
64
 
 
65
def doubleFork():
 
66
    # Python unix daemon trick from the Activestate recipe 66012
 
67
    try:
 
68
        pid = os.fork()
 
69
        if pid > 0:
 
70
            # exit first parent
 
71
            sys.exit(0)
 
72
    except OSError, e:
 
73
        print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
 
74
        sys.exit(1)
 
75
 
 
76
    # decouple from parent environment
 
77
    os.chdir("/")   #don't prevent unmounting
 
78
    os.setsid()
 
79
    os.umask(0)
 
80
 
 
81
    # do second fork
 
82
    try:
 
83
        pid = os.fork()
 
84
        if pid > 0:
 
85
            # exit from second parent, print eventual PID before
 
86
            print "Forking background with PID %d" % pid
 
87
            sys.stdout.flush()
 
88
            sys.exit(0)
 
89
    except OSError, e:
 
90
        print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
 
91
        sys.exit(1)
 
92
    
 
93
    sys.stdout = sys.stderr = Log()
 
94
    print "Starting Yokadi daemon with pid %s" % os.getpid()
 
95
 
 
96
def sigTermHandler(signal, stack):
 
97
    """Handler when yokadid receive SIGTERM"""
 
98
    print "Receive SIGTERM. Exiting"
 
99
    print "End of yokadi Daemon"
 
100
    event[0]=False
 
101
    event[1]="SIGTERM"
 
102
 
 
103
def sigHupHandler(signal, stack):
 
104
    """Handler when yokadid receive SIGHUP"""
 
105
    print "Receive SIGHUP. Reloading configuration"
 
106
    event[0]=False
 
107
    event[1]="SIGHUP"
 
108
 
 
109
 
 
110
def eventLoop():
 
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={}
 
118
    triggeredDueTasks={}
 
119
    activeTaskFilter=[Task.q.status!="done",
 
120
                      Task.q.projectID == Project.q.id,
 
121
                      Project.q.active == True]
 
122
    while event[0]:
 
123
        time.sleep(DELAY)
 
124
        now=datetime.today().replace(microsecond=0)
 
125
        delayTasks=Task.select(AND(Task.q.dueDate < now+delta,
 
126
                                   Task.q.dueDate > now,
 
127
                                   *activeTaskFilter))
 
128
        dueTasks=Task.select(AND(Task.q.dueDate < now,
 
129
                                 *activeTaskFilter))
 
130
        processTasks(delayTasks, triggeredDelayTasks, cmdDelayTemplate, suspend)
 
131
        processTasks(dueTasks, triggeredDueTasks, cmdDueTemplate, suspend)
 
132
 
 
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"""
 
139
    now=datetime.now()
 
140
    for task in tasks:
 
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
 
145
                continue
 
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)
 
151
        process.wait()
 
152
        #TODO: redirect stdout/stderr properly to Log (not so easy...)
 
153
        triggeredTasks[task.id]=(task.dueDate, datetime.now())
 
154
 
 
155
def killYokadid(dbName):
 
156
    """Kill Yokadi daemon
 
157
    @param dbName: only kill Yokadid running for this database
 
158
    """
 
159
    selfpid=os.getpid()
 
160
    for line in getoutput("ps -ef|grep python | grep [y]okadid.py ").split("\n"):
 
161
        pid=int(line.split()[1])
 
162
        if pid==selfpid:
 
163
            continue
 
164
        if dbName is None:
 
165
            print "Killing Yokadid with pid %s" % pid
 
166
            os.kill(pid, SIGTERM)
 
167
        else:
 
168
            if dbName in line:
 
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)
 
173
 
 
174
def parseOptions():
 
175
    parser = OptionParser()
 
176
    
 
177
    parser.add_option("-d", "--db", dest="filename",
 
178
                      help="TODO database", metavar="FILE")
 
179
 
 
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")
 
183
 
 
184
    parser.add_option("-f", "--foreground",
 
185
                      dest="foreground", default=False, action="store_true", 
 
186
                      help="Don't fork background. Usefull for debug")
 
187
 
 
188
    return parser.parse_args()
 
189
 
 
190
 
 
191
def main():
 
192
    #TODO: check that yokadid is not already running for this database ? Not very harmful...
 
193
    #TODO: change unix process name to "yokadid"
 
194
 
 
195
    # Make the event list global to allow communication with main event loop
 
196
    global event
 
197
 
 
198
    (options, args) = parseOptions()
 
199
 
 
200
    if options.kill:
 
201
        killYokadid(options.filename)
 
202
        sys.exit(0)
 
203
 
 
204
    signal(SIGTERM, sigTermHandler)
 
205
    signal(SIGHUP, sigHupHandler)
 
206
 
 
207
 
 
208
    if not options.foreground:
 
209
        doubleFork()
 
210
 
 
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
 
214
 
 
215
    connectDatabase(options.filename, createIfNeeded=False)
 
216
    
 
217
    # Basic tests :
 
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"
 
220
        sys.exit(1)
 
221
 
 
222
    # Start the main event Loop
 
223
    try:
 
224
        while event[1]!="SIGTERM":
 
225
            eventLoop()
 
226
            event[0]=True
 
227
    except KeyboardInterrupt:
 
228
        print "\nExiting..."
 
229
 
 
230
if __name__ == "__main__":
 
231
    main()