baby-bro 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ pkg
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2010-12-03
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
@@ -0,0 +1,26 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ baby-bro.gemspec
7
+ bin/bro
8
+ config/babybrorc.example
9
+ lib/baby-bro.rb
10
+ lib/baby-bro/base_config.rb
11
+ lib/baby-bro/exec.rb
12
+ lib/baby-bro/files.rb
13
+ lib/baby-bro/hash_object.rb
14
+ lib/baby-bro/monitor.rb
15
+ lib/baby-bro/monitor_config.rb
16
+ lib/baby-bro/project.rb
17
+ lib/baby-bro/reporter.rb
18
+ lib/baby-bro/session.rb
19
+ lib/extensions/fixnum.rb
20
+ script/console
21
+ script/destroy
22
+ script/generate
23
+ spec/baby-bro_spec.rb
24
+ spec/spec.opts
25
+ spec/spec_helper.rb
26
+ tasks/rspec.rake
@@ -0,0 +1,5 @@
1
+
2
+ For more information on baby-bro, see https://github.com/capitalthought/baby-bro
3
+
4
+
5
+
@@ -0,0 +1,100 @@
1
+ = baby-bro
2
+
3
+ * http://github.com/capitalthought/baby-bro
4
+
5
+ == DESCRIPTION:
6
+
7
+ Baby Bro monitors timestamp changes in configured project directories and automatically tracks active development time for those projects. The name is a play on "Big Brother", which came up in a conversation with a colleague when discussing the idea for this utility. As in, if your employer were running this utility on your workstation with you knowing it, "Big Brother" would be watching you.
8
+
9
+ Baby Bro isn't meant to be used like that, however. It's meant to be used by anyone who wants to automatically keep track of the amount of time they spend actively working on files in a particular project's directory.
10
+
11
+ == SYNOPSIS:
12
+
13
+ When working on source code, a developer is typically modifying files in a particular directory and saving them periodically. By monitoring the timestamps of files in a project directory and detecting when a file has been updated, one could theoretically measure a developer's time spent changing code by logging timestamp changes and grouping them in to sessions of continuous activity. This is how Baby Bro works.
14
+
15
+ == REQUIREMENTS:
16
+
17
+
18
+
19
+ == INSTALL:
20
+
21
+ Baby Bro is installed as a Ruby gem.
22
+
23
+ sudo gem install baby-bro
24
+
25
+ Create a configuration file. By default baby-bro will look in your home directory for .babybrorc.
26
+
27
+ The config file is YAML. In addition to configuring options for the monitor, you must configure at least one project to monitor before baby-bro will do anything.
28
+
29
+ An example config file:
30
+
31
+ ---
32
+ :data_directory: ~/.babybro
33
+ :polling_interval: 1 minute
34
+ :idle_interval: 5 minutes
35
+ :projects:
36
+ - :name: Baby Bro
37
+ :directory: ~/src/capitalthought/baby-bro
38
+ - :name: Some Other Project
39
+ :directory: ~/src/capitalthought/sop
40
+
41
+ This configuration tells baby-bro to monitor activity sessions in the "Baby Bro" project directory ~/src/capitalthought/baby-bro and also in some other project's directory. The monitor will poll every "1 minute" for updated files in the directories and record "sessions" or stretches of continuous activity. A session is considered to be active as long as updates to files in the directory occur at least every "5 minutes".
42
+
43
+ If activity is suspended for more than the idle interval, a new session is started and recorded. Activity is detected when any file in a project's directory has its mtime changed.
44
+
45
+ == MONITORING ACTIVITY:
46
+
47
+ All baby-bro functionality is accessed through one executable: 'bro', which is installed in your gem's executable path.
48
+
49
+ To start baby-bro:
50
+
51
+ baby-bro start -t
52
+
53
+ This will start baby-bro's monitor in the background. The -t flag causes the monitor to output status to standard output and is useful to see what the monitor is detecting. Omit this flag to keep baby-bro quiet.
54
+
55
+ To stop baby-bro:
56
+
57
+ baby-bro stop
58
+
59
+ To re-read the config file:
60
+
61
+ baby-bro restart
62
+
63
+ == REPORTING:
64
+
65
+ To view a report of activity sessions recorded by baby-bro:
66
+
67
+ baby-bro report
68
+
69
+ To view the help message and see other options:
70
+
71
+ baby-bro --help
72
+
73
+ That's it. You can add as many projects to your config as you like. They will all get monitored by baby-bro.
74
+
75
+ == TODO:
76
+
77
+ * Enable reporting of specific date ranges
78
+ * Default reports to the current day
79
+ * Enable option to export reports in .csv, .json and .yaml file formats.
80
+ * Add some tests.
81
+
82
+ == CONTRIBUTING:
83
+
84
+ Contributions to Baby Bro are welcome. All pull requests will be considered. Feel free to e-mail me first about ideas or suggestions: billdoughty AT capitalthought DOT com.
85
+
86
+ == LICENSE:
87
+
88
+ Copyright 2010 Capital Thought, LLC
89
+
90
+ Licensed under the Apache License, Version 2.0 (the "License");
91
+ you may not use any part of this software or its source code except
92
+ in compliance with the License. You may obtain a copy of the License at
93
+
94
+ http://www.apache.org/licenses/LICENSE-2.0
95
+
96
+ Unless required by applicable law or agreed to in writing, software
97
+ distributed under the License is distributed on an "AS IS" BASIS,
98
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
99
+ See the License for the specific language governing permissions and
100
+ limitations under the License.
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ # gem 'hoe', '>= 2.1.0'
3
+ # require 'hoe'
4
+ require 'fileutils'
5
+ require './lib/baby-bro'
6
+
7
+ # Hoe.plugin :newgem
8
+ # # Hoe.plugin :website
9
+ # # Hoe.plugin :cucumberfeatures
10
+ #
11
+ # # Generate all the Rake tasks
12
+ # # Run 'rake -T' to see list of generated tasks (from gem root directory)
13
+ # $hoe = Hoe.spec 'baby-bro' do
14
+ # self.developer 'Bill Doughty', 'billdoughty@capitalthought.com'
15
+ # self.post_install_message = 'PostInstall.txt' # TODO remove post-install message not required
16
+ # self.rubyforge_name = self.name # TODO this is default value
17
+ # self.summary = %Q{File activity monitor for automatic time tracking.}
18
+ # self.description = %Q{Baby Bro monitors the timestamps changes for files in directories on your filesystem and records time spent actively working in those directories.}
19
+ # # self.extra_deps = [['activesupport','>= 2.0.2']]
20
+ #
21
+ # end
22
+ #
23
+ # require 'newgem/tasks'
24
+ # Dir['tasks/**/*.rake'].each { |t| load t }
25
+ #
26
+ # TODO - want other tests/tasks run by default? Add them to the list
27
+ # remove_task :default
28
+ # task :default => [:spec, :features]
29
+
30
+ begin
31
+ require 'jeweler'
32
+ Jeweler::Tasks.new do |gem|
33
+ gem.name = "baby-bro"
34
+ gem.summary = %Q{File activity monitor for time tracking.}
35
+ gem.description = %Q{Baby Bro monitors the timestamps changes for files in directories on your filesystem and records activity and estimates time spent actively working in those directories.}
36
+ gem.email = "billdoughty@capitalthought.com"
37
+ gem.homepage = "http://github.com/capitalthought/baby-bro"
38
+ gem.authors = ["Bill Doughty"]
39
+ gem.add_development_dependency "rspec", ">= 1.3.1"
40
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
41
+ end
42
+ Jeweler::GemcutterTasks.new
43
+ rescue LoadError
44
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
45
+ end
46
+
47
+ require 'rake/rdoctask'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "baby-bro #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,73 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{baby-bro}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Bill Doughty"]
12
+ s.date = %q{2010-12-07}
13
+ s.default_executable = %q{bro}
14
+ s.description = %q{Baby Bro monitors the timestamps changes for files in directories on your filesystem and records activity and estimates time spent actively working in those directories.}
15
+ s.email = %q{billdoughty@capitalthought.com}
16
+ s.executables = ["bro"]
17
+ s.extra_rdoc_files = [
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".gitignore",
22
+ "History.txt",
23
+ "Manifest.txt",
24
+ "PostInstall.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "baby-bro.gemspec",
29
+ "bin/bro",
30
+ "config/babybrorc.example",
31
+ "lib/baby-bro.rb",
32
+ "lib/baby-bro/base_config.rb",
33
+ "lib/baby-bro/exec.rb",
34
+ "lib/baby-bro/files.rb",
35
+ "lib/baby-bro/hash_object.rb",
36
+ "lib/baby-bro/monitor.rb",
37
+ "lib/baby-bro/monitor_config.rb",
38
+ "lib/baby-bro/project.rb",
39
+ "lib/baby-bro/reporter.rb",
40
+ "lib/baby-bro/session.rb",
41
+ "lib/extensions/fixnum.rb",
42
+ "script/console",
43
+ "script/destroy",
44
+ "script/generate",
45
+ "spec/baby-bro_spec.rb",
46
+ "spec/spec.opts",
47
+ "spec/spec_helper.rb",
48
+ "tasks/rspec.rake"
49
+ ]
50
+ s.homepage = %q{http://github.com/capitalthought/baby-bro}
51
+ s.rdoc_options = ["--charset=UTF-8"]
52
+ s.require_paths = ["lib"]
53
+ s.rubygems_version = %q{1.3.7}
54
+ s.summary = %q{File activity monitor for time tracking.}
55
+ s.test_files = [
56
+ "spec/baby-bro_spec.rb",
57
+ "spec/spec_helper.rb"
58
+ ]
59
+
60
+ if s.respond_to? :specification_version then
61
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
62
+ s.specification_version = 3
63
+
64
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
65
+ s.add_development_dependency(%q<rspec>, [">= 1.3.1"])
66
+ else
67
+ s.add_dependency(%q<rspec>, [">= 1.3.1"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<rspec>, [">= 1.3.1"])
71
+ end
72
+ end
73
+
data/bin/bro ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # The command line Haml parser.
3
+
4
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
5
+ require 'baby-bro'
6
+ require 'baby-bro/exec'
7
+
8
+ opts = BabyBroExec::Exec::Bro.new(ARGV)
9
+ opts.parse!
@@ -0,0 +1,10 @@
1
+ # Baby Bro Config
2
+ ---
3
+ :data_directory: ~/.babybro
4
+ :polling_interval: 1 minute
5
+ :idle_interval: 5 minutes
6
+ :projects:
7
+ - :name: Baby Bro
8
+ :directory: ~/src/capitalthought/baby-bro
9
+ - :name: Some Other Project
10
+ :directory: ~/src/capitalthought/sop
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+ $:.unshift(File.join(File.dirname(__FILE__), 'baby-bro')) unless $:.include?( $:.unshift(File.join(File.dirname(__FILE__), 'baby-bro')) )
3
+
4
+ module BabyBro
5
+ VERSION = '0.0.1'
6
+ end
7
+
8
+ require 'extensions/fixnum'
9
+ require 'baby-bro/hash_object'
10
+ require 'baby-bro/monitor'
11
+ require 'baby-bro/reporter'
@@ -0,0 +1,64 @@
1
+ module BabyBro
2
+ module BaseConfig
3
+ def self.included( base )
4
+ base.send(:include, Files)
5
+ end
6
+
7
+ def process_base_config( options )
8
+ @config_file = options[:config_file]
9
+ config = YAML.load( File.open( @config_file ) )
10
+ @last_config_update = file_timestamp( @config_file )
11
+ @projects = config[:projects]
12
+ @data_directory = config[:data][:directory]
13
+ raise "Data directory not specified" unless @data_directory
14
+ @data_directory.gsub!('~', ENV["HOME"])
15
+ # puts "Data Directory: #{@data_directory}"
16
+ config[:data][:pid_file] = File.join(@data_directory, ".pid")
17
+ raise "No projects specified" unless @projects
18
+ validate_projects( @projects )
19
+ puts "Config file #{@config_file} loaded."
20
+ options.merge(config)
21
+ end
22
+
23
+
24
+ def validate_projects( projects )
25
+ projects.each_with_index do |project, i|
26
+ raise "No name given for project #{i}" unless project[:name]
27
+ raise "No directory given for project #{project[:name]}" unless project[:directory]
28
+ project[:directory].gsub!('~', ENV["HOME"])
29
+ begin
30
+ Dir.entries( project[:directory] )
31
+ rescue
32
+ raise "Invalid directory #{project[:directory]} for project #{project[:name]}"
33
+ end
34
+ end
35
+ project_names = projects.map{|p| p[:name]}
36
+ project_dirs = projects.map{|p| p[:directory]}
37
+ dup_names = project_names - project_names.uniq
38
+ dup_dirs = project_dirs - project_dirs.uniq
39
+ raise "ERROR: Duplicate project name(s) not allowed: #{dup_names.join(", ")}" if dup_names.any?
40
+ raise "ERROR: Duplicate project directories(s) allowed: #{dup_dirs.join(", ")}" if dup_dirs.any?
41
+ end
42
+
43
+ def initialize_database
44
+ FileUtils.mkdir_p( @data_directory )
45
+ version_file = File.join(@data_directory, '.version')
46
+ unless File.exist?(version_file)
47
+ File.open(version_file, 'w') do |f|
48
+ f.write(::BabyBro::VERSION.to_s)
49
+ end
50
+ end
51
+ @projects.map!{|p| Project.new(p, @config)}
52
+ end
53
+
54
+ def base_config_changed
55
+ if @last_config_update > file_timestamp( @config_file )
56
+ puts "config new"
57
+ return true
58
+ else
59
+ return false
60
+ end
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,208 @@
1
+ require 'optparse'
2
+ require 'fileutils'
3
+ require 'pp'
4
+ require 'yaml'
5
+
6
+ module BabyBroExec
7
+ # This module handles the various BabyBro executables (`baby-bro`, etc).
8
+ module Exec
9
+ class Generic
10
+ # @param args [Array<String>] The command-line arguments
11
+ def initialize(args)
12
+ @args = args
13
+ @options = {}
14
+ end
15
+
16
+ # Parses the command-line arguments and runs the executable.
17
+ # Calls `Kernel#exit` at the end, so it never returns.
18
+ #
19
+ # @see #parse
20
+ def parse!
21
+ begin
22
+ parse
23
+ rescue Exception => e
24
+ raise e if @options[:tron] || e.is_a?(SystemExit) || true
25
+
26
+ $stderr.print "#{e.class}: " unless e.class == RuntimeError
27
+ $stderr.puts "#{e.message}"
28
+ $stderr.puts " Use --tron for stacktrace."
29
+ exit 1
30
+ end
31
+ exit 0
32
+ end
33
+
34
+ # Parses the command-line arguments and runs the executable.
35
+ # This does not handle exceptions or exit the program.
36
+ #
37
+ # @see #parse!
38
+ def parse
39
+ @opts = OptionParser.new(&method(:set_opts))
40
+ @opts.parse!(@args)
41
+
42
+ process_result
43
+
44
+ @options
45
+ end
46
+
47
+ # @return [String] A description of the executable
48
+ def to_s
49
+ @opts.to_s
50
+ end
51
+
52
+ protected
53
+
54
+ # Finds the line of the source template
55
+ # on which an exception was raised.
56
+ #
57
+ # @param exception [Exception] The exception
58
+ # @return [String] The line number
59
+ def get_line(exception)
60
+ # SyntaxErrors have weird line reporting
61
+ # when there's trailing whitespace,
62
+ # which there is for BabyBro documents.
63
+ return (exception.message.scan(/:(\d+)/).first || ["??"]).first if exception.is_a?(::SyntaxError)
64
+ (exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first
65
+ end
66
+
67
+ # Tells optparse how to parse the arguments
68
+ # available for all executables.
69
+ #
70
+ # This is meant to be overridden by subclasses
71
+ # so they can add their own options.
72
+ #
73
+ # @param opts [OptionParser]
74
+ def set_opts(opts)
75
+ opts.banner = <<END
76
+ Usage: bro [options] [command] [date]
77
+
78
+ Command is one of the following:
79
+
80
+ start - starts the monitor process in the background
81
+ stop - stops the monitor process
82
+ status - prints the status of the monitor process
83
+ restart - restarts the monitor process (forces re-reading of config file)
84
+ report - prints out time tracking reports
85
+
86
+ The date argument is optional and only used for the report command.
87
+ It must be a valid date string. When passed, the date argument will
88
+ cause reports to be printed for only that date.
89
+
90
+ END
91
+
92
+ @options[:config_file] = "#{ENV["HOME"]}/.babybrorc"
93
+ @options[:tron] = false
94
+ opts.on('-c', '--config FILE', "Use this config file. default is #{@options[:config_file]}") do |config_file|
95
+ @options[:config_file] = config_file
96
+ end
97
+
98
+ opts.on('-t', '--tron', :NONE, 'Trace on. Show debug output and a full stack trace on error') do
99
+ @options[:tron] = true
100
+ end
101
+
102
+ opts.on('-f', '--force', :NONE, 'Force starting of monitor when PID file is stale.') do
103
+ @options[:force_start] = true
104
+ end
105
+
106
+ opts.on_tail("-?", "-h", "--help", "Show this message") do
107
+ puts opts
108
+ exit
109
+ end
110
+
111
+ opts.on_tail("-v", "--version", "Print version") do
112
+ puts("BabyBro #{::BabyBro::VERSION}")
113
+ exit
114
+ end
115
+ end
116
+
117
+ # Processes the options set by the command-line arguments.
118
+ #
119
+ # This is meant to be overridden by subclasses
120
+ # so they can run their respective programs.
121
+ def process_result
122
+ end
123
+
124
+ COLORS = { :red => 31, :green => 32, :yellow => 33 }
125
+
126
+ # Prints a status message about performing the given action,
127
+ # colored using the given color (via terminal escapes) if possible.
128
+ #
129
+ # @param name [#to_s] A short name for the action being performed.
130
+ # Shouldn't be longer than 11 characters.
131
+ # @param color [Symbol] The name of the color to use for this action.
132
+ # Can be `:red`, `:green`, or `:yellow`.
133
+ def puts_action(name, color, arg)
134
+ printf color(color, "%11s %s\n"), name, arg
135
+ end
136
+
137
+ # Wraps the given string in terminal escapes
138
+ # causing it to have the given color.
139
+ # If terminal esapes aren't supported on this platform,
140
+ # just returns the string instead.
141
+ #
142
+ # @param color [Symbol] The name of the color to use.
143
+ # Can be `:red`, `:green`, or `:yellow`.
144
+ # @param str [String] The string to wrap in the given color.
145
+ # @return [String] The wrapped string.
146
+ def color(color, str)
147
+ raise "[BUG] Unrecognized color #{color}" unless COLORS[color]
148
+
149
+ # Almost any real Unix terminal will support color,
150
+ # so we just filter for Windows terms (which don't set TERM)
151
+ # and not-real terminals, which aren't ttys.
152
+ return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty?
153
+ return "\e[#{COLORS[color]}m#{str}\e[0m"
154
+ end
155
+
156
+ private
157
+
158
+ def open_file(filename, flag = 'r')
159
+ return if filename.nil?
160
+ flag = 'wb' if @options[:unix_newlines] && flag == 'w'
161
+ File.open(filename, flag)
162
+ end
163
+
164
+ def handle_load_error(err)
165
+ dep = err.message[/^no such file to load -- (.*)/, 1]
166
+ raise err if @options[:tron] || dep.nil? || dep.empty?
167
+ $stderr.puts <<MESSAGE
168
+ Required dependency #{dep} not found!
169
+ Run "gem install #{dep}" to get it.
170
+ Use --tron for stacktrace.
171
+ MESSAGE
172
+ exit 1
173
+ end
174
+ end
175
+
176
+ class Bro < Generic
177
+ # Processes the options set by the command-line arguments.
178
+ #
179
+ # This is meant to be overridden by subclasses
180
+ # so they can run their respective programs.
181
+ def process_result
182
+ args = @args.dup
183
+ command = args.shift
184
+ case command
185
+ when 'start', nil
186
+ monitor = ::BabyBro::Monitor.new( @options )
187
+ monitor.start
188
+ when 'stop'
189
+ monitor = ::BabyBro::Monitor.new( @options )
190
+ monitor.stop
191
+ when 'status'
192
+ monitor = ::BabyBro::Monitor.new( @options )
193
+ monitor.status
194
+ when 'restart'
195
+ monitor = ::BabyBro::Monitor.new( @options )
196
+ monitor.stop && monitor.start
197
+ when 'report'
198
+ reporter = ::BabyBro::Reporter.new( @options, args )
199
+ reporter.run
200
+ else
201
+ puts "Unknown command: #{command}"
202
+ end
203
+ end
204
+
205
+ end
206
+
207
+ end
208
+ end
@@ -0,0 +1,31 @@
1
+ module BabyBro
2
+ module Files
3
+
4
+ def file_timestamp( filename )
5
+ file = File.new(filename)
6
+ mtime = file.mtime
7
+ file.close
8
+ mtime
9
+ end
10
+
11
+ def touch_file( filename, time )
12
+ `touch -t #{time.strftime("%Y%m%d%H%M.%S")} #{filename}`
13
+ end
14
+
15
+ # returns files in the specified directory
16
+ def find_files( directory, pattern='*')
17
+ `find #{directory} -name "#{pattern}"`.split("\n").reject{|f| f==directory}
18
+ end
19
+
20
+ # returns files in the specified directory that are newer than the specified file
21
+ def find_files_newer_than_file( directory, filename )
22
+ `find #{directory} -newer #{filename}`.split("\n")
23
+ end
24
+
25
+ # returns files in the specified directory that are newer than the time expression
26
+ # time_interval_expression is in english, eg. "15 minutes"
27
+ def find_recent_files( directory, time_interval_expression )
28
+ `find '#{directory}' -newermt "#{time_interval_expression} ago"`.split("\n")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ class HashObject
2
+ def initialize hash_obj, no_exception_on_missing_key = false
3
+ @hash_obj = hash_obj
4
+ @no_exception_on_missing_key = no_exception_on_missing_key
5
+ end
6
+
7
+ def [] key
8
+ @hash_obj[key]
9
+ end
10
+
11
+ def keys
12
+ return @hash_obj.keys
13
+ end
14
+
15
+ def merge hash
16
+ HashObject.new(@hash_obj.merge( hash ))
17
+ end
18
+
19
+ def merge! hash
20
+ @hash_obj.merge!( hash )
21
+ self
22
+ end
23
+
24
+ def method_missing method, *args
25
+ key = method.to_s
26
+ if @hash_obj.keys.include? key
27
+ obj = @hash_obj[key]
28
+ obj = HashObject.new(obj) if obj.is_a? Hash
29
+ return obj
30
+ elsif @hash_obj.keys.include? key.to_sym
31
+ obj = @hash_obj[key.to_sym]
32
+ obj = HashObject.new(obj) if obj.is_a? Hash
33
+ return obj
34
+ elsif matches = key.match( /(\w*)=/ )
35
+ key = matches[1].to_sym
36
+ @hash_obj[key]=*args
37
+ else
38
+ raise "No field in Hash object: #{key}" unless @no_exception_on_missing_key
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,140 @@
1
+ %w(project base_config monitor_config).each do |file|
2
+ require File.join(File.dirname(__FILE__),file)
3
+ end
4
+
5
+ module BabyBro
6
+ class Monitor
7
+ include BaseConfig
8
+ include MonitorConfig
9
+ attr_accessor :data_directory, :projects, :config
10
+
11
+ def initialize( options )
12
+ load_config( options )
13
+ end
14
+
15
+ def start
16
+ if pid = active_pid && !( @config.force_start )
17
+ puts "ERROR: PID file detected. Cannot start baby-bro."
18
+ puts "Check if process #{pid} is running."
19
+ puts "If it is not, delete #{pid_file} and try starting baby-bro again."
20
+ return false
21
+ end
22
+ @continue = true
23
+ @previous_SIGINT_handler = Kernel.trap( "SIGINT" ) {}
24
+ Kernel.trap( "SIGINT" ) do
25
+ print "Baby Bro monitor is shutting down..."
26
+ $stdout.flush
27
+ @continue = false;
28
+ if @previous_SIGINT_handler != "DEFAULT" && @previous_SIGINT_handler != "IGNORE"
29
+ Kernel.trap( "SIGINT" ) { @previous_SIGINT_handler.call }
30
+ end
31
+ end
32
+ pid = Process.fork
33
+ if pid.nil? then
34
+ # In child
35
+ main
36
+ else
37
+ # In parent
38
+ Process.detach(pid)
39
+ create_pid_file( pid )
40
+ puts "Baby Bro monitor started."
41
+ end
42
+ return true
43
+ end
44
+
45
+ def stop
46
+ unless pid = active_pid
47
+ puts "ERROR: No pid file found for Baby Bro (#{pid_file})."
48
+ puts "If Baby Bro monitor is running, you need to kill it manually."
49
+ return false
50
+ end
51
+ begin
52
+ puts "Sending SIGINT to Baby Bro monitor process #{pid}."
53
+ Process.kill( "SIGINT", pid )
54
+ rescue Errno::ESRCH
55
+ puts "No Baby Bro monitor process found with PID #{pid}."
56
+ puts "Removing PID file #{pid_file}."
57
+ remove_pid_file
58
+ end
59
+ sleep_time = 10.0
60
+ while( true )
61
+ begin
62
+ Process.kill( 0, pid ) # check if the process is still alive, raises Errno::ESRCH if not
63
+ sleep 0.1
64
+ sleep_time -= 0.1
65
+ if sleep_time == 0
66
+ Process.kill( "SIGKILL", pid )
67
+ puts "Baby Bro monitor process #{pid} not responding to SIGINT."
68
+ puts "Sending SIGKILL to Baby Bro monitor process #{pid}."
69
+ end
70
+ rescue Errno::ESRCH
71
+ break
72
+ end
73
+ end
74
+ puts "Baby Bro monitor terminated."
75
+ return true
76
+ end
77
+
78
+ def status
79
+ if pid = active_pid
80
+ begin
81
+ Process.kill( 0, pid ) # check if the process is still alive, raises Errno::ESRCH if not
82
+ puts "Baby Bro monitor process is running with PID #{pid}."
83
+ rescue Errno::ESRCH
84
+ puts "PID file #{pidfile} found, but no Baby Bro monitor process is running with PID #{pid}."
85
+ remove_pid_file
86
+ puts "PID file removed."
87
+ end
88
+ else
89
+ puts "Baby Bro monitor process is not running."
90
+ end
91
+ end
92
+
93
+ private
94
+ def main
95
+ while( @continue )
96
+ load_config( @config ) if base_config_changed
97
+ self.projects.each do |project|
98
+ # tron "Polling #{project.name}: #{project.directory}"
99
+ project.log_activity
100
+ end
101
+ sleep @polling_interval
102
+ end
103
+ sleep 5
104
+ remove_pid_file
105
+ puts "complete."
106
+ end
107
+
108
+ def active_pid
109
+ File.exist?( pid_file ) && File.read(pid_file).to_i
110
+ end
111
+
112
+ def pid_file
113
+ self.config.data.pid_file
114
+ end
115
+
116
+ def remove_pid_file
117
+ File.delete( pid_file )
118
+ end
119
+
120
+ def create_pid_file( pid )
121
+ File.open( pid_file, 'w' ) do |f|
122
+ f.write( pid.to_s )
123
+ end
124
+ end
125
+
126
+ def load_config( options )
127
+ @config = HashObject.new( process_base_config( options ), true )
128
+ process_monitor_config( @config )
129
+ initialize_database
130
+ end
131
+
132
+ def tron string
133
+ $stdout.puts if @config && @config.tron
134
+ end
135
+
136
+ def tron string
137
+ $stdout.puts if @config && @config.tron
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,31 @@
1
+ module BabyBro
2
+ module MonitorConfig
3
+ def process_monitor_config( config )
4
+ @polling_interval = eval(config[:monitor][:polling_interval].gsub(/\s/, '.')) || 5
5
+ end
6
+
7
+ def validate_projects( projects )
8
+ projects.each_with_index do |project, i|
9
+ raise "No name given for project #{i}" unless project[:name]
10
+ raise "No directory given for project #{project[:name]}" unless project[:directory]
11
+ project[:directory].gsub!('~', ENV["HOME"])
12
+ begin
13
+ Dir.entries( project[:directory] )
14
+ rescue
15
+ raise "Invalid directory #{project[:directory]} for project #{project[:name]}"
16
+ end
17
+ end
18
+ project_names = projects.map{|p| p[:name]}
19
+ project_dirs = projects.map{|p| p[:directory]}
20
+ dup_names = project_names - project_names.uniq
21
+ dup_dirs = project_dirs - project_dirs.uniq
22
+ raise "ERROR: Duplicate project name(s) not allowed: #{dup_names.join(", ")}" if dup_names.any?
23
+ raise "ERROR: Duplicate project directories(s) allowed: #{dup_dirs.join(", ")}" if dup_dirs.any?
24
+ end
25
+
26
+ def initialize_databases
27
+ @projects.map!{|p| Project.new(p, config)}
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,85 @@
1
+ %w(files session).each do |file|
2
+ require File.join(File.dirname(__FILE__),file)
3
+ end
4
+
5
+ module BabyBro
6
+ class Project < HashObject
7
+ attr_accessor :config
8
+ include Files
9
+
10
+ def initialize( hash, config )
11
+ super hash
12
+ @config = config
13
+ self.data_dir = File.join( config.data.directory, self.name.gsub(' ', '_') )
14
+ FileUtils.mkdir_p( self.data_dir )
15
+ self.last_checked_file = File.join( self.data_dir, "last_checked" )
16
+ FileUtils.touch( self.last_checked_file ) unless File.exist?( self.last_checked_file )
17
+ self.monitor_start_file = File.join( self.data_dir, "monitor_start" )
18
+ FileUtils.touch( self.monitor_start_file ) unless File.exist?( self.monitor_start_file )
19
+ self.reports_dir = File.join( self.data_dir, "reports" )
20
+ FileUtils.mkdir_p( self.reports_dir )
21
+ self.sessions_dir = File.join( self.data_dir, "sessions" )
22
+ FileUtils.mkdir_p( self.sessions_dir )
23
+ end
24
+
25
+ def last_checked
26
+ file_timestamp self.last_checked_file
27
+ end
28
+
29
+ def update_last_checked ( time=Time.now )
30
+ touch_file( self.last_checked_file, time )
31
+ end
32
+
33
+ def get_updated_files
34
+ files = find_files_newer_than_file(self.directory, self.last_checked_file)
35
+ files
36
+ end
37
+
38
+ def find_active_session
39
+ session_files = find_recent_files(self.sessions_dir, self.config.monitor.idle_interval)
40
+ session_files = session_files.reject{|e| e.strip!;e.nil? || e=="" || e == self.sessions_dir}
41
+ if session_files.length > 1
42
+ session_files.sort!
43
+ end
44
+ Session.load_session(session_files.last) if session_files.any?
45
+ end
46
+
47
+ def sessions
48
+ session_files = find_files( self.sessions_dir )
49
+ session_files.sort.map{|f| Session.load_session(f)}
50
+ end
51
+
52
+ def log_activity
53
+ check_time = Time.now
54
+ updated_files = self.get_updated_files
55
+ updated_files.each do |file|
56
+ tron file
57
+ end
58
+ if updated_files.any?
59
+ process_activity( check_time )
60
+ end
61
+ update_last_checked( check_time-1 )
62
+ end
63
+
64
+ def process_activity( check_time )
65
+ if session = find_active_session
66
+ session.update_activity( check_time )
67
+ if session.start_date < Date.today # start a new session for today
68
+ session = Session.create_session( check_time, self.sessions_dir )
69
+ end
70
+ else
71
+ session = Session.create_session( check_time, self.sessions_dir )
72
+ end
73
+ tron "#{self.name} Session Activity: "
74
+ tron " start_time: #{session.start_time}"
75
+ tron " duration: #{session.duration_in_english}"
76
+ end
77
+
78
+ private
79
+ def tron string
80
+ if config.tron
81
+ $stdout.puts string
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,49 @@
1
+ %w(project base_config).each do |file|
2
+ require File.join(File.dirname(__FILE__),file)
3
+ end
4
+
5
+ module BabyBro
6
+ class Reporter
7
+ include BaseConfig
8
+ attr_accessor :data_directory, :projects, :config
9
+
10
+ def initialize( options, args )
11
+ @config = HashObject.new( process_base_config( options ) )
12
+ process_reporting_config( @config )
13
+ initialize_database
14
+ end
15
+
16
+ def run
17
+ @projects.each do |project|
18
+ print_project_report( project )
19
+ end
20
+ end
21
+
22
+ private
23
+ def process_reporting_config( config )
24
+ end
25
+
26
+ def print_project_report( project, date=nil )
27
+ $stdout.puts
28
+ $stdout.puts "#{project.name}"
29
+ $stdout.puts "="*project.name.size
30
+ cumulative_time = 0
31
+ sessions = project.sessions
32
+ if sessions.any?
33
+ sessions_by_date = sessions.group_by(&:start_date)
34
+ sessions_by_date.keys.sort.each do |date|
35
+ sessions = sessions_by_date[date].sort
36
+ $stdout.puts " #{date.strftime("%Y-%m-%d")}"
37
+ sessions.each do |session|
38
+ $stdout.puts " #{session.start_time.strftime("%I:%M %p")} - #{session.duration_in_english}"
39
+ cumulative_time += session.duration
40
+ end
41
+ $stdout.puts " Total: #{Session.duration_in_english(sessions.inject(0){|sum,n| sum = sum+n.duration})}"
42
+ end
43
+ $stdout.puts "Grand Total: #{Session.duration_in_english(cumulative_time)}"
44
+ else
45
+ $stdout.puts " No sessions for this project."
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,93 @@
1
+ module BabyBro
2
+ class Session
3
+ include Files
4
+ attr_accessor :start_time, :start_date
5
+
6
+ def self.create_session( time, dirname )
7
+ session = Session.new( time, dirname )
8
+ end
9
+
10
+ def self.load_session( session_filename )
11
+ Session.new( session_filename )
12
+ end
13
+
14
+ def update_activity( time )
15
+ touch_file( self.filename, time )
16
+ end
17
+
18
+ def filename
19
+ basename = @start_time.strftime("%Y-%m-%d_%H:%M:%S")
20
+ File.join(@dirname, basename)
21
+ end
22
+
23
+ def last_activity
24
+ file_timestamp(self.filename)
25
+ end
26
+
27
+ def destroy
28
+ File.delete( self.filename )
29
+ end
30
+
31
+ def duration
32
+ duration = last_activity - @start_time
33
+ duration < 0 ? 0 : duration
34
+ end
35
+
36
+ def duration_in_english
37
+ Session.duration_in_english( self.duration )
38
+ end
39
+
40
+ def self.duration_in_english( duration )
41
+ time = []
42
+ time_duration = duration
43
+ days = hours = minutes = seconds = 0
44
+ if time_duration > 1.day
45
+ days = (time_duration / 1.day).to_i
46
+ time_duration -= days.days
47
+ time << "#{days}d" if days != 0
48
+ end
49
+ if time_duration > 1.hour
50
+ hours = (time_duration / 1.hour).to_i
51
+ time_duration -= hours.hours
52
+ time << "#{hours}h" if hours != 0
53
+ end
54
+ if time_duration > 1.minute
55
+ minutes = (time_duration / 1.minute).to_i
56
+ time_duration -= minutes.minutes
57
+ time << "#{minutes}m"
58
+ end
59
+ time << "#{time_duration.to_i}s"
60
+ breakdown = time.join(' ')
61
+ output = "#{"%05.2f" % (duration/1.hour)} hours or #{breakdown}"
62
+ end
63
+
64
+ def <=> b
65
+ self.start_date <=> b.start_date
66
+ end
67
+
68
+ private
69
+ def initialize( time_or_session_filename, dirname=nil )
70
+ if time_or_session_filename.is_a? Time
71
+ @start_time = time_or_session_filename
72
+ @start_date = Date.civil( @start_time.year, @start_time.month, @start_time.day )
73
+ @dirname = dirname
74
+ self.update_activity( @start_time )
75
+ elsif time_or_session_filename.is_a? String
76
+ date_string = File.basename( time_or_session_filename )
77
+ @dirname = File.dirname( time_or_session_filename )
78
+ date_parts, time_parts = date_string.split('_')
79
+ hour, minutes, seconds = time_parts.split(':').map(&:to_i)
80
+ year, month, day = date_parts.split('-').map(&:to_i)
81
+ @start_time = Time.local( year, month, day, hour, minutes, seconds )
82
+ @start_date = Date.civil( year, month, day )
83
+ unless self.filename == time_or_session_filename
84
+ puts "filename: #{self.filename}"
85
+ puts "time_or_session_filename: #{time_or_session_filename}"
86
+ raise "bad filename for time"
87
+ end
88
+ else
89
+ raise "Unknown Session initializer"
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,18 @@
1
+ class Fixnum
2
+ def seconds
3
+ self
4
+ end
5
+ alias :second :seconds
6
+ def minutes
7
+ self * 60
8
+ end
9
+ alias :minute :minutes
10
+ def hours
11
+ self * 60 * 60
12
+ end
13
+ alias :hour :hours
14
+ def days
15
+ self * 60 * 60 * 24
16
+ end
17
+ alias :day :days
18
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/baby-bro.rb'}"
9
+ puts "Loading baby-bro gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ # Time to add your specs!
4
+ # http://rspec.info/
5
+ describe "Place your specs here" do
6
+
7
+ it "find this spec in spec directory" do
8
+ # violated "Be sure to write your specs"
9
+ end
10
+
11
+ end
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,10 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
5
+ gem 'rspec'
6
+ require 'spec'
7
+ end
8
+
9
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
10
+ require 'baby-bro'
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
5
+ require 'spec'
6
+ end
7
+ begin
8
+ require 'spec/rake/spectask'
9
+ rescue LoadError
10
+ puts <<-EOS
11
+ To use rspec for testing you must install rspec gem:
12
+ gem install rspec
13
+ EOS
14
+ exit(0)
15
+ end
16
+
17
+ desc "Run the specs under spec/models"
18
+ Spec::Rake::SpecTask.new do |t|
19
+ t.spec_opts = ['--options', "spec/spec.opts"]
20
+ t.spec_files = FileList['spec/**/*_spec.rb']
21
+ end
22
+
23
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.pattern = 'spec/**/*_spec.rb'
26
+ spec.rcov = true
27
+ end
28
+
29
+ task :spec => :check_dependencies
30
+
31
+ task :default => :spec
32
+
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: baby-bro
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Bill Doughty
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-12-07 00:00:00 -06:00
19
+ default_executable: bro
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 25
30
+ segments:
31
+ - 1
32
+ - 3
33
+ - 1
34
+ version: 1.3.1
35
+ type: :development
36
+ version_requirements: *id001
37
+ description: Baby Bro monitors the timestamps changes for files in directories on your filesystem and records activity and estimates time spent actively working in those directories.
38
+ email: billdoughty@capitalthought.com
39
+ executables:
40
+ - bro
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - README.rdoc
45
+ files:
46
+ - .gitignore
47
+ - History.txt
48
+ - Manifest.txt
49
+ - PostInstall.txt
50
+ - README.rdoc
51
+ - Rakefile
52
+ - VERSION
53
+ - baby-bro.gemspec
54
+ - bin/bro
55
+ - config/babybrorc.example
56
+ - lib/baby-bro.rb
57
+ - lib/baby-bro/base_config.rb
58
+ - lib/baby-bro/exec.rb
59
+ - lib/baby-bro/files.rb
60
+ - lib/baby-bro/hash_object.rb
61
+ - lib/baby-bro/monitor.rb
62
+ - lib/baby-bro/monitor_config.rb
63
+ - lib/baby-bro/project.rb
64
+ - lib/baby-bro/reporter.rb
65
+ - lib/baby-bro/session.rb
66
+ - lib/extensions/fixnum.rb
67
+ - script/console
68
+ - script/destroy
69
+ - script/generate
70
+ - spec/baby-bro_spec.rb
71
+ - spec/spec.opts
72
+ - spec/spec_helper.rb
73
+ - tasks/rspec.rake
74
+ has_rdoc: true
75
+ homepage: http://github.com/capitalthought/baby-bro
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options:
80
+ - --charset=UTF-8
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ hash: 3
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ requirements: []
102
+
103
+ rubyforge_project:
104
+ rubygems_version: 1.3.7
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: File activity monitor for time tracking.
108
+ test_files:
109
+ - spec/baby-bro_spec.rb
110
+ - spec/spec_helper.rb