# frozen_string_literal: truerequire_relative'period'classLogger# Device used for logging messages.classLogDeviceincludePeriodattr_reader:devattr_reader:filenameincludeMonitorMixindefinitialize(log=nil,shift_age: nil,shift_size: nil,shift_period_suffix: nil,binmode: false,reraise_write_errors: [],skip_header: false)@dev=@filename=@shift_age=@shift_size=@shift_period_suffix=nil@binmode=binmode@reraise_write_errors=reraise_write_errors@skip_header=skip_headermon_initializeset_dev(log)set_file(shift_age,shift_size,shift_period_suffix)if@filenameenddefwrite(message)handle_write_errors("writing")dosynchronizedoif@shift_ageand@dev.respond_to?(:stat)handle_write_errors("shifting"){check_shift_log}endhandle_write_errors("writing"){@dev.write(message)}endendenddefclosebeginsynchronizedo@dev.closerescuenilendrescueException@dev.closerescuenilendenddefreopen(log=nil,shift_age: nil,shift_size: nil,shift_period_suffix: nil,binmode: nil)# reopen the same filename if no argument, do nothing for IOlog||=@filenameif@filename@binmode=binmodeunlessbinmode.nil?iflogsynchronizedoif@filenameand@dev@dev.closerescuenil# close only file opened by Logger@filename=nilendset_dev(log)set_file(shift_age,shift_size,shift_period_suffix)if@filenameendendselfendprivate# :stopdoc:MODE=File::WRONLY|File::APPEND# TruffleRuby < 24.2 does not have File::SHARE_DELETEifFile.const_defined?:SHARE_DELETEMODE_TO_OPEN=MODE|File::SHARE_DELETE|File::BINARYelseMODE_TO_OPEN=MODE|File::BINARYendMODE_TO_CREATE=MODE_TO_OPEN|File::CREAT|File::EXCLdefset_dev(log)iflog.respond_to?(:write)andlog.respond_to?(:close)@dev=logiflog.respond_to?(:path)andpath=log.pathifFile.exist?(path)@filename=pathendendelse@dev=open_logfile(log)@filename=logendenddefset_file(shift_age,shift_size,shift_period_suffix)@shift_age=shift_age||@shift_age||7@shift_size=shift_size||@shift_size||1048576@shift_period_suffix=shift_period_suffix||@shift_period_suffix||'%Y%m%d'unless@shift_age.is_a?(Integer)base_time=@dev.respond_to?(:stat)?@dev.stat.mtime:Time.now@next_rotate_time=next_rotate_time(base_time,@shift_age)endendifMODE_TO_OPEN==MODEdeffixup_mode(dev)devendelsedeffixup_mode(dev)returndevif@binmodedev.autoclose=falseold_dev=devdev=File.new(dev.fileno,mode: MODE,path: dev.path)old_dev.closePathAttr.set_path(dev,filename)ifdefined?(PathAttr)devendenddefopen_logfile(filename)begindev=File.open(filename,MODE_TO_OPEN)rescueErrno::ENOENTcreate_logfile(filename)elsedev=fixup_mode(dev)dev.sync=truedev.binmodeif@binmodedevendenddefcreate_logfile(filename)beginlogdev=File.open(filename,MODE_TO_CREATE)logdev.flock(File::LOCK_EX)logdev=fixup_mode(logdev)logdev.sync=truelogdev.binmodeif@binmodeadd_log_header(logdev)unless@skip_headerlogdev.flock(File::LOCK_UN)logdevrescueErrno::EEXIST# file is created by another processopen_logfile(filename)endenddefhandle_write_errors(mesg)yieldrescue*@reraise_write_errorsraiserescuewarn("log #{mesg} failed. #{$!}")enddefadd_log_header(file)file.write("# Logfile created on %s by %s\n"%[Time.now.to_s,Logger::ProgName])iffile.size==0enddefcheck_shift_logif@shift_age.is_a?(Integer)# Note: always returns false if '0'.if@filename&&(@shift_age>0)&&(@dev.stat.size>@shift_size)lock_shift_log{shift_log_age}endelsenow=Time.nowifnow>=@next_rotate_time@next_rotate_time=next_rotate_time(now,@shift_age)lock_shift_log{shift_log_period(previous_period_end(now,@shift_age))}endendenddeflock_shift_logretry_limit=8retry_sleep=0.1beginFile.open(@filename,MODE_TO_OPEN)do|lock|lock.flock(File::LOCK_EX)# inter-process locking. will be unlocked at closing fileifFile.identical?(@filename,lock)andFile.identical?(lock,@dev)yield# log shiftingelse# log shifted by another process (i-node before locking and i-node after locking are different)@dev.closerescuenil@dev=open_logfile(@filename)endendtruerescueErrno::ENOENT# @filename file would not exist right after #rename and before #create_logfileifretry_limit<=0warn("log rotation inter-process lock failed. #{$!}")elsesleepretry_sleepretry_limit-=1retry_sleep*=2retryendendrescuewarn("log rotation inter-process lock failed. #{$!}")enddefshift_log_age(@shift_age-3).downto(0)do|i|ifFileTest.exist?("#{@filename}.#{i}")File.rename("#{@filename}.#{i}","#{@filename}.#{i+1}")endendshift_log_file("#{@filename}.0")enddefshift_log_period(period_end)suffix=period_end.strftime(@shift_period_suffix)age_file="#{@filename}.#{suffix}"ifFileTest.exist?(age_file)# try to avoid filename crash caused by Timestamp change.idx=0# .99 can be overridden; avoid too much file search with 'loop do'whileidx<100idx+=1age_file="#{@filename}.#{suffix}.#{idx}"breakunlessFileTest.exist?(age_file)endendshift_log_file(age_file)enddefshift_log_file(shifted)stat=@dev.stat@dev.closerescuenilFile.rename(@filename,shifted)@dev=create_logfile(@filename)mode,uid,gid=stat.mode,stat.uid,stat.gidbegin@dev.chmod(mode)ifmodemode=nil@dev.chown(uid,gid)rescueErrno::EPERMifmode# failed to chmod, probably nothing can do more.elsifuiduid=nilretry# to change gid onlyendendreturntrueendendendFile.open(__FILE__)do|f|File.new(f.fileno,autoclose: false,path: "").pathrescueIOErrormodulePathAttr# :nodoc:attr_reader:pathdefself.set_path(file,path)file.extend(self).instance_variable_set(:@path,path)endendend