blob: b9331bb8e7694191708bca3ec87afb1790c03082 [file] [log] [blame]
Joe Gregorio0eec3082011-03-07 08:25:17 -05001#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2010 Google Inc. All Rights Reserved.
5# Portions copyright PSF License
6# http://code.activestate.com/recipes/278731-creating-a-daemon-the-python-way/
7
8"""A pm-action hook for setting timezone.
9
10Uses the Google Latitude API and the geonames.org
11API to find your cellphones latitude and longitude
12and from the determine the timezone you are in,
13and then sets the computer's timezone to that.
14"""
15
16__author__ = 'jcgregorio@google.com (Joe Gregorio)'
17
18
19from apiclient.discovery import build
20
21import httplib2
22import os
23import pickle
24import pprint
25import subprocess
26import sys
27import time
28import uritemplate
29
30from apiclient.anyjson import simplejson
31from apiclient.discovery import build
32from apiclient.oauth import FlowThreeLegged
33from apiclient.ext.authtools import run
34from apiclient.ext.file import Storage
35
36# Uncomment to get detailed logging
37# httplib2.debuglevel = 4
38
39# URI Template to convert latitude and longitude into a timezone
40GEONAMES = 'http://api.geonames.org/timezoneJSON?lat={lat}&lng={long}&username=jcgregorio'
41PID_FILE = '/var/lock/tznever.pid'
42CACHE = '/var/local/tznever/.cache'
43
44# Default daemon parameters.
45# File mode creation mask of the daemon.
46UMASK = 0
47
48# Default working directory for the daemon.
49WORKDIR = "/"
50
51# Default maximum for the number of available file descriptors.
52MAXFD = 1024
53
54# The standard I/O file descriptors are redirected to /dev/null by default.
55if (hasattr(os, "devnull")):
56 REDIRECT_TO = os.devnull
57else:
58 REDIRECT_TO = "/dev/null"
59
60
61def main():
62 storage = Storage('/var/local/tznever/latitude_credentials.dat')
63 credentials = storage.get()
64 if len(sys.argv) == 1:
65 if credentials is None or credentials.invalid == True:
66 auth_discovery = build('latitude', 'v1').auth_discovery()
67 flow = FlowThreeLegged(auth_discovery,
68 consumer_key='m-buzz.appspot.com',
69 consumer_secret='NQEHb4eU6GkjjFGe1MD5W6IC',
70 user_agent='tz-never/1.0',
71 domain='m-buzz.appspot.com',
72 scope='https://www.googleapis.com/auth/latitude',
73 xoauth_displayname='TZ Never Again',
74 location='current',
75 granularity='city'
76 )
77
78 credentials = run(flow, storage)
79 else:
80 print "You are already authorized"
81 else:
82 if credentials is None or credentials.invalid == True:
83 print "This app, tznever, is not authorized. Run from the command-line to re-authorize."
84 os.exit(1)
85
86 if len(sys.argv) > 1 and sys.argv[1] in ['hibernate', 'suspend']:
87 print "Hibernating"
88 # Kill off the possibly still running process by its pid
89 if os.path.isfile(PID_FILE):
90 f = file(PID_FILE, 'r')
91 pid = f.read()
92 f.close()
93 cmdline = ['/bin/kill', '-2', pid]
94 subprocess.Popen(cmdline)
95 os.unlink(PID_FILE)
96 elif len(sys.argv) > 1 and sys.argv[1] in ['thaw', 'resume']:
97 print "Resuming"
98 # write our pid out
99 f = file(PID_FILE, 'w')
100 f.write(str(os.getpid()))
101 f.close()
102
103 success = False
104 first_time = True
105 while not success:
106 try:
107 if not first_time:
108 time.sleep(5)
109 else:
110 first_time = False
111 print "Daemonizing so as not to gum up the works."
112 createDaemon()
113 # rewrite the PID file with our new PID
114 f = file(PID_FILE, 'w')
115 f.write(str(os.getpid()))
116 f.close()
117 http = httplib2.Http(CACHE)
118 http = credentials.authorize(http)
119
120 service = build('latitude', 'v1', http=http)
121
122 location = service.currentLocation().get(granularity='city').execute()
123 position = {
124 'lat': str(location['latitude']),
125 'long': str(location['longitude'])
126 }
127 http2 = httplib2.Http(CACHE)
128 resp, content = http2.request(uritemplate.expand(GEONAMES, position))
129 geodata = simplejson.loads(content)
130 tz = geodata['timezoneId']
131 f = file('/etc/timezone', 'w')
132 f.write(tz)
133 f.close()
134 cmdline = 'dpkg-reconfigure -f noninteractive tzdata'.split(' ')
135 subprocess.Popen(cmdline)
136 success = True
137 except httplib2.ServerNotFoundError, e:
138 print "still not connected, sleeping"
139 except KeyboardInterrupt, e:
140 if os.path.isfile(PID_FILE):
141 os.unlink(PID_FILE)
142 success = True
143 # clean up pid file
144 if os.path.isfile(PID_FILE):
145 os.unlink(PID_FILE)
146
147
148def createDaemon():
149 """Detach a process from the controlling terminal and run it in the
150 background as a daemon.
151 """
152
153 try:
154 # Fork a child process so the parent can exit. This returns control to
155 # the command-line or shell. It also guarantees that the child will not
156 # be a process group leader, since the child receives a new process ID
157 # and inherits the parent's process group ID. This step is required
158 # to insure that the next call to os.setsid is successful.
159 pid = os.fork()
160 except OSError, e:
161 raise Exception, "%s [%d]" % (e.strerror, e.errno)
162
163 if (pid == 0): # The first child.
164 # To become the session leader of this new session and the process group
165 # leader of the new process group, we call os.setsid(). The process is
166 # also guaranteed not to have a controlling terminal.
167 os.setsid()
168
169 # Is ignoring SIGHUP necessary?
170 #
171 # It's often suggested that the SIGHUP signal should be ignored before
172 # the second fork to avoid premature termination of the process. The
173 # reason is that when the first child terminates, all processes, e.g.
174 # the second child, in the orphaned group will be sent a SIGHUP.
175 #
176 # "However, as part of the session management system, there are exactly
177 # two cases where SIGHUP is sent on the death of a process:
178 #
179 # 1) When the process that dies is the session leader of a session that
180 # is attached to a terminal device, SIGHUP is sent to all processes
181 # in the foreground process group of that terminal device.
182 # 2) When the death of a process causes a process group to become
183 # orphaned, and one or more processes in the orphaned group are
184 # stopped, then SIGHUP and SIGCONT are sent to all members of the
185 # orphaned group." [2]
186 #
187 # The first case can be ignored since the child is guaranteed not to have
188 # a controlling terminal. The second case isn't so easy to dismiss.
189 # The process group is orphaned when the first child terminates and
190 # POSIX.1 requires that every STOPPED process in an orphaned process
191 # group be sent a SIGHUP signal followed by a SIGCONT signal. Since the
192 # second child is not STOPPED though, we can safely forego ignoring the
193 # SIGHUP signal. In any case, there are no ill-effects if it is ignored.
194 #
195 # import signal # Set handlers for asynchronous events.
196 # signal.signal(signal.SIGHUP, signal.SIG_IGN)
197
198 try:
199 # Fork a second child and exit immediately to prevent zombies. This
200 # causes the second child process to be orphaned, making the init
201 # process responsible for its cleanup. And, since the first child is
202 # a session leader without a controlling terminal, it's possible for
203 # it to acquire one by opening a terminal in the future (System V-
204 # based systems). This second fork guarantees that the child is no
205 # longer a session leader, preventing the daemon from ever acquiring
206 # a controlling terminal.
207 pid = os.fork() # Fork a second child.
208 except OSError, e:
209 raise Exception, "%s [%d]" % (e.strerror, e.errno)
210
211 if (pid == 0): # The second child.
212 # Since the current working directory may be a mounted filesystem, we
213 # avoid the issue of not being able to unmount the filesystem at
214 # shutdown time by changing it to the root directory.
215 os.chdir(WORKDIR)
216 # We probably don't want the file mode creation mask inherited from
217 # the parent, so we give the child complete control over permissions.
218 os.umask(UMASK)
219 else:
220 # exit() or _exit()? See below.
221 os._exit(0) # Exit parent (the first child) of the second child.
222 else:
223 # exit() or _exit()?
224 # _exit is like exit(), but it doesn't call any functions registered
225 # with atexit (and on_exit) or any registered signal handlers. It also
226 # closes any open file descriptors. Using exit() may cause all stdio
227 # streams to be flushed twice and any temporary files may be unexpectedly
228 # removed. It's therefore recommended that child branches of a fork()
229 # and the parent branch(es) of a daemon use _exit().
230 os._exit(0) # Exit parent of the first child.
231
232 # Close all open file descriptors. This prevents the child from keeping
233 # open any file descriptors inherited from the parent. There is a variety
234 # of methods to accomplish this task. Three are listed below.
235 #
236 # Try the system configuration variable, SC_OPEN_MAX, to obtain the maximum
237 # number of open file descriptors to close. If it doesn't exists, use
238 # the default value (configurable).
239 #
240 # try:
241 # maxfd = os.sysconf("SC_OPEN_MAX")
242 # except (AttributeError, ValueError):
243 # maxfd = MAXFD
244 #
245 # OR
246 #
247 # if (os.sysconf_names.has_key("SC_OPEN_MAX")):
248 # maxfd = os.sysconf("SC_OPEN_MAX")
249 # else:
250 # maxfd = MAXFD
251 #
252 # OR
253 #
254 # Use the getrlimit method to retrieve the maximum file descriptor number
255 # that can be opened by this process. If there is not limit on the
256 # resource, use the default value.
257 #
258 import resource # Resource usage information.
259 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
260 if (maxfd == resource.RLIM_INFINITY):
261 maxfd = MAXFD
262
263 # Iterate through and close all file descriptors.
264 for fd in range(0, maxfd):
265 try:
266 os.close(fd)
267 except OSError: # ERROR, fd wasn't open to begin with (ignored)
268 pass
269
270 # Redirect the standard I/O file descriptors to the specified file. Since
271 # the daemon has no controlling terminal, most daemons redirect stdin,
272 # stdout, and stderr to /dev/null. This is done to prevent side-effects
273 # from reads and writes to the standard I/O file descriptors.
274
275 # This call to open is guaranteed to return the lowest file descriptor,
276 # which will be 0 (stdin), since it was closed above.
277 os.open(REDIRECT_TO, os.O_RDWR) # standard input (0)
278
279 # Duplicate standard input to standard output and standard error.
280 os.dup2(0, 1) # standard output (1)
281 os.dup2(0, 2) # standard error (2)
282
283 return(0)
284
285if __name__ == '__main__':
286 main()
287
288
289