watch_tower 0.0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.todo +33 -0
- data/.travis.yml +14 -0
- data/Gemfile +43 -0
- data/Guardfile +25 -0
- data/MIT-LICENSE +20 -0
- data/README.md +38 -0
- data/Rakefile +8 -0
- data/TODO +17 -0
- data/bin/watchtower +10 -0
- data/ci/adapters/jruby-mysql.yml +8 -0
- data/ci/adapters/jruby-postgresql.yml +6 -0
- data/ci/adapters/jruby-sqlite.yml +6 -0
- data/ci/adapters/ruby-mysql.yml +8 -0
- data/ci/adapters/ruby-postgresql.yml +6 -0
- data/ci/adapters/ruby-sqlite.yml +6 -0
- data/ci/travis.rb +102 -0
- data/lib/watch_tower.rb +60 -0
- data/lib/watch_tower/appscript.rb +22 -0
- data/lib/watch_tower/cli.rb +15 -0
- data/lib/watch_tower/cli/.gitkeep +0 -0
- data/lib/watch_tower/cli/install.rb +63 -0
- data/lib/watch_tower/cli/open.rb +24 -0
- data/lib/watch_tower/cli/start.rb +140 -0
- data/lib/watch_tower/config.rb +38 -0
- data/lib/watch_tower/core_ext.rb +4 -0
- data/lib/watch_tower/core_ext/.gitkeep +0 -0
- data/lib/watch_tower/editor.rb +17 -0
- data/lib/watch_tower/editor/.gitkeep +0 -0
- data/lib/watch_tower/editor/base_appscript.rb +34 -0
- data/lib/watch_tower/editor/base_ps.rb +6 -0
- data/lib/watch_tower/editor/textmate.rb +17 -0
- data/lib/watch_tower/editor/xcode.rb +22 -0
- data/lib/watch_tower/errors.rb +25 -0
- data/lib/watch_tower/eye.rb +79 -0
- data/lib/watch_tower/project.rb +14 -0
- data/lib/watch_tower/project/.gitkeep +0 -0
- data/lib/watch_tower/project/any_based.rb +22 -0
- data/lib/watch_tower/project/git_based.rb +86 -0
- data/lib/watch_tower/project/init.rb +38 -0
- data/lib/watch_tower/project/path_based.rb +144 -0
- data/lib/watch_tower/server.rb +62 -0
- data/lib/watch_tower/server/.gitkeep +0 -0
- data/lib/watch_tower/server/app.rb +37 -0
- data/lib/watch_tower/server/assets/images/WatchTower.jpg +0 -0
- data/lib/watch_tower/server/assets/images/percentage.png +0 -0
- data/lib/watch_tower/server/assets/javascripts/application.js +3 -0
- data/lib/watch_tower/server/assets/javascripts/percentage.coffee +8 -0
- data/lib/watch_tower/server/assets/stylesheets/application.css +7 -0
- data/lib/watch_tower/server/assets/stylesheets/global.sass +71 -0
- data/lib/watch_tower/server/assets/stylesheets/project.sass +59 -0
- data/lib/watch_tower/server/configurations.rb +10 -0
- data/lib/watch_tower/server/configurations/asset.rb +43 -0
- data/lib/watch_tower/server/database.rb +105 -0
- data/lib/watch_tower/server/db/migrate/001_create_projects.rb +15 -0
- data/lib/watch_tower/server/db/migrate/002_create_files.rb +16 -0
- data/lib/watch_tower/server/db/migrate/003_create_time_entries.rb +12 -0
- data/lib/watch_tower/server/db/migrate/004_create_durations.rb +14 -0
- data/lib/watch_tower/server/db/migrate/005_add_hash_to_time_entries.rb +6 -0
- data/lib/watch_tower/server/db/migrate/006_add_hash_to_files.rb +6 -0
- data/lib/watch_tower/server/decorator.rb +21 -0
- data/lib/watch_tower/server/decorator/application_decorator.rb +91 -0
- data/lib/watch_tower/server/decorator/file_decorator.rb +38 -0
- data/lib/watch_tower/server/decorator/project_decorator.rb +51 -0
- data/lib/watch_tower/server/helpers.rb +13 -0
- data/lib/watch_tower/server/helpers/asset.rb +29 -0
- data/lib/watch_tower/server/helpers/improved_partials.rb +41 -0
- data/lib/watch_tower/server/models/duration.rb +11 -0
- data/lib/watch_tower/server/models/file.rb +31 -0
- data/lib/watch_tower/server/models/project.rb +17 -0
- data/lib/watch_tower/server/models/time_entry.rb +64 -0
- data/lib/watch_tower/server/public/assets/WatchTower-4d6de11e1bd34165ad91ac46fb711bf3.jpg +0 -0
- data/lib/watch_tower/server/public/assets/application-7829b53b5ece1a16d22dc3d00f329023.css +107 -0
- data/lib/watch_tower/server/public/assets/application-e0e6b7731aade460f680331e65cf0682.js +9359 -0
- data/lib/watch_tower/server/public/assets/percentage-d8589e21a5fc85d32a445f531ff8ab95.png +0 -0
- data/lib/watch_tower/server/vendor/assets/javascripts/jquery-ui.js +11729 -0
- data/lib/watch_tower/server/vendor/assets/javascripts/jquery.js +8981 -0
- data/lib/watch_tower/server/vendor/assets/javascripts/jquery_ujs.js +363 -0
- data/lib/watch_tower/server/views/.gitkeep +0 -0
- data/lib/watch_tower/server/views/_file.haml +9 -0
- data/lib/watch_tower/server/views/_project.haml +13 -0
- data/lib/watch_tower/server/views/index.haml +7 -0
- data/lib/watch_tower/server/views/layout.haml +32 -0
- data/lib/watch_tower/server/views/project.haml +12 -0
- data/lib/watch_tower/templates/config.yml +146 -0
- data/lib/watch_tower/templates/watchtower.plist +23 -0
- data/lib/watch_tower/version.rb +8 -0
- data/spec/factories.rb +45 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/active_record.rb +44 -0
- data/spec/support/factory_girl.rb +6 -0
- data/spec/support/launchy.rb +3 -0
- data/spec/support/sinatra.rb +10 -0
- data/spec/support/timecop.rb +7 -0
- data/spec/watch_tower/appscript_spec.rb +6 -0
- data/spec/watch_tower/cli/install_spec.rb +16 -0
- data/spec/watch_tower/cli/open_spec.rb +14 -0
- data/spec/watch_tower/cli/start_spec.rb +17 -0
- data/spec/watch_tower/cli_spec.rb +15 -0
- data/spec/watch_tower/config_spec.rb +25 -0
- data/spec/watch_tower/editor/textmate_spec.rb +43 -0
- data/spec/watch_tower/editor/xcode_spec.rb +43 -0
- data/spec/watch_tower/editor_spec.rb +19 -0
- data/spec/watch_tower/eye_spec.rb +130 -0
- data/spec/watch_tower/project/git_based_spec.rb +131 -0
- data/spec/watch_tower/project/path_based_spec.rb +111 -0
- data/spec/watch_tower/project_spec.rb +82 -0
- data/spec/watch_tower/server/app_spec.rb +186 -0
- data/spec/watch_tower/server/decorator/project_decorator_spec.rb +60 -0
- data/spec/watch_tower/server/models/file_spec.rb +284 -0
- data/spec/watch_tower/server/models/project_spec.rb +165 -0
- data/spec/watch_tower/server/models/time_entry_spec.rb +37 -0
- data/spec/watch_tower/server_spec.rb +4 -0
- data/watch_tower.gemspec +80 -0
- metadata +450 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
require 'appscript'
|
4
|
+
rescue LoadError
|
5
|
+
require 'rbconfig'
|
6
|
+
if RbConfig::CONFIG['target_os'] =~ /darwin/i
|
7
|
+
STDERR.puts "Please install 'appscript' to use this gem with Textmate"
|
8
|
+
STDERR.puts "gem install appscript"
|
9
|
+
end
|
10
|
+
# Define a simple class so the gem works even if Appscript is not installed
|
11
|
+
module ::Appscript
|
12
|
+
class Application
|
13
|
+
def is_running?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def app(*args)
|
19
|
+
Application.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
File without changes
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module WatchTower
|
2
|
+
module CLI
|
3
|
+
module Install
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval <<-END, __FILE__, __LINE__ + 1
|
12
|
+
# This module needs Thor::Actions
|
13
|
+
include ::Thor::Actions
|
14
|
+
|
15
|
+
# Mappings (aliases)
|
16
|
+
map "-i" => :install
|
17
|
+
|
18
|
+
# Install WatchTower
|
19
|
+
desc "install", "Install Watch Tower"
|
20
|
+
def install
|
21
|
+
# Install the configuration file
|
22
|
+
install_config_file
|
23
|
+
# Install the bootloader
|
24
|
+
install_bootloader
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
# Install the configuration file
|
29
|
+
def install_config_file
|
30
|
+
self.class.source_root(TEMPLATE_PATH)
|
31
|
+
copy_file 'config.yml', File.join(USER_PATH, 'config.yml')
|
32
|
+
end
|
33
|
+
|
34
|
+
# Install bootloader
|
35
|
+
def install_bootloader
|
36
|
+
require 'rbconfig'
|
37
|
+
case RbConfig::CONFIG['target_os']
|
38
|
+
when /darwin/
|
39
|
+
install_bootloader_on_mac
|
40
|
+
else
|
41
|
+
puts <<-MSG
|
42
|
+
WatchTower bootloader is not supported on your OS, you'd have to run it manually
|
43
|
+
for the time being. Support for many editors and many OSes is planned for the
|
44
|
+
future, if you would like to help, or drop in an issue please don't hesitate to
|
45
|
+
do so on the Github page: https://github.com/TechnoGate/watch_tower
|
46
|
+
MSG
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Install bootloader on Mac OS X
|
51
|
+
def install_bootloader_on_mac
|
52
|
+
self.class.source_root(TEMPLATE_PATH)
|
53
|
+
copy_file 'watchtower.plist', File.join(ENV['HOME'], 'Library',
|
54
|
+
'LaunchAgents', 'fr.technogate.WatchTower.plist')
|
55
|
+
|
56
|
+
puts "\nCreated. Now run:\n launchctl load ~/Library/LaunchAgents/fr.technogate.WatchTower.plist\n\n"
|
57
|
+
end
|
58
|
+
END
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module WatchTower
|
2
|
+
module CLI
|
3
|
+
module Open
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval <<-END, __FILE__, __LINE__ + 1
|
12
|
+
# Open the WatchTower server in the browser
|
13
|
+
#
|
14
|
+
# TODO: Should be able to determine the port of the server.
|
15
|
+
desc "open", "Open the WatchTower in the browser"
|
16
|
+
def open
|
17
|
+
system "open http://localhost:9282"
|
18
|
+
end
|
19
|
+
END
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module WatchTower
|
2
|
+
module CLI
|
3
|
+
module Start
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval <<-END, __FILE__, __LINE__ + 1
|
12
|
+
# Mappings (aliases)
|
13
|
+
map "-s" => :start
|
14
|
+
|
15
|
+
# Start watchtower
|
16
|
+
desc "start", "Start the Watch Tower"
|
17
|
+
method_option :bootloader,
|
18
|
+
type: :boolean,
|
19
|
+
required: false,
|
20
|
+
aliases: "-b",
|
21
|
+
default: false,
|
22
|
+
desc: "Is it invoked from the bootloader?"
|
23
|
+
method_option :foreground,
|
24
|
+
type: :boolean,
|
25
|
+
required: false,
|
26
|
+
aliases: "-f",
|
27
|
+
default: false,
|
28
|
+
desc: "Do not run in the background."
|
29
|
+
method_option :host,
|
30
|
+
type: :string,
|
31
|
+
required: false,
|
32
|
+
aliases: "-h",
|
33
|
+
default: 'localhost',
|
34
|
+
desc: "Set the server's host"
|
35
|
+
method_option :port,
|
36
|
+
type: :numeric,
|
37
|
+
required: false,
|
38
|
+
aliases: "-p",
|
39
|
+
default: 9282,
|
40
|
+
desc: "Set the server's port."
|
41
|
+
def start
|
42
|
+
if Config[:enabled] &&
|
43
|
+
(!options[:bootloader] || (options(:bootloader) && Config[:launch_on_boot]))
|
44
|
+
LOG.info "Starting WatchTower."
|
45
|
+
start!
|
46
|
+
else
|
47
|
+
abort "You need to edit the config file located at #{Config::CONFIG_FILE}."
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
def start!
|
53
|
+
if options[:foreground]
|
54
|
+
LOG.debug "#{__FILE__}:#{__LINE__}: Running WatchTower in foreground."
|
55
|
+
|
56
|
+
# Start WatchTower
|
57
|
+
start_watch_tower
|
58
|
+
|
59
|
+
LOG.debug "#{__FILE__}:#{__LINE__}: WatchTower has finished."
|
60
|
+
else
|
61
|
+
LOG.debug "#{__FILE__}:#{__LINE__}: Running WatchTower in the background."
|
62
|
+
pid = fork do
|
63
|
+
# Try to replace ruby with WatchTower in the command line (for ps)
|
64
|
+
$0 = 'watchtower' unless $0 == 'watchtower'
|
65
|
+
|
66
|
+
# Tell ruby that we are a daemon
|
67
|
+
Process.daemon
|
68
|
+
|
69
|
+
# Start WatchTower
|
70
|
+
start_watch_tower
|
71
|
+
|
72
|
+
LOG.debug "#{__FILE__}:#{__LINE__}: WatchTower has finished."
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Start watch tower
|
78
|
+
# This method just start the watch tower it doesn't know
|
79
|
+
# or care if we are in a forked process or not, all it cares about
|
80
|
+
# is starting the database server before starting the eye
|
81
|
+
#
|
82
|
+
# see #start_server
|
83
|
+
# see #start_eye
|
84
|
+
def start_watch_tower
|
85
|
+
# Start the server
|
86
|
+
start_server
|
87
|
+
|
88
|
+
# Wait until the database starts
|
89
|
+
until Server::Database.is_connected? do
|
90
|
+
sleep(1)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Wait until the database has migrated
|
94
|
+
until Server::Database.is_migrated? do
|
95
|
+
sleep(1)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Start the eye now.
|
99
|
+
start_eye
|
100
|
+
end
|
101
|
+
|
102
|
+
# Start the eye
|
103
|
+
# This method just start the watch tower it doesn't know
|
104
|
+
# or care if we are in a forked process or not
|
105
|
+
def start_eye
|
106
|
+
LOG.debug "#{__FILE__}:#{__LINE__}: Starting the eye."
|
107
|
+
Eye.start!(watch_tower_options)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Start the web server
|
111
|
+
# This method just start the watch tower it doesn't know
|
112
|
+
# or care if we are in a forked process or not
|
113
|
+
def start_server
|
114
|
+
LOG.debug "#{__FILE__}:#{__LINE__}: Starting the web server."
|
115
|
+
Server.start!(watch_tower_options)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return Watch Tower options
|
119
|
+
# same as options but modified to correspond to WatchTower options
|
120
|
+
# instead of CLI options
|
121
|
+
#
|
122
|
+
# @return [Hash] options
|
123
|
+
def watch_tower_options
|
124
|
+
return @watch_tower_options if @watch_tower_options
|
125
|
+
|
126
|
+
@watch_tower_options = options.dup
|
127
|
+
@watch_tower_options.delete(:bootloader)
|
128
|
+
|
129
|
+
# Log the options as a Debug
|
130
|
+
LOG.debug "#{__FILE__}:#{__LINE__}: Options are \#{@watch_tower_options.inspect}."
|
131
|
+
|
132
|
+
# Return the options
|
133
|
+
@watch_tower_options
|
134
|
+
end
|
135
|
+
END
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_support/hash_with_indifferent_access'
|
2
|
+
|
3
|
+
module WatchTower
|
4
|
+
module Config
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Define the config file's path
|
8
|
+
CONFIG_FILE = File.join(USER_PATH, 'config.yml')
|
9
|
+
|
10
|
+
# Define the config class variable
|
11
|
+
@@config = nil
|
12
|
+
|
13
|
+
# Return a particular config variable from the parsed config file
|
14
|
+
#
|
15
|
+
# @param [String|Symbol] config
|
16
|
+
# @return mixed
|
17
|
+
# @raise [Void]
|
18
|
+
def [](config)
|
19
|
+
ensure_config_file_exists
|
20
|
+
@@config ||= HashWithIndifferentAccess.new(YAML.parse_file(CONFIG_FILE).to_ruby)
|
21
|
+
@@config[:watch_tower].send(:[], config)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
# Ensures config file exists in the user config folder
|
26
|
+
#
|
27
|
+
# @param [Void]
|
28
|
+
# @return [Void]
|
29
|
+
# @raise [Void]
|
30
|
+
def ensure_config_file_exists
|
31
|
+
unless File.exists?(CONFIG_FILE)
|
32
|
+
File.open(CONFIG_FILE, 'w') do |f|
|
33
|
+
f.write(File.read(File.join(TEMPLATE_PATH, 'config.yml')))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module WatchTower
|
2
|
+
module Editor
|
3
|
+
extend ::ActiveSupport::Autoload
|
4
|
+
|
5
|
+
autoload :BaseAppscript
|
6
|
+
autoload :BasePs
|
7
|
+
autoload :Textmate
|
8
|
+
autoload :Xcode
|
9
|
+
|
10
|
+
def self.editors
|
11
|
+
Editor.constants. # Collect the defined constants
|
12
|
+
collect { |c| "::WatchTower::Editor::#{c}"}. # Access them under the Server module
|
13
|
+
collect(&:constantize). # Make them a constant
|
14
|
+
select { |c| c.class == Class } # Keep only classes
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
File without changes
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'watch_tower/appscript'
|
2
|
+
|
3
|
+
module WatchTower
|
4
|
+
module Editor
|
5
|
+
module BaseAppscript
|
6
|
+
def self.included(base)
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
def self.included(base)
|
12
|
+
base.class_eval <<-END, __FILE__, __LINE__ + 1
|
13
|
+
# Include AppScript
|
14
|
+
include ::Appscript
|
15
|
+
|
16
|
+
def is_running?
|
17
|
+
editor.is_running? if editor
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_path
|
21
|
+
current_paths.try(:first)
|
22
|
+
end
|
23
|
+
|
24
|
+
def current_paths
|
25
|
+
if is_running?
|
26
|
+
editor.document.get.collect(&:path).collect(&:get)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
END
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module WatchTower
|
2
|
+
module Editor
|
3
|
+
class Textmate
|
4
|
+
include BaseAppscript
|
5
|
+
|
6
|
+
protected
|
7
|
+
# This method returns an instance of ::Appscript::Application
|
8
|
+
#
|
9
|
+
# returns [::Appscript::Application | nil]
|
10
|
+
def editor
|
11
|
+
app 'Textmate'
|
12
|
+
rescue ::FindApp::ApplicationNotFoundError
|
13
|
+
LOG.debug "#{__FILE__}:#{__LINE__ - 2}: Textmate application can't be found, maybe not installed?"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module WatchTower
|
2
|
+
module Editor
|
3
|
+
class Xcode
|
4
|
+
include BaseAppscript
|
5
|
+
|
6
|
+
protected
|
7
|
+
# This method returns an instance of ::Appscript::Application
|
8
|
+
#
|
9
|
+
# returns [::Appscript::Application | nil]
|
10
|
+
def editor
|
11
|
+
# Cannot use app('Xcode') because it fails when multiple Xcode versions are installed
|
12
|
+
# Taken from timetap
|
13
|
+
# https://github.com/apalancat/timetap/blob/editors/lib/time_tap/editors.rb#L25
|
14
|
+
pid = app('System Events').processes[its.name.eq('Xcode')].first.unix_id.get
|
15
|
+
app.by_pid(pid)
|
16
|
+
rescue ::FindApp::ApplicationNotFoundError
|
17
|
+
rescue ::Appscript::CommandError
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module WatchTower
|
2
|
+
# Global Error
|
3
|
+
WatchTowerError = Class.new Exception
|
4
|
+
|
5
|
+
# Exceptions raised by the Project module
|
6
|
+
ProjectError = Class.new WatchTowerError
|
7
|
+
FileNotFound = Class.new ProjectError
|
8
|
+
|
9
|
+
# Exception raised by the Path module
|
10
|
+
PathError = Class.new ProjectError
|
11
|
+
PathNotUnderCodePath = Class.new PathError
|
12
|
+
|
13
|
+
# Exception raised by the Editor module
|
14
|
+
EditorError = Class.new WatchTowerError
|
15
|
+
TextmateError = Class.new EditorError
|
16
|
+
XcodeError = Class.new EditorError
|
17
|
+
|
18
|
+
# Exceptions raised by the Server module
|
19
|
+
ServerError = Class.new WatchTowerError
|
20
|
+
DatabaseError = Class.new ServerError
|
21
|
+
DatabaseConfigNotFoundError = Class.new DatabaseError
|
22
|
+
|
23
|
+
# Exceptions raised by the Eye module
|
24
|
+
EyeError = Class.new WatchTowerError
|
25
|
+
end
|