maglev-webtools 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE.txt +25 -0
- data/README.rdoc +121 -0
- data/bin/webtools +15 -0
- data/lib/web_tools/#debugger.rb# +212 -0
- data/lib/web_tools/browser.rb +45 -0
- data/lib/web_tools/debugger.rb +219 -0
- data/lib/web_tools/info.rb +29 -0
- data/lib/web_tools/middleware/debugger.rb +118 -0
- data/lib/web_tools/support/app_model.rb +117 -0
- data/lib/web_tools/support/code_browser.rb +109 -0
- data/lib/web_tools/support/ruby.rb +132 -0
- data/lib/web_tools/support/service_helper.rb +22 -0
- data/lib/web_tools/support/smalltalk_extensions.rb +65 -0
- data/lib/web_tools/support/smalltalk_tools.rb +16 -0
- data/lib/web_tools/ui.rb +67 -0
- data/lib/web_tools.rb +10 -0
- data/public/images/favicon.ico +0 -0
- data/public/javascript/CodeMirror/LICENSE +23 -0
- data/public/javascript/CodeMirror/css/Smalltalk.css +34 -0
- data/public/javascript/CodeMirror/js/codemirror.js +582 -0
- data/public/javascript/CodeMirror/js/editor.js +1671 -0
- data/public/javascript/CodeMirror/js/highlight.js +68 -0
- data/public/javascript/CodeMirror/js/parseSmalltalk.js +126 -0
- data/public/javascript/CodeMirror/js/parsedummy.js +32 -0
- data/public/javascript/CodeMirror/js/select.js +699 -0
- data/public/javascript/CodeMirror/js/stringstream.js +159 -0
- data/public/javascript/CodeMirror/js/tokenize.js +57 -0
- data/public/javascript/CodeMirror/js/undo.js +413 -0
- data/public/javascript/CodeMirror/js/util.js +133 -0
- data/public/javascript/CodeMirror/testSmalltalkParser.html +116 -0
- data/public/javascript/ace/ace-uncompressed.js +17299 -0
- data/public/javascript/ace/ace.js +1 -0
- data/public/javascript/ace/keybinding-emacs.js +1 -0
- data/public/javascript/ace/keybinding-vim.js +1 -0
- data/public/javascript/ace/mode-c_cpp.js +1 -0
- data/public/javascript/ace/mode-clojure.js +1 -0
- data/public/javascript/ace/mode-coffee.js +1 -0
- data/public/javascript/ace/mode-csharp.js +1 -0
- data/public/javascript/ace/mode-css.js +1 -0
- data/public/javascript/ace/mode-groovy.js +1 -0
- data/public/javascript/ace/mode-html.js +1 -0
- data/public/javascript/ace/mode-java.js +1 -0
- data/public/javascript/ace/mode-javascript.js +1 -0
- data/public/javascript/ace/mode-json.js +1 -0
- data/public/javascript/ace/mode-lua.js +1 -0
- data/public/javascript/ace/mode-markdown.js +1 -0
- data/public/javascript/ace/mode-ocaml.js +1 -0
- data/public/javascript/ace/mode-perl.js +1 -0
- data/public/javascript/ace/mode-php.js +1 -0
- data/public/javascript/ace/mode-python.js +1 -0
- data/public/javascript/ace/mode-ruby.js +1 -0
- data/public/javascript/ace/mode-scad.js +1 -0
- data/public/javascript/ace/mode-scala.js +1 -0
- data/public/javascript/ace/mode-scss.js +1 -0
- data/public/javascript/ace/mode-svg.js +1 -0
- data/public/javascript/ace/mode-textile.js +1 -0
- data/public/javascript/ace/mode-xml.js +1 -0
- data/public/javascript/ace/theme-clouds.js +1 -0
- data/public/javascript/ace/theme-clouds_midnight.js +1 -0
- data/public/javascript/ace/theme-cobalt.js +1 -0
- data/public/javascript/ace/theme-crimson_editor.js +1 -0
- data/public/javascript/ace/theme-dawn.js +1 -0
- data/public/javascript/ace/theme-eclipse.js +1 -0
- data/public/javascript/ace/theme-idle_fingers.js +1 -0
- data/public/javascript/ace/theme-kr_theme.js +1 -0
- data/public/javascript/ace/theme-merbivore.js +1 -0
- data/public/javascript/ace/theme-merbivore_soft.js +1 -0
- data/public/javascript/ace/theme-mono_industrial.js +1 -0
- data/public/javascript/ace/theme-monokai.js +1 -0
- data/public/javascript/ace/theme-pastel_on_dark.js +1 -0
- data/public/javascript/ace/theme-solarized_dark.js +1 -0
- data/public/javascript/ace/theme-solarized_light.js +1 -0
- data/public/javascript/ace/theme-textmate.js +1 -0
- data/public/javascript/ace/theme-twilight.js +1 -0
- data/public/javascript/ace/theme-vibrant_ink.js +1 -0
- data/public/javascript/ace/worker-coffee.js +1 -0
- data/public/javascript/ace/worker-css.js +1 -0
- data/public/javascript/ace/worker-javascript.js +1 -0
- data/public/javascript/webtools/#debugger.coffee# +253 -0
- data/public/javascript/webtools/browser.js +260 -0
- data/public/javascript/webtools/debugger.coffee +286 -0
- data/public/javascript/webtools/debugger.js +366 -0
- data/public/javascript/webtools/sessions.coffee +17 -0
- data/public/javascript/webtools/sessions.js +27 -0
- data/public/javascript/webtools/version.coffee +14 -0
- data/public/javascript/webtools/version.js +20 -0
- data/public/stylesheets/base/images/ui-anim_basic_16x16.gif +0 -0
- data/public/stylesheets/base/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/public/stylesheets/base/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/public/stylesheets/base/images/ui-icons_222222_256x240.png +0 -0
- data/public/stylesheets/base/images/ui-icons_2e83ff_256x240.png +0 -0
- data/public/stylesheets/base/images/ui-icons_454545_256x240.png +0 -0
- data/public/stylesheets/base/images/ui-icons_888888_256x240.png +0 -0
- data/public/stylesheets/base/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/public/stylesheets/base/jquery.ui.accordion.css +12 -0
- data/public/stylesheets/base/jquery.ui.all.css +2 -0
- data/public/stylesheets/base/jquery.ui.autocomplete.css +39 -0
- data/public/stylesheets/base/jquery.ui.base.css +11 -0
- data/public/stylesheets/base/jquery.ui.button.css +35 -0
- data/public/stylesheets/base/jquery.ui.core.css +37 -0
- data/public/stylesheets/base/jquery.ui.datepicker.css +61 -0
- data/public/stylesheets/base/jquery.ui.dialog.css +13 -0
- data/public/stylesheets/base/jquery.ui.progressbar.css +4 -0
- data/public/stylesheets/base/jquery.ui.resizable.css +13 -0
- data/public/stylesheets/base/jquery.ui.selectable.css +3 -0
- data/public/stylesheets/base/jquery.ui.slider.css +17 -0
- data/public/stylesheets/base/jquery.ui.tabs.css +11 -0
- data/public/stylesheets/base/jquery.ui.theme.css +247 -0
- data/public/stylesheets/jquery.contextMenu.css +62 -0
- data/public/stylesheets/reset.css +18 -0
- data/public/stylesheets/webtools.css +53 -0
- data/public/test.html +47 -0
- data/views/browser.rhtml +63 -0
- data/views/debugger.rhtml +59 -0
- data/views/index.rhtml +8 -0
- data/views/layout.rhtml +24 -0
- data/views/sessions.rhtml +12 -0
- data/views/version.rhtml +10 -0
- metadata +316 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'web_tools'
|
|
2
|
+
require 'maglev/debugger'
|
|
3
|
+
|
|
4
|
+
module WebTools::Middleware
|
|
5
|
+
class Debugger
|
|
6
|
+
attr_reader :production_mode
|
|
7
|
+
|
|
8
|
+
def initialize(app, *args)
|
|
9
|
+
@app = app
|
|
10
|
+
self.production_mode = true if args.include?(:production)
|
|
11
|
+
self.production_mode ||= false if args.include?(:development)
|
|
12
|
+
self.production_mode ||= (ENV["RACK_ENV"] == :production)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def production_mode=(bool)
|
|
16
|
+
@production_mode = bool
|
|
17
|
+
if bool
|
|
18
|
+
@debugger = self
|
|
19
|
+
else
|
|
20
|
+
@debugger = InProcessDebugger.new(@app)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
dup._call(env)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def _call(env)
|
|
29
|
+
@debugger.wrap_call(env) do
|
|
30
|
+
Maglev::Debugger.debug(production_mode) { @app.call(env) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def wrap_call(env)
|
|
35
|
+
yield
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class InProcessDebugger
|
|
39
|
+
def initialize(app)
|
|
40
|
+
@app = app
|
|
41
|
+
@debugger_app = WebTools::Debugger.new
|
|
42
|
+
load_template
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def load_template
|
|
46
|
+
file = File.expand_path(__FILE__)
|
|
47
|
+
_, @template = ::IO.read(file).gsub("\r\n", "\n").split(/^__END__$/, 2)
|
|
48
|
+
ERB.new(@template).def_method(self.class, 'render_template(title, path)')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def debugger_active?
|
|
52
|
+
defined?(@@debugger_active) && !!@@debugger_active &&
|
|
53
|
+
defined?(@@debugged_process) && @@debugged_process.alive?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wrap_call(env)
|
|
57
|
+
if debugger_active?
|
|
58
|
+
p env
|
|
59
|
+
if env["PATH_INFO"] == "/"
|
|
60
|
+
[500, {"Content-Type" => "text/html"}, info_message(env)]
|
|
61
|
+
else
|
|
62
|
+
response = @debugger_app.call(env)
|
|
63
|
+
response[1]['Access-Control-Allow-Origin'] = "*"
|
|
64
|
+
response
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
application_call(env, yield)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def application_call(env, result)
|
|
72
|
+
if result.is_a? Maglev::Debugger::Process
|
|
73
|
+
raise result.exception if result[:skip_debugger]
|
|
74
|
+
result[:skip_debugger] = false
|
|
75
|
+
@@debugged_path = env["PATH_INFO"]
|
|
76
|
+
@@debugged_process = result.thread
|
|
77
|
+
@@debugger_active = true
|
|
78
|
+
@@debugged_exception = result.exception.message
|
|
79
|
+
[500, {"Content-Type" => "text/html"}, info_message(env)]
|
|
80
|
+
else
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def info_message(env)
|
|
86
|
+
[render_template(@@debugged_exception,
|
|
87
|
+
env['rack.url_scheme'].to_s + "://" +
|
|
88
|
+
env['HTTP_HOST'].to_s +
|
|
89
|
+
env['SCRIPT_NAME'].to_s + "/process/" +
|
|
90
|
+
@@debugged_process.object_id.to_s)]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
__END__
|
|
97
|
+
<!doctype html system>
|
|
98
|
+
<html>
|
|
99
|
+
<head>
|
|
100
|
+
<title>Maglev Debugger</title>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<h4><%= title %></h4>
|
|
104
|
+
There was an error, and execution has been suspended.
|
|
105
|
+
You can interact with this debugging service to inspect the problem.
|
|
106
|
+
<br>
|
|
107
|
+
The entry point is at:
|
|
108
|
+
<br>
|
|
109
|
+
<a href="<%= path %>"><%= path %></a>
|
|
110
|
+
<br>
|
|
111
|
+
Once you're done debugging, you can click
|
|
112
|
+
<form style="display:inline" name="resumeForm" method="POST" action="<%= path %>">
|
|
113
|
+
<input type="hidden" name="running" VALUE="true">
|
|
114
|
+
<a href="#" onClick="document.resumeForm.submit(); return false">here</a>
|
|
115
|
+
</form>
|
|
116
|
+
to resume the original process.
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
|
|
2
|
+
require 'web_tools/support/code_browser'
|
|
3
|
+
require 'web_tools/support/smalltalk_extensions'
|
|
4
|
+
|
|
5
|
+
# This module emulates all of the API from the Smalltalk side of things
|
|
6
|
+
module WebTools
|
|
7
|
+
|
|
8
|
+
# This is a ViewModel for the WebTools Application.
|
|
9
|
+
#
|
|
10
|
+
# All of the methods that return "Objects" should return a Hash. Keys
|
|
11
|
+
# beginning with '_' are reserved for metadata applied by the GUI.
|
|
12
|
+
class AppModel
|
|
13
|
+
def initialize
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
VERSION_HEADERS = [['Attribute', 'The attribute.'],
|
|
17
|
+
['Stone', 'The value for the stone process.'],
|
|
18
|
+
['WebTools', 'The value for the WebTools vm process (if different than the Stone''s value)']]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Returns a hash of configuration parameters for the stone and the gem.
|
|
22
|
+
# The has has three keys:
|
|
23
|
+
# + :timestamp => when the report was generated
|
|
24
|
+
# + :headers => array of [name, description] pairs for the fields
|
|
25
|
+
# + :report => An array of data. Each entry is an array of the field data.
|
|
26
|
+
#
|
|
27
|
+
def version_report
|
|
28
|
+
stone_rpt = stone_version_report
|
|
29
|
+
gem_rpt = gem_version_report
|
|
30
|
+
data = { }
|
|
31
|
+
(stone_rpt.keys + gem_rpt.keys).each do |k|
|
|
32
|
+
g = stone_rpt[k] == gem_rpt[k] ? '' : gem_rpt[k]
|
|
33
|
+
data[k] = [stone_rpt[k], g]
|
|
34
|
+
end
|
|
35
|
+
{ :timestamp => Time.now.asctime,
|
|
36
|
+
:headers => VERSION_HEADERS,
|
|
37
|
+
:report => data }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# An array of [name, description] entries for the fields in the session report.
|
|
41
|
+
SESSION_FIELDS =
|
|
42
|
+
[['User', 'The UserProfile of the session, or nil if the UserProfile is recently created and not visible from this session''s transactional view, or the session is no longer active.'],
|
|
43
|
+
['PID', 'The process ID of the Gem process of the session.'],
|
|
44
|
+
['Host', 'The hostname of the machine running the Gem process (a String, limited to 127 bytes).'],
|
|
45
|
+
['Prim #', 'Primitive number in which the Gem is executing (if it is executing in a long primitive such as MFC).'],
|
|
46
|
+
['View Age', 'Time since the session''s most recent beginTransaction, commitTransaction, or abortTransaction.'],
|
|
47
|
+
['State', 'The session state (an enum from SesTaskStatusEType in session.ht).'],
|
|
48
|
+
['Trans', 'One of the following: ''none'' if the session is in transactionless mode, ''out'' if it is not in a transaction, and ''in'' if it is in a transaction.'],
|
|
49
|
+
['Oldest CR?', 'A Boolean whose value is true if the session is currently referencing the oldest commit record, and false if it is not.'],
|
|
50
|
+
['Serial', 'The session''s serial number. A serial number will not be reused until the stone is restarted.'],
|
|
51
|
+
['Session', "The session's sessionId."],
|
|
52
|
+
['GCI IP', 'A String containing the ip address of host running the GCI process. If the GCI application is linked (using libgcilnk*.so or gcilnk*.dll) this ip address is the address of the machine running the gem process.'],
|
|
53
|
+
['Priority', 'The priority of the session where 0 is lowest, 2 is normal, and 4 is highest. Session priority is used by the stone to order requests for service by sessions.'],
|
|
54
|
+
['Host ID', 'Unique host ID of the host where the session is running.'],
|
|
55
|
+
['Quiet', 'Time since the session''s most recent request to stone.'],
|
|
56
|
+
['Age', 'Time since the session logged in.'],
|
|
57
|
+
['CRB', 'Commit Record Backlog: number of commits which have occurred since the session obtained its view.']]
|
|
58
|
+
|
|
59
|
+
# Returns a hash of configuration parameters for the stone and the gem.
|
|
60
|
+
# The has has three keys:
|
|
61
|
+
# + :timestamp => when the report was generated
|
|
62
|
+
# + :headers => array of [name, description] pairs for the fields
|
|
63
|
+
# + :report => An array of data. Each entry is an array of the field data.
|
|
64
|
+
#
|
|
65
|
+
def session_report
|
|
66
|
+
ts = Time.now
|
|
67
|
+
now = ts.to_i
|
|
68
|
+
session_info = Maglev::System.current_session_ids.map do |id|
|
|
69
|
+
sess_desc = Maglev::System.description_of_session id
|
|
70
|
+
sess_desc[0] = sess_desc[0].instance_variable_get(:@_st_userId) # UserProfile
|
|
71
|
+
sess_desc[3] = '' if sess_desc[3] == 0 # Primitive?
|
|
72
|
+
sess_desc[4] = format_secs(now - sess_desc[4]) # View Age
|
|
73
|
+
sess_desc[6] = ['none', 'out', 'in'][sess_desc[6] + 1] # Transaction
|
|
74
|
+
sess_desc[13] = format_secs(now - sess_desc[13]) # Quiet
|
|
75
|
+
sess_desc[14] = format_secs(now - sess_desc[14]) # Age
|
|
76
|
+
sess_desc
|
|
77
|
+
# sess_cache_slot = Maglev::System.cache_slot_for_sessionid id
|
|
78
|
+
end
|
|
79
|
+
{ :timestamp => ts.asctime,
|
|
80
|
+
:headers => SESSION_FIELDS,
|
|
81
|
+
:report => session_info }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
SECS_PER_DAY = 86400
|
|
86
|
+
SECS_PER_HOUR = 3600
|
|
87
|
+
SECS_PER_MIN = 60
|
|
88
|
+
SECS_PER_SEC = 1
|
|
89
|
+
|
|
90
|
+
# Format number of seconds like "3 days 12:07:58"
|
|
91
|
+
def format_secs(seconds)
|
|
92
|
+
splits = []
|
|
93
|
+
[SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MIN, SECS_PER_SEC].each do |x|
|
|
94
|
+
splits << seconds / x
|
|
95
|
+
seconds = seconds % x
|
|
96
|
+
end
|
|
97
|
+
days = splits.shift
|
|
98
|
+
|
|
99
|
+
ts = "%02d:%02d:%02d" % splits
|
|
100
|
+
days > 0 ? "#{days} #{days == 1 ? 'day' : 'days'} #{ts}" : ts
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def stone_version_report
|
|
104
|
+
results = { }
|
|
105
|
+
rpt = Maglev::System.stone_version_report
|
|
106
|
+
rpt.keys.each { |k| results[k] = rpt.at(k) }
|
|
107
|
+
results
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def gem_version_report
|
|
111
|
+
results = { }
|
|
112
|
+
rpt = Maglev::System.gem_version_report
|
|
113
|
+
rpt.keys.each { |k| results[k] = rpt.at(k) }
|
|
114
|
+
results
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'web_tools/support/ruby'
|
|
2
|
+
|
|
3
|
+
module WebTools
|
|
4
|
+
# The CodeBrowser is a ViewModel for MagLev code browsing. It keeps the
|
|
5
|
+
# entire state of the UI and returns structured data for use by a UI.
|
|
6
|
+
# For the first pass, we do not cache anything.
|
|
7
|
+
class CodeBrowser
|
|
8
|
+
def self.class_and_module_list
|
|
9
|
+
{ 'modules' => WebTools::Ruby.class_and_module_names }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def select_module(module_name)
|
|
13
|
+
mod = Ruby.find_in_namespace(module_name)
|
|
14
|
+
methods = mod.instance_methods(false) +
|
|
15
|
+
mod.protected_instance_methods(false) +
|
|
16
|
+
mod.private_instance_methods(false)
|
|
17
|
+
{
|
|
18
|
+
:ancestors => mod.ancestors.reverse,
|
|
19
|
+
:constants => mod.constants.sort,
|
|
20
|
+
:instance_methods => methods.uniq.sort,
|
|
21
|
+
:module_methods => Ruby.module_fns_for(mod),
|
|
22
|
+
:selected_module => module_name,
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def select_constant(module_name, const_name)
|
|
27
|
+
parent = Ruby.find_in_namespace module_name
|
|
28
|
+
{
|
|
29
|
+
:const_value => ObjectInfo.for(parent.const_get(const_name)),
|
|
30
|
+
:selected_constant => const_name,
|
|
31
|
+
:selected_module => module_name,
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def select_method(module_name, method_name, is_instance_method)
|
|
36
|
+
mod = Ruby.find_in_namespace(module_name)
|
|
37
|
+
src, file, line = mod.method_source(method_name, is_instance_method)
|
|
38
|
+
{
|
|
39
|
+
:is_instance_method => is_instance_method,
|
|
40
|
+
:method_line_number => line,
|
|
41
|
+
:method_source => src,
|
|
42
|
+
:method_source_file => file,
|
|
43
|
+
:module_name => module_name,
|
|
44
|
+
:selected_method => method_name,
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def object_info(object_id)
|
|
49
|
+
ObjectInfo.for(object_id)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class ObjectInfo
|
|
54
|
+
# Return a new ObjectInfo instance for the object with the given object
|
|
55
|
+
# id.
|
|
56
|
+
def self.for_id(object_id)
|
|
57
|
+
new(ObjectSpace._id2ref(object_id))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Return a new ObjectInfo instance for the given object.
|
|
61
|
+
def self.for(object)
|
|
62
|
+
new(object)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
attr_reader :info
|
|
66
|
+
|
|
67
|
+
def initialize(obj)
|
|
68
|
+
@info = { }
|
|
69
|
+
@info[:object_id] = obj.object_id
|
|
70
|
+
@info[:class] = obj.class.name
|
|
71
|
+
@info[:inspect] = obj.inspect
|
|
72
|
+
@info[:instance_variables] = []
|
|
73
|
+
@info[:enumerated] = []
|
|
74
|
+
@info[:enumerated_size] = obj.respond_to?(:size) ? obj.size : nil
|
|
75
|
+
|
|
76
|
+
obj.instance_variables.each do |iv|
|
|
77
|
+
val = obj.instance_variable_get(iv)
|
|
78
|
+
@info[:instance_variables] << [iv, val.inspect, val.object_id]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
limit = 10
|
|
82
|
+
case obj
|
|
83
|
+
when Enumerable, Array
|
|
84
|
+
obj.each_with_index do |o,i|
|
|
85
|
+
if i > limit
|
|
86
|
+
@info[:enumerated] << '...'
|
|
87
|
+
break
|
|
88
|
+
else
|
|
89
|
+
@info[:enumerated] << o.inspect
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
when Hash
|
|
94
|
+
obj.each_with_index do |pair, idx|
|
|
95
|
+
if idx > limit
|
|
96
|
+
@info[:enumerated] << ['', '...']
|
|
97
|
+
break
|
|
98
|
+
end
|
|
99
|
+
@info[:enumerated] << [pair[0].to_s, pair[1].inspect]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def to_json(*args)
|
|
106
|
+
@info.to_json
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'maglev/method_source'
|
|
2
|
+
module WebTools
|
|
3
|
+
# Provide information about the Ruby environment
|
|
4
|
+
module Ruby
|
|
5
|
+
# Traverse the Ruby namespace hierarchy and execute block for all classes
|
|
6
|
+
# and modules. Returns an IdentitySet of all classes and modules found.
|
|
7
|
+
# Skips autoloads (i.e., does not trigger them and does not yield them to
|
|
8
|
+
# the block).
|
|
9
|
+
#
|
|
10
|
+
# @param [Module] klass The Class or Module object to start traversal.
|
|
11
|
+
# Default is Object.
|
|
12
|
+
#
|
|
13
|
+
# @param [IdentitySet] rg The recursion guard used to prevent infinite
|
|
14
|
+
# loops; also used as return value.
|
|
15
|
+
#
|
|
16
|
+
# @return [IdentitySet] An IdentitySet of all the Classes and Modules
|
|
17
|
+
# registered in the Ruby namespace
|
|
18
|
+
#
|
|
19
|
+
def each_module(klass=Object, rg=IdentitySet.new, &block)
|
|
20
|
+
raise 'Must be class or module' unless Module === klass
|
|
21
|
+
unless rg.include?(klass)
|
|
22
|
+
rg.add klass
|
|
23
|
+
yield klass
|
|
24
|
+
klass.constants.each do |c|
|
|
25
|
+
unless klass.autoload?(c)
|
|
26
|
+
obj = klass.const_get c
|
|
27
|
+
each_module(obj, rg, &block) if Module === obj
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
rg
|
|
32
|
+
end
|
|
33
|
+
module_function :each_module
|
|
34
|
+
|
|
35
|
+
# Returns a sorted list of class and module names in the Ruby Namespace
|
|
36
|
+
#
|
|
37
|
+
# @return [Array] sorted list of class and module names found in the Ruby
|
|
38
|
+
# namespace hierarchy.
|
|
39
|
+
def class_and_module_names
|
|
40
|
+
names = []
|
|
41
|
+
each_module { |klass| names << klass.name }
|
|
42
|
+
names.sort
|
|
43
|
+
end
|
|
44
|
+
module_function :class_and_module_names
|
|
45
|
+
|
|
46
|
+
# A simple message from Ruby Land.
|
|
47
|
+
def say_something
|
|
48
|
+
"Hello from MagLev, My name is '#{self.name}'"
|
|
49
|
+
end
|
|
50
|
+
module_function :say_something
|
|
51
|
+
|
|
52
|
+
# Return an object named in the Ruby namespace.
|
|
53
|
+
#
|
|
54
|
+
# @param [String] name The name of the object. E.g., "Object",
|
|
55
|
+
# "Errno::EACCES", "Foo::Bar::Baz".
|
|
56
|
+
#
|
|
57
|
+
# @return [Object] the named object.
|
|
58
|
+
#
|
|
59
|
+
# @raise [NameError] if the name can't be found
|
|
60
|
+
def find_in_namespace(name)
|
|
61
|
+
name.split('::').inject(Object) do |parent, name|
|
|
62
|
+
obj = parent.const_get name
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
module_function :find_in_namespace
|
|
66
|
+
|
|
67
|
+
MODULE_MOD_FNS = Module.instance_methods(false)
|
|
68
|
+
|
|
69
|
+
def module_info_for(name)
|
|
70
|
+
mod = find_in_namespace name
|
|
71
|
+
{ :constants => mod.constants.sort,
|
|
72
|
+
:class_methods => module_fns_for(mod),
|
|
73
|
+
:instance_methods => mod.instance_methods(false).sort }
|
|
74
|
+
end
|
|
75
|
+
module_function :module_info_for
|
|
76
|
+
|
|
77
|
+
def module_fns_for(mod)
|
|
78
|
+
res = []
|
|
79
|
+
begin
|
|
80
|
+
sclass = mod.__singleton_class
|
|
81
|
+
res = sclass.instance_methods(false) - MODULE_MOD_FNS
|
|
82
|
+
rescue => e
|
|
83
|
+
# nothing
|
|
84
|
+
end
|
|
85
|
+
res
|
|
86
|
+
end
|
|
87
|
+
module_function :module_fns_for
|
|
88
|
+
|
|
89
|
+
# Get the source code for the named class and method
|
|
90
|
+
#
|
|
91
|
+
# The success of this method depends on being called from a Ruby stack,
|
|
92
|
+
# not a Smalltalk stack.
|
|
93
|
+
#
|
|
94
|
+
# @param [String] The name of the class
|
|
95
|
+
# @param [String] The method name
|
|
96
|
+
# @param [Boolean] if true, then look for a class method, otherwise look
|
|
97
|
+
# for an instance method.
|
|
98
|
+
def source_for(class_name, method_name, instance_method=true)
|
|
99
|
+
klass = find_in_namespace(class_name)
|
|
100
|
+
gsnmeth = klass.gs_method_for(method_name, instance_method)
|
|
101
|
+
src = gsnmeth.__source_string
|
|
102
|
+
file,line = gsnmeth.__source_location
|
|
103
|
+
file.nil? ? "#{src}\n# No file information available." : "#{src}\n##{file}:#{line}"
|
|
104
|
+
end
|
|
105
|
+
module_function :source_for
|
|
106
|
+
|
|
107
|
+
# Return a display string suitable for rendering a constant.
|
|
108
|
+
# Currently, it just returns the results of #inspect on the object.
|
|
109
|
+
#
|
|
110
|
+
# TODO
|
|
111
|
+
# 1. If the const value is a proc, then return "Proc: " + the source code.
|
|
112
|
+
#
|
|
113
|
+
# @param [String,Symbol] class_name The name of the class to query.
|
|
114
|
+
# @param [String,Symbol] const_name The name of the constant.
|
|
115
|
+
# @return [String] A description of the constant.
|
|
116
|
+
#
|
|
117
|
+
def const_info_for(class_name, const_name)
|
|
118
|
+
klass = find_in_namespace class_name
|
|
119
|
+
const = klass.const_get const_name
|
|
120
|
+
const.inspect
|
|
121
|
+
end
|
|
122
|
+
module_function :const_info_for
|
|
123
|
+
|
|
124
|
+
# def logit(msg)
|
|
125
|
+
# File.open("/tmp/log", "a+") do |f|
|
|
126
|
+
# f.puts msg
|
|
127
|
+
# f.flush
|
|
128
|
+
# end
|
|
129
|
+
# end
|
|
130
|
+
# module_function :logit
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'json/pure' unless defined? JSON
|
|
2
|
+
|
|
3
|
+
module WebTools::Support::ServiceHelper
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.set :show_exceptions, true
|
|
6
|
+
base.set :raise_errors, false
|
|
7
|
+
|
|
8
|
+
base.error do
|
|
9
|
+
excep = request.env['sinatra.error']
|
|
10
|
+
{ '_stack' => excep.backtrace.join("<br>") }.to_json
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns a JSON string that contains the data under the "data" key.
|
|
15
|
+
# Adds other keys (_time, _stack) if appropriate.
|
|
16
|
+
def prepare_data(data)
|
|
17
|
+
raise "Expecting Hash" unless Hash === data
|
|
18
|
+
data['_time'] = ((Time.now - @ts) * 1_000).to_i
|
|
19
|
+
data['_stack'] = @stack
|
|
20
|
+
data.to_json
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Extra Smalltalk features used by WebTools
|
|
2
|
+
class Maglev::System
|
|
3
|
+
class_primitive_nobridge 'stone_version_report', 'stoneVersionReport'
|
|
4
|
+
class_primitive_nobridge 'gem_version_report', 'gemVersionReport'
|
|
5
|
+
|
|
6
|
+
# Returns an Array of Fixnums corresponding to all of the sessions
|
|
7
|
+
# currently running on the GemStone system.
|
|
8
|
+
#
|
|
9
|
+
class_primitive_nobridge 'current_session_ids', 'currentSessions'
|
|
10
|
+
|
|
11
|
+
# Return the cache process slot number (a SmallInteger) for the given
|
|
12
|
+
# session ID. The session must be connected to the same shared page
|
|
13
|
+
# cache as the session invoking this method.
|
|
14
|
+
#
|
|
15
|
+
# A return of nil indicates the session could not be located.
|
|
16
|
+
class_primitive_nobridge 'cache_slot_for_sessionid', 'cacheSlotForSessionId:'
|
|
17
|
+
|
|
18
|
+
# Returns an Array describing the session as follows:
|
|
19
|
+
#
|
|
20
|
+
# 1. The UserProfile of the session, or nil if the UserProfile is recently
|
|
21
|
+
# created and not visible from this session's transactional view,
|
|
22
|
+
# or the session is no longer active.
|
|
23
|
+
# 2. The process ID of the Gem process of the session (an Integer).
|
|
24
|
+
# 3. The hostname of the machine running the Gem process
|
|
25
|
+
# (a String, limited to 127 bytes).
|
|
26
|
+
# 4. Primitive number in which the Gem is executing, or 0 if it is not executing
|
|
27
|
+
# in a long primitive.
|
|
28
|
+
# 5. Time of the session's most recent beginTransaction, commitTransaction, or
|
|
29
|
+
# abortTransaction (from System timeGmt).
|
|
30
|
+
# 6. The session state (a SmallInteger).
|
|
31
|
+
# 7. A SmallInteger whose value is -1 if the session is in transactionless mode,
|
|
32
|
+
# 0 if it is not in a transaction and 1 if it is in a transaction.
|
|
33
|
+
# 8. A Boolean whose value is true if the session is currently referencing the
|
|
34
|
+
# oldest commit record, and false if it is not.
|
|
35
|
+
# 9. The session's serial number (a SmallInteger).
|
|
36
|
+
# 10. The session's sessionId (a SmallInteger).
|
|
37
|
+
# 11. A String containing the ip address of host running the GCI process.
|
|
38
|
+
# If the GCI application is linked (using libgcilnk*.so or gcilnk*.dll)
|
|
39
|
+
# this ip address is the address of the machine running the gem process .
|
|
40
|
+
# 12. The priority of the session (a SmallInteger).
|
|
41
|
+
# 13. Unique host ID of the host where the session is running (an Integer)
|
|
42
|
+
# 14. Time of the session's most recent request to stone (from System timeGmt)
|
|
43
|
+
# 15. Time the session logged in (from System timeGmt)
|
|
44
|
+
# 16. Number of commits which have occurred since the session obtained its view.
|
|
45
|
+
#
|
|
46
|
+
# Because a session can update its commit record without committing a
|
|
47
|
+
# transaction, it is possible that no session actually references the oldest
|
|
48
|
+
# commit record. Therefore, the eighth element may be false for all current
|
|
49
|
+
# sessions.
|
|
50
|
+
#
|
|
51
|
+
# To execute this method for any session other than your current session, you
|
|
52
|
+
# must have the SessionAccess privilege.
|
|
53
|
+
#
|
|
54
|
+
class_primitive_nobridge 'description_of_session', 'descriptionOfSession:'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The stoneVersionReport returns a StringKeyValueDictionary.
|
|
58
|
+
# Unfortunately, that is on a different branch of the Smalltalk class
|
|
59
|
+
# hierarchy than RubyHash, so we need to add a few methods to let us
|
|
60
|
+
# iterate over the dictionary.
|
|
61
|
+
StringKeyValueDictionary = __resolve_smalltalk_global(:StringKeyValueDictionary)
|
|
62
|
+
class StringKeyValueDictionary
|
|
63
|
+
primitive_nobridge 'keys', 'keys'
|
|
64
|
+
primitive_nobridge 'at', 'at:'
|
|
65
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# The Smalltalk product, upon which MagLev is based, comes with a simple
|
|
2
|
+
# code and statistics browser written in Smalltalk. This ruby script
|
|
3
|
+
# registers that code into the Ruby namespace, and then starts the
|
|
4
|
+
# application. Once it is running, it will print a URL to connect to.
|
|
5
|
+
|
|
6
|
+
# Register the Smalltalk WebTools Server class into the Ruby Namespace
|
|
7
|
+
SmalltalkWebTools = __resolve_smalltalk_global(:Server)
|
|
8
|
+
|
|
9
|
+
# Expose the class-side method needed to run the application
|
|
10
|
+
class SmalltalkWebTools
|
|
11
|
+
class_primitive_nobridge 'run', 'runInForeground'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Invoke. This will print out something like: http://cairo:60166/
|
|
15
|
+
# Point your web browser to the given url and play.
|
|
16
|
+
SmalltalkWebTools.run
|
data/lib/web_tools/ui.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'sinatra/base'
|
|
2
|
+
require 'web_tools'
|
|
3
|
+
|
|
4
|
+
class WebTools::UI < Sinatra::Base
|
|
5
|
+
use WebTools::Browser
|
|
6
|
+
use WebTools::Debugger
|
|
7
|
+
use WebTools::Info
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
@location = env["SCRIPT_NAME"]
|
|
11
|
+
@javascripts = %w[
|
|
12
|
+
https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js
|
|
13
|
+
https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js
|
|
14
|
+
]
|
|
15
|
+
@stylesheets = %w[reset.css webtools.css]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
get '/' do
|
|
19
|
+
erb :index
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
get '/browser' do
|
|
23
|
+
@javascripts += %w[webtools/browser.js CodeMirror/js/codemirror.js]
|
|
24
|
+
erb :browser
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
get '/debugger' do
|
|
28
|
+
@javascripts += %w[
|
|
29
|
+
ace/ace.js
|
|
30
|
+
ace/mode-ruby.js
|
|
31
|
+
webtools/debugger.js]
|
|
32
|
+
erb :debugger
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
get '/info/version' do
|
|
36
|
+
@javascripts += %w[webtools/version.js]
|
|
37
|
+
@stylesheets = []
|
|
38
|
+
erb :version
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
get '/info/sessions' do
|
|
42
|
+
@javascripts += %w[webtools/sessions.js]
|
|
43
|
+
@stylesheets = []
|
|
44
|
+
erb :sessions
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def erb(*args, &block)
|
|
50
|
+
@stylesheets = @stylesheets.map do |path|
|
|
51
|
+
unless path =~ /^((http)|\/)/
|
|
52
|
+
"#{@location}/stylesheets/#{path}"
|
|
53
|
+
else
|
|
54
|
+
path
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@javascripts = @javascripts.map do |path|
|
|
59
|
+
unless path =~ /^((http)|\/)/
|
|
60
|
+
"#{@location}/javascript/#{path}"
|
|
61
|
+
else
|
|
62
|
+
path
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
super
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/web_tools.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module WebTools
|
|
2
|
+
module Support; end
|
|
3
|
+
module Middleware; end
|
|
4
|
+
|
|
5
|
+
path = File.expand_path("../web_tools", __FILE__)
|
|
6
|
+
autoload :Browser, File.join(path, "browser.rb")
|
|
7
|
+
autoload :Debugger, File.join(path, "debugger.rb")
|
|
8
|
+
autoload :Info, File.join(path, "info.rb")
|
|
9
|
+
autoload :UI, File.join(path, "ui.rb")
|
|
10
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2007-2010 Marijn Haverbeke
|
|
2
|
+
|
|
3
|
+
This software is provided 'as-is', without any express or implied
|
|
4
|
+
warranty. In no event will the authors be held liable for any
|
|
5
|
+
damages arising from the use of this software.
|
|
6
|
+
|
|
7
|
+
Permission is granted to anyone to use this software for any
|
|
8
|
+
purpose, including commercial applications, and to alter it and
|
|
9
|
+
redistribute it freely, subject to the following restrictions:
|
|
10
|
+
|
|
11
|
+
1. The origin of this software must not be misrepresented; you must
|
|
12
|
+
not claim that you wrote the original software. If you use this
|
|
13
|
+
software in a product, an acknowledgment in the product
|
|
14
|
+
documentation would be appreciated but is not required.
|
|
15
|
+
|
|
16
|
+
2. Altered source versions must be plainly marked as such, and must
|
|
17
|
+
not be misrepresented as being the original software.
|
|
18
|
+
|
|
19
|
+
3. This notice may not be removed or altered from any source
|
|
20
|
+
distribution.
|
|
21
|
+
|
|
22
|
+
Marijn Haverbeke
|
|
23
|
+
marijnh@gmail.com
|