]> git.phdru.name Git - xsetbg.git/blob - xsetbg.py
phd.pp.ru => phdru.name.
[xsetbg.git] / xsetbg.py
1 #! /usr/bin/env python
2 """Set a random background image (XWin)
3
4 Select a random image from a (list of) directory(s)
5 and set it as the desktop wallpaper (display it in the root window)
6 using xli or xsetbg programs.
7
8 """
9
10 __version__ = "$Revision$"[11:-2]
11 __revision__ = "$Id$"[5:-2]
12 __date__ = "$Date$"[7:-2]
13
14 __author__ = "Oleg Broytman <phd@phdru.name>"
15 __copyright__ = "Copyright (C) 2000-2010 PhiloSoft Design"
16 __license__ = "GNU GPL"
17
18
19 import sys, os
20 from ConfigParser import SafeConfigParser
21
22 import anydbm
23 import random
24 import shelve
25 import subprocess
26 from time import time
27 from fcntl import flock, LOCK_EX, LOCK_UN, LOCK_NB
28
29 from wsgiref import simple_server
30 from wsgiref.handlers import SimpleHandler
31 from wsgiref.simple_server import WSGIServer, make_server
32 simple_server.ServerHandler = SimpleHandler # Stop logging to stdout
33
34
35 def error(error_str, error_code=1):
36    sys.stderr.write("%s: Error: %s\n" % (sys.argv[0], error_str))
37    sys.exit(error_code)
38
39
40 xsetbg_dir = os.path.dirname(os.path.abspath(__file__))
41 os.chdir(xsetbg_dir)
42
43 config = SafeConfigParser()
44 config.read("xsetbg.conf")
45
46 if config.has_option("images", "directory") or \
47    config.has_option("images", "directory0") or \
48    config.has_option("images", "directory1"):
49    image_dirs = []
50    if config.has_option("images", "directory"):
51       image_dirs.append(config.get("images", "directory"))
52    if config.has_option("images", "directory0"):
53       image_dirs.append(config.get("images", "directory0"))
54    if config.has_option("images", "directory1"):
55       image_dirs.append(config.get("images", "directory1"))
56    i = 2
57    while True:
58       option = "directory%d" % i
59       if config.has_option("images", option):
60          image_dirs.append(config.get("images", option))
61          i += 1
62       else:
63          break
64 else:
65    image_dirs = ["images"]
66
67 image_dirs = [os.path.join(xsetbg_dir,
68    os.path.expandvars(os.path.expanduser(dirname)))
69       for dirname in image_dirs
70 ]
71
72 # minimum time in seconds between background image changes
73 if config.has_option("xsetbg", "min_pause"):
74    min_pause = config.getint("xsetbg", "min_pause")
75 else:
76    min_pause = 60
77
78 borders = config.get("xsetbg", "borders").split(',')
79 if config.has_option("xsetbg", "borders"):
80    borders = [border.strip() for border in config.get("xsetbg", "borders").split(',')]
81 else:
82    borders = ["darkcyan", "steelblue", "midnightblue"]
83
84 # minimum time in seconds between occurences of the same image
85 if config.has_option("xsetbg", "min_delay"):
86    min_delay = config.getint("xsetbg", "min_delay")
87 else:
88    min_delay = 3600*24 # 24 hours
89
90 # httpd settings
91 if config.has_option("httpd", "host"):
92    host = config.get("httpd", "host")
93 else:
94    host = 'localhost'
95
96 if config.has_option("httpd", "port"):
97    port = config.getint("httpd", "port")
98 else:
99    error("Config must specify a port to listen. Abort.")
100
101 del config
102
103
104 os.umask(0066) # octal; -rw-------; make the global persistent dictionary
105                # readable only by the user
106 global_db_name = "xsetbg.db"
107
108
109 # DB keys
110 timestamp_key = "timestamp"
111 filename_key = "filename"
112 old_filename_key = "old_filename"
113
114
115 # Create the database if it is not exists yet.
116
117 try:
118    global_db = shelve.open(global_db_name, flag='c')
119 except anydbm.error, msg:
120    if str(msg) == "db type could not be determined":
121       os.remove(global_db_name)
122       global_db = shelve.open(global_db_name, flag='c')
123
124 # Remove old filenames
125 old_time = time() - min_delay
126
127 to_delete = [timestamp_key]
128 for key in global_db.keys():
129    if key.startswith('/') and global_db[key] < old_time:
130       to_delete.append(key)
131
132 for key in to_delete:
133    try:
134       del global_db[key]
135    except KeyError:
136       pass
137
138 global_db.close() # Close DB in the parent process
139
140
141 images = []
142
143 for image_dir in image_dirs:
144    # List images in all subdirectories
145    for dirpath, dirs, files in os.walk(image_dir):
146       images.extend([os.path.join(dirpath, file) for file in files])
147
148 if not images:
149    error("No images found. Abort.")
150
151
152 def published(func):
153     func._wsgi_published = True
154     return func
155
156 @published
157 def ping(force=False):
158    # Use the program's file as the lock file:
159    # lock it to prevent two processes run in parallel.
160    lock_file = open("xsetbg.py", 'r')
161    try:
162       flock(lock_file, LOCK_EX|LOCK_NB)
163    except IOError: # already locked
164       lock_file.close()
165       return
166
167
168    global_db = None
169    try:
170       # Reopen the global persistent dictionary
171       global_db = shelve.open(global_db_name, 'w')
172
173       timestamp = global_db.get(timestamp_key)
174       current_time = time()
175
176       if not force and timestamp is not None and \
177          current_time - timestamp < min_pause: # Too early to change background
178             return
179
180       # Save current time
181       global_db[timestamp_key] = current_time
182
183       # Select a random image and check if we've seen it recently;
184       # loop until we can find a new image (never seen before) or old enough.
185       for i in xrange(len(images)): # ensure the loop is not infinite
186          image_name = random.choice(images)
187          if global_db.has_key(image_name):
188             image_time = global_db[image_name]
189             if current_time - image_time > min_delay:
190                break
191          else:
192             break
193       global_db[image_name] = current_time
194
195       # Save filename
196       if global_db.has_key(filename_key):
197          global_db[old_filename_key] = global_db[filename_key]
198       global_db[filename_key] = image_name
199
200       program_options = ["xli", "-onroot", "-quiet"] + \
201          ["-center", "-border", random.choice(borders), "-zoom", "auto",
202             image_name]
203
204       rc = subprocess.call(program_options)
205       if rc:
206          error("cannot execute xli!")
207
208    finally:
209       # Unlock and close the lock file
210       flock(lock_file, LOCK_UN)
211       lock_file.close()
212       # Flush and close the global persistent dictionary
213       if global_db: global_db.close()
214
215 @published
216 def force():
217     ping(force=True)
218
219 @published
220 def stop():
221     QuitWSGIServer._quit_flag = True
222
223
224 g = globals().copy()
225 commands = dict([(name, g[name]) for name in g
226     if getattr(g[name], '_wsgi_published', False)])
227 del g
228
229 class QuitWSGIServer(WSGIServer):
230     _quit_flag = False
231
232     def serve_forever(self):
233         while not self._quit_flag:
234             self.handle_request()
235
236 def app(env, start_response):
237     command = env['PATH_INFO'][1:] # Remove the leading slash
238     if command not in commands:
239         status = '404 Not found'
240         response_headers = [('Content-type', 'text/plain')]
241         start_response(status, response_headers)
242         return ['The command was not found.\n']
243
244     try:
245         commands[command]()
246     except:
247         status = '500 Error'
248         response_headers = [('Content-type', 'text/plain')]
249         start_response(status, response_headers)
250         return ['Error occured!\n']
251
252     status = '200 OK'
253     response_headers = [('Content-type', 'text/plain')]
254     start_response(status, response_headers)
255     return ['Ok\n']
256
257 force()
258 httpd = make_server(host, port, app, server_class=QuitWSGIServer)
259 httpd.serve_forever()