root / trunk / twisted / python / logfile.py

Revision 20199, 9.2 kB (checked in by therve, 2 years ago)

Merge logfile-defaultmode-2586

Author: therve
Reviewer: ralphm, exarkun
Fixes #2586

Add documentation and tests to the defaultMode arguments of LogFile?,
and fix a potential security problem when using custom mode.

Line 
1 # -*- test-case-name: twisted.test.test_logfile -*-
2
3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories.
4 # See LICENSE for details.
5
6 """
7 A rotating, browsable log file.
8 """
9
10 # System Imports
11 import os, glob, time, stat
12
13 from twisted.python import threadable
14
15 class BaseLogFile:
16     """
17     The base class for a log file that can be rotated.
18     """
19
20     synchronized = ["write", "rotate"]
21
22     def __init__(self, name, directory, defaultMode=None):
23         """
24         Create a log file.
25
26         @param name: name of the file
27         @param directory: directory holding the file
28         @param defaultMode: permissions used to create the file. Default to
29         current permissions of the file if the file exists.
30         """
31         self.directory = directory
32         assert os.path.isdir(self.directory)
33         self.name = name
34         self.path = os.path.join(directory, name)
35         if defaultMode is None and os.path.exists(self.path):
36             self.defaultMode = stat.S_IMODE(os.stat(self.path)[stat.ST_MODE])
37         else:
38             self.defaultMode = defaultMode
39         self._openFile()
40
41     def fromFullPath(cls, filename, *args, **kwargs):
42         """
43         Construct a log file from a full file path.
44         """
45         logPath = os.path.abspath(filename)
46         return cls(os.path.basename(logPath),
47                    os.path.dirname(logPath), *args, **kwargs)
48     fromFullPath = classmethod(fromFullPath)
49
50     def shouldRotate(self):
51         """
52         Override with a method to that returns true if the log
53         should be rotated.
54         """
55         raise NotImplementedError
56
57     def _openFile(self):
58         """
59         Open the log file.
60         """
61         self.closed = False
62         if os.path.exists(self.path):
63             self._file = file(self.path, "r+", 1)
64             self._file.seek(0, 2)
65         else:
66             if self.defaultMode is not None:
67                 # Set the lowest permissions
68                 oldUmask = os.umask(0777)
69                 try:
70                     self._file = file(self.path, "w+", 1)
71                 finally:
72                     os.umask(oldUmask)
73             else:
74                 self._file = file(self.path, "w+", 1)
75         if self.defaultMode is not None:
76             try:
77                 os.chmod(self.path, self.defaultMode)
78             except OSError:
79                 # Probably /dev/null or something?
80                 pass
81
82     def __getstate__(self):
83         state = self.__dict__.copy()
84         del state["_file"]
85         return state
86
87     def __setstate__(self, state):
88         self.__dict__ = state
89         self._openFile()
90
91     def write(self, data):
92         """
93         Write some data to the file.
94         """
95         if self.shouldRotate():
96             self.flush()
97             self.rotate()
98         self._file.write(data)
99
100     def flush(self):
101         """
102         Flush the file.
103         """
104         self._file.flush()
105
106     def close(self):
107         """
108         Close the file.
109
110         The file cannot be used once it has been closed.
111         """
112         self.closed = True
113         self._file.close()
114         self._file = None
115
116     def getCurrentLog(self):
117         """
118         Return a LogReader for the current log file.
119         """
120         return LogReader(self.path)
121
122
123 class LogFile(BaseLogFile):
124     """
125     A log file that can be rotated.
126
127     A rotateLength of None disables automatic log rotation.
128     """
129     def __init__(self, name, directory, rotateLength=1000000, defaultMode=None,
130                  maxRotatedFiles=None):
131         """
132         Create a log file rotating on length.
133
134         @param name: file name.
135         @type name: C{str}
136         @param directory: path of the log file.
137         @type directory: C{str}
138         @param rotateLength: size of the log file where it rotates. Default to
139             1M.
140         @type rotateLength: C{int}
141         @param defaultMode: mode used to create the file.
142         @type defaultMode: C{int}
143         @param maxRotatedFiles: if not None, max number of log files the class
144             creates. Warning: it removes all log files above this number.
145         @type maxRotatedFiles: C{int}
146         """
147         BaseLogFile.__init__(self, name, directory, defaultMode)
148         self.rotateLength = rotateLength
149         self.maxRotatedFiles = maxRotatedFiles
150
151     def _openFile(self):
152         BaseLogFile._openFile(self)
153         self.size = self._file.tell()
154
155     def shouldRotate(self):
156         """
157         Rotate when the log file size is larger than rotateLength.
158         """
159         return self.rotateLength and self.size >= self.rotateLength
160
161     def getLog(self, identifier):
162         """
163         Given an integer, return a LogReader for an old log file.
164         """
165         filename = "%s.%d" % (self.path, identifier)
166         if not os.path.exists(filename):
167             raise ValueError, "no such logfile exists"
168         return LogReader(filename)
169
170     def write(self, data):
171         """
172         Write some data to the file.
173         """
174         BaseLogFile.write(self, data)
175         self.size += len(data)
176
177     def rotate(self):
178         """
179         Rotate the file and create a new one.
180
181         If it's not possible to open new logfile, this will fail silently,
182         and continue logging to old logfile.
183         """
184         if not (os.access(self.directory, os.W_OK) and os.access(self.path, os.W_OK)):
185             return
186         logs = self.listLogs()
187         logs.reverse()
188         for i in logs:
189             if self.maxRotatedFiles is not None and i >= self.maxRotatedFiles:
190                 os.remove("%s.%d" % (self.path, i))
191             else:
192                 os.rename("%s.%d" % (self.path, i), "%s.%d" % (self.path, i + 1))
193         self._file.close()
194         os.rename(self.path, "%s.1" % self.path)
195         self._openFile()
196
197     def listLogs(self):
198         """
199         Return sorted list of integers - the old logs' identifiers.
200         """
201         result = []
202         for name in glob.glob("%s.*" % self.path):
203             try:
204                 counter = int(name.split('.')[-1])
205                 if counter:
206                     result.append(counter)
207             except ValueError:
208                 pass
209         result.sort()
210         return result
211
212     def __getstate__(self):
213         state = BaseLogFile.__getstate__(self)
214         del state["size"]
215         return state
216
217 threadable.synchronize(LogFile)
218
219
220 class DailyLogFile(BaseLogFile):
221     """A log file that is rotated daily (at or after midnight localtime)
222     """
223     def _openFile(self):
224         BaseLogFile._openFile(self)
225         self.lastDate = self.toDate(os.stat(self.path)[8])
226
227     def shouldRotate(self):
228         """Rotate when the date has changed since last write"""
229         return self.toDate() > self.lastDate
230
231     def toDate(self, *args):
232         """Convert a unixtime to (year, month, day) localtime tuple,
233         or return the current (year, month, day) localtime tuple.
234
235         This function primarily exists so you may overload it with
236         gmtime, or some cruft to make unit testing possible.
237         """
238         # primarily so this can be unit tested easily
239         return time.localtime(*args)[:3]
240
241     def suffix(self, tupledate):
242         """Return the suffix given a (year, month, day) tuple or unixtime"""
243         try:
244             return '_'.join(map(str, tupledate))
245         except:
246             # try taking a float unixtime
247             return '_'.join(map(str, self.toDate(tupledate)))
248
249     def getLog(self, identifier):
250         """Given a unix time, return a LogReader for an old log file."""
251         if self.toDate(identifier) == self.lastDate:
252             return self.getCurrentLog()
253         filename = "%s.%s" % (self.path, self.suffix(identifier))
254         if not os.path.exists(filename):
255             raise ValueError, "no such logfile exists"
256         return LogReader(filename)
257
258     def write(self, data):
259         """Write some data to the log file"""
260         BaseLogFile.write(self, data)
261         # Guard against a corner case where time.time()
262         # could potentially run backwards to yesterday.
263         # Primarily due to network time.
264         self.lastDate = max(self.lastDate, self.toDate())
265
266     def rotate(self):
267         """Rotate the file and create a new one.
268
269         If it's not possible to open new logfile, this will fail silently,
270         and continue logging to old logfile.
271         """
272         if not (os.access(self.directory, os.W_OK) and os.access(self.path, os.W_OK)):
273             return
274         newpath = "%s.%s" % (self.path, self.suffix(self.lastDate))
275         if os.path.exists(newpath):
276             return
277         self._file.close()
278         os.rename(self.path, newpath)
279         self._openFile()
280
281     def __getstate__(self):
282         state = BaseLogFile.__getstate__(self)
283         del state["lastDate"]
284         return state
285
286 threadable.synchronize(DailyLogFile)
287
288
289 class LogReader:
290     """Read from a log file."""
291
292     def __init__(self, name):
293         self._file = file(name, "r")
294
295     def readLines(self, lines=10):
296         """Read a list of lines from the log file.
297
298         This doesn't returns all of the files lines - call it multiple times.
299         """
300         result = []
301         for i in range(lines):
302             line = self._file.readline()
303             if not line:
304                 break
305             result.append(line)
306         return result
307
308     def close(self):
309         self._file.close()
Note: See TracBrowser for help on using the browser.