11# SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries
2+ # SPDX-FileCopyrightText: 2024 Pat Satyshur
23#
34# SPDX-License-Identifier: MIT
45
5859
5960import time
6061import sys
62+ import os
6163from collections import namedtuple
6264
6365try :
66+ # pylint: disable=deprecated-class
6467 from typing import Optional , Hashable
6568 from typing_extensions import Protocol
6669
@@ -214,6 +217,8 @@ class FileHandler(StreamHandler):
214217
215218 def __init__ (self , filename : str , mode : str = "a" ) -> None :
216219 # pylint: disable=consider-using-with
220+ if mode == "r" :
221+ raise ValueError ("Can't write to a read only file" )
217222 super ().__init__ (open (filename , mode = mode ))
218223
219224 def close (self ) -> None :
@@ -229,13 +234,112 @@ def format(self, record: LogRecord) -> str:
229234 return super ().format (record ) + "\r \n "
230235
231236 def emit (self , record : LogRecord ) -> None :
232- """Generate the message and write it to the UART .
237+ """Generate the message and write it to the file .
233238
234239 :param record: The record (message object) to be logged
235240 """
236241 self .stream .write (self .format (record ))
237242
238243
244+ class RotatingFileHandler (FileHandler ):
245+ """File handler for writing log files to flash memory or external memory such as an SD card.
246+ This handler implements a very simple log rotating system similar to the python function of the
247+ same name (https://docs.python.org/3/library/logging.handlers.html#rotatingfilehandler)
248+
249+ If maxBytes is set, the handler will check to see if the log file is larger than the given
250+ limit. If the log file is larger than the limit, it is renamed and a new file is started.
251+ The old log file will be renamed with a numerical appendix '.1', '.2', etc... The variable
252+ backupCount controls how many old log files to keep. For example, if the filename is 'log.txt'
253+ and backupCount is 5, you will end up with six log files: 'log.txt', 'log.txt.1', 'log.txt.3',
254+ up to 'log.txt.5' Therefore, the maximum amount of disk space the logs can use is
255+ maxBytes*(backupCount+1).
256+
257+ If either maxBytes or backupCount is not set, or set to zero, the log rotation is disabled.
258+ This will result in a single log file with a name `filename` that will grow without bound.
259+
260+ :param str filename: The filename of the log file
261+ :param str mode: Whether to write ('w') or append ('a'); default is to append
262+ :param int maxBytes: The max allowable size of the log file in bytes.
263+ :param int backupCount: The number of old log files to keep.
264+ """
265+
266+ def __init__ (
267+ self ,
268+ filename : str ,
269+ mode : str = "a" ,
270+ maxBytes : int = 0 ,
271+ backupCount : int = 0 ,
272+ ) -> None :
273+ if maxBytes < 0 :
274+ raise ValueError ("maxBytes must be a positive number" )
275+ if backupCount < 0 :
276+ raise ValueError ("backupCount must be a positive number" )
277+
278+ self ._LogFileName = filename
279+ self ._WriteMode = mode
280+ self ._maxBytes = maxBytes
281+ self ._backupCount = backupCount
282+
283+ # Open the file and save the handle to self.stream
284+ super ().__init__ (self ._LogFileName , mode = self ._WriteMode )
285+
286+ def doRollover (self ) -> None :
287+ """Roll over the log files. This should not need to be called directly"""
288+ # At this point, we have already determined that we need to roll the log files.
289+ # Close the log file. Probably needed if we want to delete/rename files.
290+ self .close ()
291+
292+ for i in range (self ._backupCount , 0 , - 1 ):
293+ CurrentFileName = self ._LogFileName + "." + str (i )
294+ CurrentFileNamePlus = self ._LogFileName + "." + str (i + 1 )
295+ try :
296+ if i == self ._backupCount :
297+ # This is the oldest log file. Delete this one.
298+ os .remove (CurrentFileName )
299+ else :
300+ # Rename the current file to the next number in the sequence.
301+ os .rename (CurrentFileName , CurrentFileNamePlus )
302+ except OSError as e :
303+ if e .args [0 ] == 2 :
304+ # File does not exsist. This is okay.
305+ pass
306+ else :
307+ raise e
308+
309+ # Rename the current log to the first backup
310+ os .rename (self ._LogFileName , CurrentFileName )
311+
312+ # Reopen the file.
313+ # pylint: disable=consider-using-with
314+ self .stream = open (self ._LogFileName , mode = self ._WriteMode )
315+
316+ def GetLogSize (self ) -> int :
317+ """Check the size of the log file."""
318+ try :
319+ self .stream .flush () # We need to call this or the file size is always zero.
320+ LogFileSize = os .stat (self ._LogFileName )[6 ]
321+ except OSError as e :
322+ if e .args [0 ] == 2 :
323+ # Log file does not exsist. This is okay.
324+ LogFileSize = None
325+ else :
326+ raise e
327+ return LogFileSize
328+
329+ def emit (self , record : LogRecord ) -> None :
330+ """Generate the message and write it to the file.
331+
332+ :param record: The record (message object) to be logged
333+ """
334+ if (
335+ (self .GetLogSize () >= self ._maxBytes )
336+ and (self ._maxBytes > 0 )
337+ and (self ._backupCount > 0 )
338+ ):
339+ self .doRollover ()
340+ self .stream .write (self .format (record ))
341+
342+
239343class NullHandler (Handler ):
240344 """Provide an empty log handler.
241345
0 commit comments