baby-bro 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/History.txt +4 -0
- data/Manifest.txt +26 -0
- data/PostInstall.txt +5 -0
- data/README.rdoc +100 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/baby-bro.gemspec +73 -0
- data/bin/bro +9 -0
- data/config/babybrorc.example +10 -0
- data/lib/baby-bro.rb +11 -0
- data/lib/baby-bro/base_config.rb +64 -0
- data/lib/baby-bro/exec.rb +208 -0
- data/lib/baby-bro/files.rb +31 -0
- data/lib/baby-bro/hash_object.rb +41 -0
- data/lib/baby-bro/monitor.rb +140 -0
- data/lib/baby-bro/monitor_config.rb +31 -0
- data/lib/baby-bro/project.rb +85 -0
- data/lib/baby-bro/reporter.rb +49 -0
- data/lib/baby-bro/session.rb +93 -0
- data/lib/extensions/fixnum.rb +18 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/baby-bro_spec.rb +11 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/tasks/rspec.rake +32 -0
- metadata +110 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg
|
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -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
|
data/PostInstall.txt
ADDED
data/README.rdoc
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/baby-bro.gemspec
ADDED
@@ -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
data/lib/baby-bro.rb
ADDED
@@ -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
|
data/script/console
ADDED
@@ -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"
|
data/script/destroy
ADDED
@@ -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)
|
data/script/generate
ADDED
@@ -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)
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
data/tasks/rspec.rake
ADDED
@@ -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
|