logtwuncator 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +19 -0
- data/README.txt +186 -0
- data/Rakefile +19 -0
- data/bin/log_twuncator_daemon +6 -0
- data/bin/log_twuncator_service +5 -0
- data/example.yml +14 -0
- data/lib/log_twuncator.rb +3 -0
- data/lib/log_twuncator/daemon.rb +88 -0
- data/lib/log_twuncator/options.rb +47 -0
- data/lib/log_twuncator/service.rb +135 -0
- data/lib/log_twuncator/truncator.rb +112 -0
- data/lib/log_twuncator/win32_file.rb +48 -0
- data/test/test_config.yml +12 -0
- data/test/test_daemon.rb +44 -0
- data/test/test_helper.rb +22 -0
- data/test/test_service.rb +40 -0
- data/test/test_truncator.rb +93 -0
- data/test/test_win32_file.rb +45 -0
- metadata +89 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
example.yml
|
6
|
+
bin/log_twuncator_service
|
7
|
+
bin/log_twuncator_daemon
|
8
|
+
lib/log_twuncator.rb
|
9
|
+
lib/log_twuncator/truncator.rb
|
10
|
+
lib/log_twuncator/options.rb
|
11
|
+
lib/log_twuncator/service.rb
|
12
|
+
lib/log_twuncator/daemon.rb
|
13
|
+
lib/log_twuncator/win32_file.rb
|
14
|
+
test/test_config.yml
|
15
|
+
test/test_daemon.rb
|
16
|
+
test/test_truncator.rb
|
17
|
+
test/test_win32_file.rb
|
18
|
+
test/test_service.rb
|
19
|
+
test/test_helper.rb
|
data/README.txt
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
log_twuncator
|
2
|
+
by Adam Meehan (adam.meehan@gmail.com)
|
3
|
+
http://log_twuncator
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A log truncator for win32 originally to truncate rails log files but
|
8
|
+
generalised to any log files needing truncation and archiving. There
|
9
|
+
didn't seem be something easy, free and open out there so I thought
|
10
|
+
I would hack up a gem.
|
11
|
+
|
12
|
+
It comes with a windows service to install and leave running to monitor
|
13
|
+
the directories you specify in the config file. The config file allows the
|
14
|
+
specification of many directories to monitor for log file age and size.
|
15
|
+
|
16
|
+
File age is monitored by using the file creation date. In *nix programs like
|
17
|
+
logrotate the log file can be moved and recreated using POSIX signalling to
|
18
|
+
restart the process, and hence starting with a new file and creation date. On
|
19
|
+
windows this is not possible without getting really intimate with the running
|
20
|
+
process, so to get around it this program uses win32 API call to change the
|
21
|
+
created date after arching and truncating the file, leaving it in plave.
|
22
|
+
This seems to work fine with Rails and Apache but may cause problems
|
23
|
+
depending on how the running application has opened the log file to append
|
24
|
+
to it.
|
25
|
+
|
26
|
+
Please report any problems/suggestions you have.
|
27
|
+
|
28
|
+
Credits:
|
29
|
+
|
30
|
+
Herryanto Siatono (http://www.pluitsolutions.com/) for his
|
31
|
+
win32 Ferret service which I based some of the service structure on.
|
32
|
+
|
33
|
+
pdumpfs project (http://0xcc.net/pdumpfs/index.html.en) where I learned
|
34
|
+
the win32 API stuff in Ruby to do the file creation date changes.
|
35
|
+
|
36
|
+
|
37
|
+
== FEATURES/PROBLEMS:
|
38
|
+
|
39
|
+
* Multiple log truncation and archiving
|
40
|
+
* Nominate specific file or directory to monitor for truncation
|
41
|
+
* Use name formatting for archived file name
|
42
|
+
* Set defaults for settings to use in multiple monitored locations
|
43
|
+
* Set file or folder specific size and/or age limit
|
44
|
+
|
45
|
+
Future:
|
46
|
+
* Optional formats for the timestamp
|
47
|
+
* More archive name options
|
48
|
+
* Compress archived logs
|
49
|
+
* Email archived logs
|
50
|
+
|
51
|
+
== REQUIREMENTS:
|
52
|
+
|
53
|
+
win32-service gem required to run as service
|
54
|
+
|
55
|
+
== INSTALL:
|
56
|
+
|
57
|
+
gem install log_twuncator
|
58
|
+
|
59
|
+
== USAGE:
|
60
|
+
|
61
|
+
log_truncator_service <command> -N service_name <options>
|
62
|
+
|
63
|
+
For help
|
64
|
+
|
65
|
+
log_truncator_service -h
|
66
|
+
|
67
|
+
To install service:
|
68
|
+
|
69
|
+
log_truncator_service install -N log_twuncator -c c:/twuncator_config.yml -d 1 -l c:/log_twuncator.log
|
70
|
+
|
71
|
+
where:
|
72
|
+
-N : is the name of the service to install
|
73
|
+
-c : is the config file with the settings for various log monitoring paths
|
74
|
+
-d : is the number of minutes between checking the log files for truncation. Default is 5.
|
75
|
+
-l : is the log file for the truncator. Default is 'log_twuncator.log' is same folder as config file.
|
76
|
+
|
77
|
+
To remove service
|
78
|
+
|
79
|
+
log_truncator_service remove -N log_twuncator
|
80
|
+
|
81
|
+
To start/stop service
|
82
|
+
|
83
|
+
log_truncator_service start -N log_twuncator
|
84
|
+
|
85
|
+
log_truncator_service stop -N log_twuncator
|
86
|
+
|
87
|
+
And remember kids the service creation does not set the startup-type to 'Automatic',
|
88
|
+
so if you it want it to start with every windows startup then change this setting in
|
89
|
+
the Services management console ('services.msc' on the command prompt).
|
90
|
+
|
91
|
+
--- Config File
|
92
|
+
|
93
|
+
The config file allows for multiple monitored source directories for log files. The
|
94
|
+
setting options are as follows:
|
95
|
+
|
96
|
+
source_dir:
|
97
|
+
This is the root folder to monitor
|
98
|
+
|
99
|
+
filter:
|
100
|
+
This is used filter the files in the folder and takes the allowed settings used in
|
101
|
+
the ruby Dir.glob method. You could specify '*.log' for all files with log extension.
|
102
|
+
Or a full filename like 'production.log' to limit to just one file. See Dir.glob
|
103
|
+
method for more fancy possibilites including files in subdirectories.
|
104
|
+
|
105
|
+
age:
|
106
|
+
The age in days of the file before it should be archived and truncated. If set to
|
107
|
+
zero then age is never used as the trigger to truncate.
|
108
|
+
|
109
|
+
size:
|
110
|
+
The size of the log file in kilobytes above which the file should be truncated.
|
111
|
+
If set to 0 then size is never used as the trigger to truncate
|
112
|
+
|
113
|
+
archive_dir:
|
114
|
+
The full path directory to store the archived log file
|
115
|
+
|
116
|
+
archive_name:
|
117
|
+
The format of the archived log file name. You can use three components mixed with
|
118
|
+
any other characters you wish to insert into the file name when its archived. The
|
119
|
+
components are
|
120
|
+
|
121
|
+
[set_name] - the value of key in the config file for the set of options
|
122
|
+
[basename] - the original log filename without its extension
|
123
|
+
[ext] - the file extension of the log file
|
124
|
+
[timestamp] - the timestamp in the format yyyymmddHHMMSS
|
125
|
+
|
126
|
+
These can be combined to make the archived file name eg.
|
127
|
+
|
128
|
+
[basename]_[timestamp].[ext] - a log file called production.log archived at
|
129
|
+
2007-01-01 12:12:35 would become production_20070101121235.log
|
130
|
+
|
131
|
+
The config file allows for a 'defaults' section where you can set the defaults for
|
132
|
+
all subsequent monitored directories. Any setting specified in a monitoring set
|
133
|
+
will override the defaults for the same setting.
|
134
|
+
|
135
|
+
Best shown with an example:
|
136
|
+
|
137
|
+
Below is config settings in yaml format
|
138
|
+
---------------------------------------------------------------------------
|
139
|
+
defaults:
|
140
|
+
age: 7
|
141
|
+
size: 5000
|
142
|
+
filter: '*.log'
|
143
|
+
archive_dir: 'c:/logs'
|
144
|
+
archive_name: '[set_name]_[basename]_[date].[ext]'
|
145
|
+
|
146
|
+
app_name:
|
147
|
+
source_dir: 'c:/web/app/logs'
|
148
|
+
filter: 'production.log'
|
149
|
+
age: 0
|
150
|
+
archive_dir: 'c:/web/app/logs/archive'
|
151
|
+
---------------------------------------------------------------------------
|
152
|
+
|
153
|
+
This config defaults section defines all the settings, but keep in mind that a
|
154
|
+
defaults section alone does not constitute a monitored set. If you only have a
|
155
|
+
defaults section then nothing will be monitored. In the example the 'app_name'
|
156
|
+
monitored set will override the default source directory, filter, age and
|
157
|
+
archive_dir. The age is set to 0 so files will not be truncated based on age.
|
158
|
+
It will inherit the default size of 5000 (5000 KB or 5 MB) as the threshold for
|
159
|
+
truncation above which the file will be archived in c:/web/app/logs/archive
|
160
|
+
and truncated.
|
161
|
+
|
162
|
+
|
163
|
+
== LICENSE:
|
164
|
+
|
165
|
+
(The MIT License)
|
166
|
+
|
167
|
+
Copyright (c) 2007 Adam Meehan
|
168
|
+
|
169
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
170
|
+
a copy of this software and associated documentation files (the
|
171
|
+
'Software'), to deal in the Software without restriction, including
|
172
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
173
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
174
|
+
permit persons to whom the Software is furnished to do so, subject to
|
175
|
+
the following conditions:
|
176
|
+
|
177
|
+
The above copyright notice and this permission notice shall be
|
178
|
+
included in all copies or substantial portions of the Software.
|
179
|
+
|
180
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
181
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
182
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
183
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
184
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
185
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
186
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/log_twuncator.rb'
|
6
|
+
|
7
|
+
Hoe.new('logtwuncator', LogTwuncator::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'logtwuncator'
|
9
|
+
p.author = 'Adam Meehan'
|
10
|
+
p.email = 'adam.meehan@gmail.com'
|
11
|
+
p.summary = 'A win32 log file truncator and archiver with windows service'
|
12
|
+
p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
|
13
|
+
p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
14
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
15
|
+
p.remote_rdoc_dir = ''
|
16
|
+
p.extra_deps << ['win32-service', '>= 0.5.2']
|
17
|
+
end
|
18
|
+
|
19
|
+
# vim: syntax=Ruby
|
data/example.yml
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
require 'logger'
|
4
|
+
require 'win32/service'
|
5
|
+
require 'log_twuncator/truncator'
|
6
|
+
require 'log_twuncator/options'
|
7
|
+
|
8
|
+
module LogTwuncator
|
9
|
+
|
10
|
+
class ConfigFileNotFound < StandardError; end
|
11
|
+
|
12
|
+
class Daemon < Win32::Daemon
|
13
|
+
attr_reader :config, :delay, :truncators, :logger
|
14
|
+
|
15
|
+
def self.run(args = ARGV)
|
16
|
+
new LogTwuncator::Options.parse(args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options)
|
20
|
+
@config, @delay, @log = options[:config], options[:delay], options[:log]
|
21
|
+
@truncators = []
|
22
|
+
@log = File.join(File.dirname(@config), 'log_twuncator.log') unless options[:log]
|
23
|
+
@logger = Logger.new(@log)
|
24
|
+
end
|
25
|
+
|
26
|
+
def service_init
|
27
|
+
log "starting service"
|
28
|
+
load_config
|
29
|
+
end
|
30
|
+
|
31
|
+
def service_main
|
32
|
+
while running?
|
33
|
+
@truncators.each {|truncator| truncator.truncate }
|
34
|
+
|
35
|
+
# sleep for 1 second for number of seconds in delay minutes which prevents process blocking
|
36
|
+
(@delay * 60).times {sleep 1}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def service_stop
|
41
|
+
log "stopping service"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Loads config yaml file. Sets the truncator class defaults from the config section if one is labelled 'defaults'.
|
45
|
+
# Create truncators for each config section, which are run in the service_main loop.
|
46
|
+
# Config file can have ERB tags embedded for power and convenience
|
47
|
+
def load_config
|
48
|
+
raise LogTwuncator::ConfigFileNotFound unless File.exists?(@config)
|
49
|
+
config = YAML::load(ERB.new(IO.read(@config)).result)
|
50
|
+
set_defaults(symbolize_keys(config.delete('defaults'))) if config['defaults']
|
51
|
+
config.map do |key, options|
|
52
|
+
options[:logger] = @logger
|
53
|
+
options[:name] = key
|
54
|
+
begin
|
55
|
+
@truncators << LogTwuncator::Truncator.new(symbolize_keys(options))
|
56
|
+
rescue InvalidTruncatorOptions => e
|
57
|
+
log "Config set '#{key}' was skipped due to the following errors:\n#{e.message}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
rescue ConfigFileNotFound
|
61
|
+
log "Config file not found"
|
62
|
+
rescue => e
|
63
|
+
log "Unknown error occurred: #{e}"
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
# Sets the class defaults for the truncator class
|
68
|
+
def set_defaults(options)
|
69
|
+
LogTwuncator::Truncator.defaults = options
|
70
|
+
end
|
71
|
+
|
72
|
+
def log(msg)
|
73
|
+
if @logger
|
74
|
+
@logger.info msg
|
75
|
+
else
|
76
|
+
puts msg
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Taken from rails
|
81
|
+
def symbolize_keys(hash)
|
82
|
+
hash.inject({}) do |options, (key, value)|
|
83
|
+
options[key.to_sym] = value
|
84
|
+
options
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module LogTwuncator
|
4
|
+
class Options
|
5
|
+
|
6
|
+
def self.parse(args = ARGV)
|
7
|
+
options = {}
|
8
|
+
options[:delay] = 5
|
9
|
+
opts = OptionParser.new do |opts|
|
10
|
+
opts.banner = "Usage: log_twuncator_daemon.rb [options]"
|
11
|
+
opts.separator ""
|
12
|
+
opts.separator "Specific options:"
|
13
|
+
|
14
|
+
opts.on("-d", "--delay N", Integer, "Check files for truncation every N minutes (default: 5)") do |v|
|
15
|
+
options[:delay] = v
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("-c", "--config FILE", String, "Config file with projects session files to check") do |v|
|
19
|
+
options[:config] = v
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("-l", "--log FILE", String, "Log file for truncator") do |v|
|
23
|
+
options[:log] = v
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
27
|
+
puts opts
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
end
|
31
|
+
opts.parse!(args)
|
32
|
+
validate_options options
|
33
|
+
options
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.validate_options(options)
|
37
|
+
unless options[:delay]
|
38
|
+
puts "Delay in seconds required [-d N]"
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
unless options[:config]
|
42
|
+
puts "Config file required [-c FILE]"
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'win32/service'
|
3
|
+
|
4
|
+
module LogTwuncator
|
5
|
+
class ServiceCommand
|
6
|
+
attr_reader :command, :options
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@options = {}
|
10
|
+
@options[:delay] = 5
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(args)
|
14
|
+
@command = args.shift unless args[0] == '-h'
|
15
|
+
@command = @command.downcase if @command
|
16
|
+
|
17
|
+
parse(args)
|
18
|
+
@options
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(args)
|
22
|
+
begin
|
23
|
+
opts = OptionParser.new do |opts|
|
24
|
+
opts.separator ""
|
25
|
+
opts.banner = "Usage: log_twuncator_service <command> [options]"
|
26
|
+
opts.separator ""
|
27
|
+
opts.separator "Specific options:"
|
28
|
+
opts.separator ""
|
29
|
+
opts.separator "Commands : install, remove, start and stop"
|
30
|
+
opts.separator ""
|
31
|
+
opts.on("-N", "--name SERVICE_NAME", String, "Name of windows service") do |v|
|
32
|
+
@options[:name] = v
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-c", "--config FILE", String, "Config file with log file truncation settings") do |v|
|
36
|
+
@options[:config] = v
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("-d", "--delay N", Integer, "Check files for truncation every N minutes") do |v|
|
40
|
+
@options[:delay] = v
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("-l", "--log FILE", String, "Log file for truncator") do |v|
|
44
|
+
@options[:log] = v
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
48
|
+
puts opts.help
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
end
|
52
|
+
opts.parse!(args)
|
53
|
+
validate_options
|
54
|
+
rescue OptionParser::ParseError => e
|
55
|
+
puts e
|
56
|
+
puts opts
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_options
|
61
|
+
errors = []
|
62
|
+
errors << "Service name is required (switch -N)" unless @options[:name]
|
63
|
+
errors << "Delay is required (switch -d)" if !@options[:delay] && @command == 'install'
|
64
|
+
errors << "Config file is required (switch -c)" if !@options[:config] && @command == 'install'
|
65
|
+
errors << "Config file does not exist" if @options[:config] && !FileTest.exists?(@options[:config])
|
66
|
+
|
67
|
+
if errors.size > 0
|
68
|
+
puts "Error found."
|
69
|
+
puts errors.join("\n")
|
70
|
+
exit
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
class Service
|
77
|
+
|
78
|
+
def install
|
79
|
+
svc = Win32::Service.new
|
80
|
+
if Win32::Service.exists?(@options[:name])
|
81
|
+
puts "Service name '#{@options[:name]}' already exists."
|
82
|
+
return
|
83
|
+
end
|
84
|
+
|
85
|
+
svc.create_service do |s|
|
86
|
+
s.service_name = @options[:name]
|
87
|
+
s.display_name = @options[:name]
|
88
|
+
s.binary_path_name = binary_path_name
|
89
|
+
end
|
90
|
+
|
91
|
+
svc.close
|
92
|
+
puts "Log Twuncator service '#{@options[:name]}' installed."
|
93
|
+
rescue Win32::ServiceError => e
|
94
|
+
puts "Service '#{@options[:name]}' failed to install due to error: #{e.message}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def start
|
98
|
+
Win32::Service.start(@options[:name])
|
99
|
+
puts "'#{@options[:name]}' service started."
|
100
|
+
rescue Win32::ServiceError => e
|
101
|
+
puts "Service '#{@options[:name]}' failed to start due to error: #{e.message}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def stop
|
105
|
+
Win32::Service.stop(@options[:name])
|
106
|
+
puts "'#{@options[:name]}' service stopped."
|
107
|
+
rescue Win32::ServiceError => e
|
108
|
+
puts "Service '#{@options[:name]}' failed to stop due to error: #{e.message}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def remove
|
112
|
+
begin
|
113
|
+
Win32::Service.stop(@options[:name])
|
114
|
+
rescue
|
115
|
+
end
|
116
|
+
|
117
|
+
Win32::Service.delete(@options[:name])
|
118
|
+
puts "'#{@options[:name]}' service removed."
|
119
|
+
rescue Win32::ServiceError => e
|
120
|
+
puts "Service '#{@options[:name]}' failed to be removed due to error: #{e.message}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def binary_path_name
|
124
|
+
path = "#{Config::CONFIG['bindir']}/rubyw #{Config::CONFIG['bindir']}/log_twuncator_daemon"
|
125
|
+
path << " -c #{@options[:config]}"
|
126
|
+
path << " -d #{@options[:delay]}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def run(args)
|
130
|
+
command = LogTwuncator::ServiceCommand.new
|
131
|
+
@options = command.run(args)
|
132
|
+
self.send(command.command.to_sym)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'log_twuncator/win32_file'
|
3
|
+
|
4
|
+
module LogTwuncator
|
5
|
+
|
6
|
+
class InvalidTruncatorOptions < StandardError; end
|
7
|
+
|
8
|
+
class Truncator
|
9
|
+
@@defaults = {
|
10
|
+
:age => 7,
|
11
|
+
:size => 1_000,
|
12
|
+
:archive_name => "[basename]_[timestamp].[ext]"
|
13
|
+
}
|
14
|
+
|
15
|
+
TIMESTAMP_FORMAT = "%Y%m%d%H%M%S"
|
16
|
+
|
17
|
+
attr_reader :name, :age, :size, :source_dir, :filter, :archive_dir, :archive_name, :logger
|
18
|
+
|
19
|
+
def initialize(options)
|
20
|
+
@options = @@defaults.merge(options)
|
21
|
+
[:name, :age, :size, :source_dir, :filter, :archive_dir, :archive_name, :logger].each {|sym| instance_variable_set("@#{sym}".to_sym, @options[sym]) }
|
22
|
+
if errors = self.class.validate_options(@options)
|
23
|
+
raise InvalidTruncatorOptions, errors.join("\n")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Find files using source directory and filter and then loop of files checking for files
|
28
|
+
# large than _size_ in kilobytes or older than _age_ in days.
|
29
|
+
def truncate
|
30
|
+
path_filter = File.join(@source_dir, @filter)
|
31
|
+
truncated = 0
|
32
|
+
time = Time.now - age_in_seconds
|
33
|
+
Dir[path_filter].each do |file|
|
34
|
+
next unless (@size > 0 && File.stat(file).size > size_in_bytes) || (@age > 0 && File.stat(file).ctime < time)
|
35
|
+
archive_time = Time.now
|
36
|
+
archive file, archive_time
|
37
|
+
File.truncate(file, 0)
|
38
|
+
set_truncated_time(file, archive_time)
|
39
|
+
truncated += 1
|
40
|
+
log "Truncated #{file}"
|
41
|
+
end
|
42
|
+
truncated
|
43
|
+
rescue => e
|
44
|
+
log "Unknown error occurred: #{e}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Archive _file_name_ to archive directory specified by _archive_dir_
|
50
|
+
def archive(file_name, time = Time.now)
|
51
|
+
archive_file = archive_filename(file_name)
|
52
|
+
FileUtils.makedirs(File.dirname(archive_file))
|
53
|
+
FileUtils.copy(file_name, archive_file)
|
54
|
+
log "Archived file #{file_name} to #{archive_file}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Contruct the archived filename from the archive_name setting. The filename components are allowed which are [basename], [ext] and [timestamp].
|
58
|
+
# The archived filname is then constructed by substituting the values for the file in specified in archive_name
|
59
|
+
def archive_filename(file_name, time = Time.now)
|
60
|
+
basename = File.basename(file_name, ".*")
|
61
|
+
ext = File.extname(file_name)
|
62
|
+
time_str = time.strftime(TIMESTAMP_FORMAT)
|
63
|
+
new_name = @archive_name.sub("[set_name]", @name.to_s).sub("[basename]", basename).sub(".[ext]", ext).sub("[timestamp]", time_str)
|
64
|
+
File.join(@archive_dir, new_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_truncated_time(file, time)
|
68
|
+
LogTwuncator::Win32File.set_file_time(file, time, nil, nil)
|
69
|
+
end
|
70
|
+
|
71
|
+
def log(msg)
|
72
|
+
if @logger
|
73
|
+
@logger.info(msg)
|
74
|
+
else
|
75
|
+
puts msg
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# File size converted from kilobytes to bytes
|
80
|
+
def size_in_bytes
|
81
|
+
@size * 1024
|
82
|
+
end
|
83
|
+
|
84
|
+
# Age converted from days into seconds
|
85
|
+
def age_in_seconds
|
86
|
+
@age * 86400
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.validate_options(options)
|
90
|
+
errors = []
|
91
|
+
errors << "No source directory defined" unless options[:source_dir]
|
92
|
+
errors << "Source directory is not a valid path" unless options[:source_dir] && FileTest.exists?(options[:source_dir])
|
93
|
+
errors << "Archive directory is not a valid path" unless options[:archive_dir] && FileTest.exists?(options[:archive_dir])
|
94
|
+
errors << "No file filter defined" unless options[:filter]
|
95
|
+
errors << "Size is not a valid number" unless options[:size] && options[:size].is_a?(Fixnum)
|
96
|
+
errors << "Age is not a valid number" unless options[:age] && options[:age].is_a?(Fixnum)
|
97
|
+
errors << "Either age or size must be non-zero" if options[:size].is_a?(Fixnum) && (options[:age] == 0 && options[:size] == 0)
|
98
|
+
|
99
|
+
return errors.empty? ? false : errors
|
100
|
+
end
|
101
|
+
|
102
|
+
# Allows the class defaults to be set
|
103
|
+
def self.defaults=(value)
|
104
|
+
@@defaults = value
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.defaults
|
108
|
+
@@defaults
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'Win32API'
|
2
|
+
|
3
|
+
# This code has been mostly taken from pdumpfs project http://raa.ruby-lang.org/project/pdumpfs/
|
4
|
+
# Uses the Win32API to do win32 system calls to change file dates
|
5
|
+
module LogTwuncator
|
6
|
+
class Win32File
|
7
|
+
GENERIC_WRITE = 0x40000000
|
8
|
+
GENERIC_EXECUTE = 0x20000000
|
9
|
+
GENERIC_ALL = 0x10000000
|
10
|
+
FILE_SHARE_WRITE = 2
|
11
|
+
OPEN_EXISTING = 3
|
12
|
+
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
|
13
|
+
|
14
|
+
GetLocaltime = Win32API.new("kernel32", "GetLocalTime", "P", 'V')
|
15
|
+
SystemTimeToFileTime = Win32API.new("kernel32", "SystemTimeToFileTime", "PP", 'I')
|
16
|
+
CreateFile = Win32API.new("kernel32", "CreateFileA", "PLLLLLL", "L")
|
17
|
+
SetFileTime = Win32API.new("kernel32", "SetFileTime", "LPPP", "I")
|
18
|
+
CloseHandle = Win32API.new("kernel32", "CloseHandle", "L", "I")
|
19
|
+
|
20
|
+
# Convert Ruby time into win32 FileTime
|
21
|
+
def self.get_file_time(time)
|
22
|
+
pSYSTEMTIME = ' ' * 2 * 8 # 2byte x 8
|
23
|
+
pFILETIME = ' ' * 2 * 8 # 2byte x 8
|
24
|
+
GetLocaltime.call(pSYSTEMTIME)
|
25
|
+
time_arr = pSYSTEMTIME.unpack("S8")
|
26
|
+
time_arr[0..1] = time.year, time.month
|
27
|
+
time_arr[3..6] = time.day, time.hour, time.min, time.sec
|
28
|
+
SystemTimeToFileTime.call(time_arr.pack("S8"), pFILETIME)
|
29
|
+
pFILETIME
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set created, accessed and modified dates for a file
|
33
|
+
def self.set_file_time(file, ctime=nil, atime=nil, mtime=nil)
|
34
|
+
hFile = 0
|
35
|
+
|
36
|
+
times = [ctime, atime, mtime].collect do |time|
|
37
|
+
next( 0 ) if time.nil?
|
38
|
+
get_file_time(time.dup.utc)
|
39
|
+
end
|
40
|
+
|
41
|
+
hFile = CreateFile.call(file.dup, GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0)
|
42
|
+
res = SetFileTime.call(hFile, times[0], times[1], times[2])
|
43
|
+
CloseHandle.call(hFile)
|
44
|
+
return res
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
defaults:
|
2
|
+
age: 7
|
3
|
+
size: 5000
|
4
|
+
filter: '*.log'
|
5
|
+
archive_dir: <%= File.dirname(File.expand_path(__FILE__)) + '/test/test_archive' %>
|
6
|
+
archive_name: '[basename]_[timestamp].[ext]'
|
7
|
+
|
8
|
+
app_name:
|
9
|
+
source_dir: <%= File.dirname(File.expand_path(__FILE__)) + '/test/test_log' %>
|
10
|
+
filter: '*.log'
|
11
|
+
age: 2
|
12
|
+
size: 1
|
data/test/test_daemon.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
require 'log_twuncator/daemon'
|
3
|
+
|
4
|
+
class TestDaemon < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
setup_paths_and_files
|
8
|
+
@defaults = LogTwuncator::Truncator.defaults
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_create_daemon_with_args
|
12
|
+
d = create
|
13
|
+
assert_equal @test_config, d.config
|
14
|
+
assert_equal 1, d.delay
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_load_config
|
18
|
+
d = create
|
19
|
+
d.load_config
|
20
|
+
assert_equal 1, d.truncators.size
|
21
|
+
assert_equal @log_path, d.truncators[0].source_dir
|
22
|
+
assert_equal 'app_name', d.truncators.first.name
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_truncator_class_defaults_loaded
|
26
|
+
d = create
|
27
|
+
d.load_config
|
28
|
+
assert_equal 5000, LogTwuncator::Truncator.defaults[:size]
|
29
|
+
assert_equal 7, LogTwuncator::Truncator.defaults[:age]
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_log_file
|
33
|
+
d = create
|
34
|
+
d.load_config
|
35
|
+
end
|
36
|
+
|
37
|
+
def create
|
38
|
+
LogTwuncator::Daemon.run( %w{-d 1 -c } << @test_config << '-l' << @log_file)
|
39
|
+
end
|
40
|
+
|
41
|
+
def teardown
|
42
|
+
LogTwuncator::Truncator.defaults = @defaults
|
43
|
+
end
|
44
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
class Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup_paths_and_files
|
6
|
+
@base_path = File.dirname(File.expand_path(__FILE__))
|
7
|
+
|
8
|
+
@archive_path = File.join(@base_path, 'test_archive')
|
9
|
+
@log_path = File.join(@base_path, 'test_log')
|
10
|
+
|
11
|
+
Dir.mkdir(@archive_path) unless FileTest.exist?(@archive_path)
|
12
|
+
Dir.mkdir(@log_path) unless FileTest.exist?(@log_path)
|
13
|
+
|
14
|
+
FileUtils.rm Dir[@log_path + "/*.*"]
|
15
|
+
FileUtils.rm Dir[@archive_path + "/*.*"]
|
16
|
+
|
17
|
+
@test_config = File.join(@base_path, 'test_config.yml')
|
18
|
+
@test_log = File.join(@log_path, 'test.log')
|
19
|
+
@log_file = File.join(@base_path, 'log_twuncator.log')
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
require 'log_twuncator/service'
|
3
|
+
require 'win32/service'
|
4
|
+
|
5
|
+
class TestService < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
setup_paths_and_files
|
9
|
+
@cmd = LogTwuncator::ServiceCommand.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_install_args
|
13
|
+
options = create_command
|
14
|
+
assert_equal 'install', @cmd.command
|
15
|
+
assert_equal 'test_log_truncator', options[:name]
|
16
|
+
assert_equal 1, options[:delay]
|
17
|
+
assert_equal @test_config, options[:config]
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_create_service
|
21
|
+
assert_nothing_raised { create_service }
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_command
|
25
|
+
@cmd.run( %w{install -N test_log_truncator -d 1 -c } << @test_config )
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_service
|
29
|
+
name = 'test_log_truncator'
|
30
|
+
LogTwuncator::Service.new.run(%w{install -N test_log_truncator -d 1 -c } << @test_config)
|
31
|
+
assert Win32::Service.exists?(name)
|
32
|
+
yield(name) if block_given?
|
33
|
+
sleep 2
|
34
|
+
Win32::Service.delete(name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def teardown
|
38
|
+
Win32::Service.delete('test_log_truncator') if Win32::Service.exists?('test_log_truncator')
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
require 'log_twuncator/truncator'
|
3
|
+
|
4
|
+
class TestTruncator < Test::Unit::TestCase
|
5
|
+
|
6
|
+
DAY_IN_SECONDS = 86400
|
7
|
+
|
8
|
+
def setup
|
9
|
+
setup_paths_and_files
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_defaults
|
13
|
+
lt = LogTwuncator::Truncator.new({:source_dir => @log_path, :archive_dir => @archive_path, :filter => '*.log'})
|
14
|
+
assert_equal 1_000, lt.size
|
15
|
+
assert_equal 7, lt.age
|
16
|
+
assert_equal '*.log', lt.filter
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_defaults_overridden_with_create_options
|
20
|
+
lt = create({ :size => 2_000, :age => 30 })
|
21
|
+
assert_equal 30, lt.age
|
22
|
+
assert_equal 2_000, lt.size
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_archive_name_format
|
26
|
+
file = create_dummy_log @test_log
|
27
|
+
lt = create(:filter => 'test.log', :archive_dir => @archive_path, :archive_name => '[set_name]_[basename]_[timestamp].[ext]')
|
28
|
+
time = Time.new
|
29
|
+
assert_equal File.join(@archive_path, "app_name_test_#{time.strftime("%Y%m%d%H%M%S")}.log"), lt.send(:archive_filename, @test_log, time)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_single_file_truncation
|
33
|
+
file = create_dummy_log @test_log
|
34
|
+
lt = create(:filter => 'test.log', :archive_dir => @archive_path)
|
35
|
+
lt.truncate
|
36
|
+
assert FileTest::zero?(file)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_multiple_file_truncation
|
40
|
+
file1 = create_dummy_log(File.dirname(@test_log) + '/test1.log')
|
41
|
+
file2 = create_dummy_log(File.dirname(@test_log) + '/test2.log')
|
42
|
+
lt = create(:filter => 'test[1-2]*.log', :archive_dir => @archive_path)
|
43
|
+
assert_equal 2, lt.truncate
|
44
|
+
assert FileTest::zero?(file1)
|
45
|
+
assert FileTest::zero?(file2)
|
46
|
+
assert_equal 2, Dir[@archive_path + '/*.log'].size
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_file_skipped_when_size_is_zero
|
50
|
+
file = create_dummy_log @test_log
|
51
|
+
lt = create :size => 0, :age => 7
|
52
|
+
assert_equal 0, lt.truncate
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_file_skipped_when_age_is_zero
|
56
|
+
file = create_dummy_log @test_log
|
57
|
+
lt = create :size => 2, :age => 0
|
58
|
+
assert_equal 0, lt.truncate
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_file_skipped_when_below_size_and_age
|
62
|
+
file = create_dummy_log @test_log
|
63
|
+
lt = create :size => 2, :age => 7
|
64
|
+
assert_equal 0, lt.truncate
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_file_archived_when_below_size_but_over_age
|
68
|
+
file = create_dummy_log @test_log
|
69
|
+
LogTwuncator::Win32File.set_file_time(file, Time.now - (2 * DAY_IN_SECONDS))
|
70
|
+
lt = create :size => 2, :age => 1
|
71
|
+
assert_equal 1, lt.truncate
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_file_archived_when_below_age_but_over_size
|
75
|
+
file = create_dummy_log @test_log, 3
|
76
|
+
lt = create :size => 2, :age => 1
|
77
|
+
assert_equal 1, lt.truncate
|
78
|
+
end
|
79
|
+
|
80
|
+
def create(options={})
|
81
|
+
LogTwuncator::Truncator.new({:name => 'app_name', :source_dir => @log_path, :archive_dir => @archive_path, :filter => '*.log', :size => 1}.merge(options))
|
82
|
+
end
|
83
|
+
|
84
|
+
# size is in kilobytes
|
85
|
+
def create_dummy_log(name, size=1)
|
86
|
+
filesize = 0
|
87
|
+
bytes = size * 1024
|
88
|
+
File.open(name, 'w') do |f|
|
89
|
+
f.puts "A" * bytes
|
90
|
+
end
|
91
|
+
name
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
require 'log_twuncator/win32_file'
|
3
|
+
|
4
|
+
class TestWin32File < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
setup_paths_and_files
|
8
|
+
@test_file = File.join(@log_path, 'test.txt')
|
9
|
+
@yesterday = yesterday = Time.now - 86400
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_set_file_time_with_no_changes
|
13
|
+
f = File.new(@test_file, File::CREAT)
|
14
|
+
f.close
|
15
|
+
mtime = File.stat(@test_file).mtime
|
16
|
+
LogTwuncator::Win32File.set_file_time(@test_file, nil, nil, nil)
|
17
|
+
assert_equal mtime, File.stat(@test_file).mtime
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_set_file_times_on_closed_file
|
21
|
+
file = File.open(@test_file, File::CREAT)
|
22
|
+
file.close
|
23
|
+
LogTwuncator::Win32File.set_file_time(file.path, @yesterday, nil, nil)
|
24
|
+
assert_equal @yesterday.to_s, File.stat(file.path).ctime.to_s
|
25
|
+
LogTwuncator::Win32File.set_file_time(file.path, nil, @yesterday, nil)
|
26
|
+
assert_equal @yesterday.to_s, File.stat(file.path).atime.to_s
|
27
|
+
LogTwuncator::Win32File.set_file_time(file.path, nil, nil, @yesterday)
|
28
|
+
assert_equal @yesterday.to_s, File.stat(file.path).mtime.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_set_file_times_on_open_file
|
32
|
+
File.open(@test_file, File::CREAT) do |file|
|
33
|
+
LogTwuncator::Win32File.set_file_time(file.path, @yesterday, nil, nil)
|
34
|
+
assert_equal @yesterday.to_s, File.stat(file.path).ctime.to_s
|
35
|
+
|
36
|
+
# FIXME: these fail and I think its because of the file interaction which changes the dates
|
37
|
+
# LogTwuncator::Win32File.set_file_time(file.path, nil, @yesterday, nil)
|
38
|
+
# assert_equal @yesterday.to_s, File.stat(file.path).atime.to_s
|
39
|
+
# LogTwuncator::Win32File.set_file_time(file.path, nil, nil, @yesterday)
|
40
|
+
# assert_equal @yesterday.to_s, File.stat(file.path).mtime.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: logtwuncator
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2007-10-17 00:00:00 +10:00
|
8
|
+
summary: A win32 log file truncator and archiver with windows service
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: adam.meehan@gmail.com
|
12
|
+
homepage: " by Adam Meehan (adam.meehan@gmail.com)"
|
13
|
+
rubyforge_project: logtwuncator
|
14
|
+
description: "It comes with a windows service to install and leave running to monitor the directories you specify in the config file. The config file allows the specification of many directories to monitor for log file age and size. File age is monitored by using the file creation date. In *nix programs like logrotate the log file can be moved and recreated using POSIX signalling to restart the process, and hence starting with a new file and creation date. On windows this is not possible without getting really intimate with the running process, so to get around it this program uses win32 API call to change the created date after arching and truncating the file, leaving it in plave. This seems to work fine with Rails and Apache but may cause problems depending on how the running application has opened the log file to append to it. Please report any problems/suggestions you have. Credits:"
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Adam Meehan
|
31
|
+
files:
|
32
|
+
- History.txt
|
33
|
+
- Manifest.txt
|
34
|
+
- README.txt
|
35
|
+
- Rakefile
|
36
|
+
- example.yml
|
37
|
+
- bin/log_twuncator_service
|
38
|
+
- bin/log_twuncator_daemon
|
39
|
+
- lib/log_twuncator.rb
|
40
|
+
- lib/log_twuncator/truncator.rb
|
41
|
+
- lib/log_twuncator/options.rb
|
42
|
+
- lib/log_twuncator/service.rb
|
43
|
+
- lib/log_twuncator/daemon.rb
|
44
|
+
- lib/log_twuncator/win32_file.rb
|
45
|
+
- test/test_config.yml
|
46
|
+
- test/test_daemon.rb
|
47
|
+
- test/test_truncator.rb
|
48
|
+
- test/test_win32_file.rb
|
49
|
+
- test/test_service.rb
|
50
|
+
- test/test_helper.rb
|
51
|
+
test_files:
|
52
|
+
- test/test_daemon.rb
|
53
|
+
- test/test_helper.rb
|
54
|
+
- test/test_service.rb
|
55
|
+
- test/test_truncator.rb
|
56
|
+
- test/test_win32_file.rb
|
57
|
+
rdoc_options:
|
58
|
+
- --main
|
59
|
+
- README.txt
|
60
|
+
extra_rdoc_files:
|
61
|
+
- History.txt
|
62
|
+
- Manifest.txt
|
63
|
+
- README.txt
|
64
|
+
executables:
|
65
|
+
- log_twuncator_service
|
66
|
+
- log_twuncator_daemon
|
67
|
+
extensions: []
|
68
|
+
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
dependencies:
|
72
|
+
- !ruby/object:Gem::Dependency
|
73
|
+
name: win32-service
|
74
|
+
version_requirement:
|
75
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 0.5.2
|
80
|
+
version:
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: hoe
|
83
|
+
version_requirement:
|
84
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 1.3.0
|
89
|
+
version:
|