ladybug 0.0.1.alpha
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.
- checksums.yaml +7 -0
- data/README.md +1 -0
- data/lib/ladybug/debugger.rb +156 -0
- data/lib/ladybug/middleware.rb +245 -0
- data/lib/ladybug/object_manager.rb +139 -0
- data/lib/ladybug/script_repository.rb +50 -0
- data/lib/ladybug.rb +4 -0
- metadata +63 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 19fccf01c424b4f35e62b84e6d7aaf3978e0253d
|
4
|
+
data.tar.gz: 5a12abd7a1eb3148e9fd067675ffacb4012f4dd4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 481c6763053abdaea1057eba44c38003bc8475010a9684a02c82e5f5e55b217b960f93f29250c19a6cd5306aca4aac8bc6f644c1cd709e86017355a09504f3ac
|
7
|
+
data.tar.gz: 61f3f7a14e828a8192a21836f8f9325ebba844a091390b46e3540673476d8492db06f68f1bf70fa420ee9755beb22308276cdb1aa34abe5356a5dad8190c2f96
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Ladybug
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# A simple debugger using set_trace_func
|
2
|
+
# Allows for external control while in a breakpoint
|
3
|
+
|
4
|
+
module Ladybug
|
5
|
+
class Debugger
|
6
|
+
def initialize
|
7
|
+
@breakpoints = []
|
8
|
+
|
9
|
+
@to_main_thread = Queue.new
|
10
|
+
@from_main_thread = Queue.new
|
11
|
+
|
12
|
+
@on_pause = -> {}
|
13
|
+
@on_resume = -> {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
RubyVM::InstructionSequence.compile_option = {
|
18
|
+
trace_instruction: true
|
19
|
+
}
|
20
|
+
|
21
|
+
set_trace_func trace_func
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_pause(&block)
|
25
|
+
@on_pause = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_resume(&block)
|
29
|
+
@on_resume = block
|
30
|
+
end
|
31
|
+
|
32
|
+
def resume
|
33
|
+
@to_main_thread.push({ command: 'continue' })
|
34
|
+
end
|
35
|
+
|
36
|
+
def step_over
|
37
|
+
@to_main_thread.push({ command: 'step_over' })
|
38
|
+
end
|
39
|
+
|
40
|
+
def evaluate(expression)
|
41
|
+
@to_main_thread.push({
|
42
|
+
command: 'eval',
|
43
|
+
arguments: {
|
44
|
+
expression: expression
|
45
|
+
}
|
46
|
+
})
|
47
|
+
|
48
|
+
# Block on eval, returns result
|
49
|
+
@from_main_thread.pop
|
50
|
+
end
|
51
|
+
|
52
|
+
# returns a breakpoint ID
|
53
|
+
def set_breakpoint(filename:, line_number:)
|
54
|
+
breakpoint = {
|
55
|
+
filename: filename,
|
56
|
+
line_number: line_number,
|
57
|
+
id: "filename:#{line_number}"
|
58
|
+
}
|
59
|
+
|
60
|
+
@breakpoints.push(breakpoint)
|
61
|
+
|
62
|
+
breakpoint[:id]
|
63
|
+
end
|
64
|
+
|
65
|
+
def remove_breakpoint(breakpoint_id)
|
66
|
+
filename, line_number = breakpoint_id.split(":")
|
67
|
+
line_number = line_number.to_i
|
68
|
+
|
69
|
+
@breakpoints.delete_if { |bp| bp[:id] == breakpoint_id }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def trace_func
|
75
|
+
@step_over_file = nil
|
76
|
+
|
77
|
+
proc { |event, filename, line_number, id, binding, klass, *rest|
|
78
|
+
# This check is called a lot so perhaps worth making faster,
|
79
|
+
# but might not matter much with small number of breakpoints in practice
|
80
|
+
breakpoint_hit = @breakpoints.find do |bp|
|
81
|
+
bp[:filename] == filename && bp[:line_number] == line_number
|
82
|
+
end
|
83
|
+
|
84
|
+
break_on_step_over = (@step_over_file == filename)
|
85
|
+
|
86
|
+
if breakpoint_hit || break_on_step_over
|
87
|
+
local_variables =
|
88
|
+
binding.local_variables.each_with_object({}) do |lvar, hash|
|
89
|
+
hash[lvar] = binding.local_variable_get(lvar)
|
90
|
+
end
|
91
|
+
|
92
|
+
instance_variables =
|
93
|
+
binding.eval("instance_variables").each_with_object({}) do |ivar, hash|
|
94
|
+
hash[ivar] = binding.eval("instance_variable_get(:#{ivar})")
|
95
|
+
end
|
96
|
+
|
97
|
+
pause_info = {
|
98
|
+
breakpoint_id: breakpoint_hit ? breakpoint_hit[:id] : nil,
|
99
|
+
label: Kernel.caller_locations.first.base_label,
|
100
|
+
local_variables: local_variables,
|
101
|
+
instance_variables: instance_variables,
|
102
|
+
filename: filename,
|
103
|
+
line_number: line_number
|
104
|
+
# call_frames: []
|
105
|
+
|
106
|
+
# Call frames are pretty complicated...
|
107
|
+
# call_frames: Kernel.caller_locations.first(3).map do |call_frame|
|
108
|
+
# {
|
109
|
+
# callFrameId: SecureRandom.uuid,
|
110
|
+
# functionName: call_frame.base_label,
|
111
|
+
# scopeChain: [
|
112
|
+
# {
|
113
|
+
# type: "local",
|
114
|
+
# startLocation: ,
|
115
|
+
# endLocation:
|
116
|
+
# }
|
117
|
+
# ],
|
118
|
+
# url: "#{"http://rails.com"}/#{filename}",
|
119
|
+
# this:
|
120
|
+
# }
|
121
|
+
# end
|
122
|
+
}
|
123
|
+
|
124
|
+
@on_pause.call(pause_info)
|
125
|
+
|
126
|
+
loop do
|
127
|
+
# block until we get a command from the debugger thread
|
128
|
+
message = @to_main_thread.pop
|
129
|
+
|
130
|
+
case message[:command]
|
131
|
+
when 'continue'
|
132
|
+
@step_over_file = nil
|
133
|
+
break
|
134
|
+
when 'step_over'
|
135
|
+
# todo: does "step over" really mean break in the same file,
|
136
|
+
# or is there are a deeper meaning
|
137
|
+
@step_over_file = filename
|
138
|
+
break
|
139
|
+
when 'eval'
|
140
|
+
evaluated =
|
141
|
+
begin
|
142
|
+
binding.eval(message[:arguments][:expression])
|
143
|
+
rescue
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
@from_main_thread.push(evaluated)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Notify the debugger thread that we've resumed
|
151
|
+
@on_resume.call({})
|
152
|
+
end
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
require 'faye/websocket'
|
2
|
+
require 'json'
|
3
|
+
require 'thread'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
require 'ladybug/script_repository'
|
7
|
+
require 'ladybug/debugger'
|
8
|
+
require 'ladybug/object_manager'
|
9
|
+
|
10
|
+
# A Rack middleware that accepts a websocket connection from the Chrome
|
11
|
+
# Devtools UI and responds to requests, interacting with Ladybug::Debugger
|
12
|
+
module Ladybug
|
13
|
+
class Middleware
|
14
|
+
def initialize(app)
|
15
|
+
@app = app
|
16
|
+
@scripts = {}
|
17
|
+
|
18
|
+
@script_repository = ScriptRepository.new
|
19
|
+
@debugger = Debugger.new
|
20
|
+
@object_manager = ObjectManager.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
puts "Debug in Chrome: chrome-devtools://devtools/bundled/inspector.html?ws=#{env['HTTP_HOST']}"
|
25
|
+
|
26
|
+
# For now, all websocket connections are assumed to be a debug connection
|
27
|
+
if Faye::WebSocket.websocket?(env)
|
28
|
+
ws = create_websocket(env)
|
29
|
+
|
30
|
+
# Return async Rack response
|
31
|
+
ws.rack_response
|
32
|
+
else
|
33
|
+
@app.call(env)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def create_websocket(env)
|
40
|
+
ws = Faye::WebSocket.new(env)
|
41
|
+
|
42
|
+
ws.on :message do |event|
|
43
|
+
# The WebSockets library silently swallows errors.
|
44
|
+
# Insert our own error handling for debugging purposes.
|
45
|
+
|
46
|
+
begin
|
47
|
+
data = JSON.parse(event.data)
|
48
|
+
|
49
|
+
if data["method"] == "Page.getResourceTree"
|
50
|
+
result = {
|
51
|
+
frameTree: {
|
52
|
+
frame: {
|
53
|
+
id: "123",
|
54
|
+
loaderId: "123",
|
55
|
+
mimeType: "text/plain",
|
56
|
+
securityOrigin: "http://localhost",
|
57
|
+
url: "http://localhost"
|
58
|
+
},
|
59
|
+
resources: @script_repository.all.map do |script|
|
60
|
+
{
|
61
|
+
mimeType: "text/plain",
|
62
|
+
type: "Script",
|
63
|
+
contentSize: script.size,
|
64
|
+
lastModified: script.last_modified_time.to_i,
|
65
|
+
url: script.virtual_url
|
66
|
+
}
|
67
|
+
end
|
68
|
+
}
|
69
|
+
}
|
70
|
+
elsif data["method"] == "Page.getResourceContent"
|
71
|
+
result = {
|
72
|
+
base64Encoded: false,
|
73
|
+
content: "hello world"
|
74
|
+
}
|
75
|
+
elsif data["method"] == "Debugger.getScriptSource"
|
76
|
+
script_id = data["params"]["scriptId"]
|
77
|
+
path = @script_repository.find(id: script_id).path
|
78
|
+
result = {
|
79
|
+
scriptSource: File.new(path, "r").read
|
80
|
+
}
|
81
|
+
elsif data["method"] == "Debugger.getPossibleBreakpoints"
|
82
|
+
# Just echo back the location the browser requested
|
83
|
+
result = { locations: [ data["params"]["start"] ] }
|
84
|
+
elsif data["method"] == "Debugger.setBreakpointByUrl"
|
85
|
+
# Chrome gives us a virtual URL;
|
86
|
+
# we need an absolute path to the file to match the API for set_trace_func
|
87
|
+
script = @script_repository.find(virtual_url: data["params"]["url"])
|
88
|
+
|
89
|
+
# DevTools gives us 0-indexed line numbers but
|
90
|
+
# ruby uses 1-indexed line numbers
|
91
|
+
line_number = data["params"]["lineNumber"]
|
92
|
+
ruby_line_number = line_number + 1
|
93
|
+
|
94
|
+
puts "setting breakpoint on #{script.path}:#{ruby_line_number}"
|
95
|
+
|
96
|
+
breakpoint_id = @debugger.set_breakpoint(
|
97
|
+
filename: script.absolute_path,
|
98
|
+
line_number: ruby_line_number
|
99
|
+
)
|
100
|
+
|
101
|
+
result = {
|
102
|
+
breakpointId: breakpoint_id,
|
103
|
+
locations: [
|
104
|
+
{
|
105
|
+
scriptId: script.id,
|
106
|
+
lineNumber: line_number,
|
107
|
+
columnNumber: data["params"]["columnNumber"],
|
108
|
+
}
|
109
|
+
]
|
110
|
+
}
|
111
|
+
elsif data["method"] == "Debugger.resume"
|
112
|
+
# Synchronously just ack the command;
|
113
|
+
# we'll async hear back from the main thread when execution resumes
|
114
|
+
@debugger.resume
|
115
|
+
result = {}
|
116
|
+
elsif data["method"] == "Debugger.stepOver"
|
117
|
+
# Synchronously just ack the command;
|
118
|
+
# we'll async hear back from the main thread when execution resumes
|
119
|
+
@debugger.step_over
|
120
|
+
result = {}
|
121
|
+
elsif data["method"] == "Debugger.evaluateOnCallFrame"
|
122
|
+
evaluated = @debugger.evaluate(data["params"]["expression"])
|
123
|
+
result = {
|
124
|
+
result: @object_manager.serialize(evaluated)
|
125
|
+
}
|
126
|
+
elsif data["method"] == "Debugger.removeBreakpoint"
|
127
|
+
@debugger.remove_breakpoint(data["params"]["breakpointId"])
|
128
|
+
result = {}
|
129
|
+
elsif data["method"] == "Runtime.getProperties"
|
130
|
+
object = @object_manager.find(data["params"]["objectId"])
|
131
|
+
|
132
|
+
result = {
|
133
|
+
result: @object_manager.get_properties(object)
|
134
|
+
}
|
135
|
+
else
|
136
|
+
result = {}
|
137
|
+
end
|
138
|
+
|
139
|
+
response = {
|
140
|
+
id: data["id"],
|
141
|
+
result: result
|
142
|
+
}
|
143
|
+
|
144
|
+
ws.send(response.to_json)
|
145
|
+
|
146
|
+
# After we send a resource tree response, we need to send these
|
147
|
+
# messages as well to get the files to show up
|
148
|
+
if data["method"] == "Page.getResourceTree"
|
149
|
+
@script_repository.all.each do |script|
|
150
|
+
message = {
|
151
|
+
method: "Debugger.scriptParsed",
|
152
|
+
params: {
|
153
|
+
scriptId: script.id,
|
154
|
+
url: script.virtual_url,
|
155
|
+
startLine: 0,
|
156
|
+
startColumn: 0,
|
157
|
+
endLine: 100, #todo: really populate
|
158
|
+
endColumn: 100
|
159
|
+
}
|
160
|
+
}.to_json
|
161
|
+
|
162
|
+
ws.send(message)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
rescue => e
|
166
|
+
puts e.message
|
167
|
+
puts e.backtrace
|
168
|
+
|
169
|
+
raise e
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
ws.on :close do |event|
|
174
|
+
p [:close, event.code, event.reason]
|
175
|
+
ws = nil
|
176
|
+
end
|
177
|
+
|
178
|
+
# Spawn a thread to handle messages from the main thread
|
179
|
+
# and notify the client.
|
180
|
+
|
181
|
+
@debugger.on_pause do |info|
|
182
|
+
# Generate an object representing this scope
|
183
|
+
# (this is here not in the debugger because the debugger
|
184
|
+
# shouldn't need to know about the requirement for a virtual object)
|
185
|
+
virtual_scope_object =
|
186
|
+
info[:local_variables].merge(info[:instance_variables])
|
187
|
+
|
188
|
+
# Register the virtual object to give it an ID and hold a ref to it
|
189
|
+
object_id = @object_manager.register(virtual_scope_object)
|
190
|
+
|
191
|
+
script = @script_repository.find(absolute_path: info[:filename])
|
192
|
+
|
193
|
+
location = {
|
194
|
+
scriptId: script.id,
|
195
|
+
lineNumber: info[:line_number] - 1,
|
196
|
+
columnNumber: 0
|
197
|
+
}
|
198
|
+
|
199
|
+
msg_to_client = {
|
200
|
+
method: "Debugger.paused",
|
201
|
+
params: {
|
202
|
+
callFrames: [
|
203
|
+
{
|
204
|
+
location: location,
|
205
|
+
callFrameId: SecureRandom.uuid,
|
206
|
+
functionName: info[:label],
|
207
|
+
scopeChain: [
|
208
|
+
{
|
209
|
+
type: "local",
|
210
|
+
startLocation: location,
|
211
|
+
endLocation: location,
|
212
|
+
object: {
|
213
|
+
className: "Object",
|
214
|
+
description: "Object",
|
215
|
+
type: "object",
|
216
|
+
objectId: object_id
|
217
|
+
}
|
218
|
+
}
|
219
|
+
],
|
220
|
+
url: script.virtual_url
|
221
|
+
}
|
222
|
+
],
|
223
|
+
hitBreakpoints: info[:breakpoint_id] ? [info[:breakpoint_id]] : [],
|
224
|
+
reason: "other"
|
225
|
+
}
|
226
|
+
}
|
227
|
+
|
228
|
+
ws.send(msg_to_client.to_json)
|
229
|
+
end
|
230
|
+
|
231
|
+
@debugger.on_resume do
|
232
|
+
msg_to_client = {
|
233
|
+
method: "Debugger.resumed",
|
234
|
+
params: {}
|
235
|
+
}
|
236
|
+
|
237
|
+
ws.send(msg_to_client.to_json)
|
238
|
+
end
|
239
|
+
|
240
|
+
@debugger.start
|
241
|
+
|
242
|
+
ws
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Ladybug
|
2
|
+
# This class:
|
3
|
+
#
|
4
|
+
# * serializes objects for display in the Chrome UI
|
5
|
+
# * maintains references to all objects for which it has given out IDs for;
|
6
|
+
# this ensures they don't get GC'd and can be dereferenced later by ID.
|
7
|
+
#
|
8
|
+
# TODO: Handle Chrome's "release" APIs to enable releasing references
|
9
|
+
# at some point and avoid too much memory growth
|
10
|
+
|
11
|
+
class ObjectManager
|
12
|
+
def initialize
|
13
|
+
@objects = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Given an ID, return the object from our registry
|
17
|
+
def find(id)
|
18
|
+
@objects[id]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Given an object, register it in our internal state and
|
22
|
+
# return an ID for it
|
23
|
+
def register(object)
|
24
|
+
object_id = SecureRandom.uuid
|
25
|
+
@objects[object_id] = object
|
26
|
+
object_id
|
27
|
+
end
|
28
|
+
|
29
|
+
# Convert a Ruby object to a hash representing a Chrome RemoteObject
|
30
|
+
# https://chromedevtools.github.io/devtools-protocol/tot/Runtime#type-RemoteObject
|
31
|
+
def serialize(object)
|
32
|
+
case object
|
33
|
+
when String
|
34
|
+
{
|
35
|
+
type: "string",
|
36
|
+
value: object,
|
37
|
+
description: object
|
38
|
+
}
|
39
|
+
when Numeric
|
40
|
+
{
|
41
|
+
type: "number",
|
42
|
+
value: object,
|
43
|
+
description: object.to_s
|
44
|
+
}
|
45
|
+
when TrueClass, FalseClass
|
46
|
+
{
|
47
|
+
type: "boolean",
|
48
|
+
value: object,
|
49
|
+
description: object.to_s
|
50
|
+
}
|
51
|
+
when Symbol
|
52
|
+
{
|
53
|
+
type: "symbol",
|
54
|
+
value: object,
|
55
|
+
description: object.to_s
|
56
|
+
}
|
57
|
+
when Array
|
58
|
+
result = {
|
59
|
+
type: "object",
|
60
|
+
className: object.class.to_s,
|
61
|
+
description: "Array(#{object.length})",
|
62
|
+
objectId: register(object),
|
63
|
+
subtype: "array"
|
64
|
+
}
|
65
|
+
|
66
|
+
result.merge!(
|
67
|
+
preview: result.merge(
|
68
|
+
overflow: false,
|
69
|
+
properties: get_properties(object)
|
70
|
+
)
|
71
|
+
)
|
72
|
+
|
73
|
+
result
|
74
|
+
when nil
|
75
|
+
{
|
76
|
+
type: "object",
|
77
|
+
subtype: "null",
|
78
|
+
value: nil
|
79
|
+
}
|
80
|
+
else
|
81
|
+
{
|
82
|
+
type: "object",
|
83
|
+
className: object.class.to_s,
|
84
|
+
description: object.to_s,
|
85
|
+
objectId: register(object)
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Ruby objects don't have properties like JS objects do,
|
91
|
+
# so we need to figure out the best properties to show for non-primitives.
|
92
|
+
#
|
93
|
+
# We first give the object a chance to tell us its debug properties,
|
94
|
+
# then we fall back to handling a bunch of common cases,
|
95
|
+
# then finally we give up and just serialize its instance variables.
|
96
|
+
def get_properties(object)
|
97
|
+
if object.respond_to?(:chrome_debug_properties)
|
98
|
+
get_properties(object.chrome_debug_properties)
|
99
|
+
elsif object.is_a? Hash
|
100
|
+
object.
|
101
|
+
map do |key, value|
|
102
|
+
kv = {
|
103
|
+
name: key,
|
104
|
+
value: serialize(value)
|
105
|
+
}
|
106
|
+
|
107
|
+
kv
|
108
|
+
end.
|
109
|
+
reject { |property| property[:value].nil? }
|
110
|
+
elsif object.is_a? Array
|
111
|
+
object.map.with_index do |element, index|
|
112
|
+
{
|
113
|
+
name: index.to_s,
|
114
|
+
value: serialize(element)
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
# TODO: This section is too magical,
|
119
|
+
# better to let users just define their own chrome_debug_properties
|
120
|
+
# and then add an optional Rails plugin to the gem which
|
121
|
+
# monkey patches rails classes.
|
122
|
+
elsif object.respond_to?(:to_a)
|
123
|
+
get_properties(object.to_a)
|
124
|
+
elsif object.respond_to?(:attributes)
|
125
|
+
get_properties(object.attributes)
|
126
|
+
elsif object.respond_to?(:to_hash)
|
127
|
+
get_properties(object.to_hash)
|
128
|
+
elsif object.respond_to?(:to_h)
|
129
|
+
get_properties(object.to_h)
|
130
|
+
else
|
131
|
+
ivar_hash = object.instance_variables.each_with_object({}) do |ivar, hash|
|
132
|
+
hash[ivar] = object.instance_variable_get(ivar)
|
133
|
+
end
|
134
|
+
|
135
|
+
get_properties(ivar_hash)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# A repository of the source files for the app.
|
2
|
+
# The Chrome Devtools API switches back and forth between different
|
3
|
+
# references to a source file; this repository enables lookup by
|
4
|
+
# different attributes and conversion between them.
|
5
|
+
|
6
|
+
module Ladybug
|
7
|
+
class ScriptRepository
|
8
|
+
# todo: would be nice to dynamically set this to the server name
|
9
|
+
ROOT_URL = "http://localhost"
|
10
|
+
|
11
|
+
# todo: accept path as param?
|
12
|
+
def initialize
|
13
|
+
@scripts = enumerate_scripts
|
14
|
+
end
|
15
|
+
|
16
|
+
def all
|
17
|
+
@scripts
|
18
|
+
end
|
19
|
+
|
20
|
+
def find(args)
|
21
|
+
@scripts.find do |script|
|
22
|
+
args.all? do |key, value|
|
23
|
+
script[key] == value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Return a list of Scripts with all attributes populated
|
31
|
+
def enumerate_scripts
|
32
|
+
Dir.glob("**/*").
|
33
|
+
reject { |f| File.directory?(f) }.
|
34
|
+
select { |f| File.extname(f) == ".rb" }.
|
35
|
+
map do |filename|
|
36
|
+
stat = File.stat(filename)
|
37
|
+
|
38
|
+
OpenStruct.new(
|
39
|
+
id: SecureRandom.uuid,
|
40
|
+
path: filename,
|
41
|
+
absolute_path: File.expand_path(filename),
|
42
|
+
virtual_url: "#{ROOT_URL}/#{filename}",
|
43
|
+
size: stat.size,
|
44
|
+
last_modified_time: stat.mtime
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
data/lib/ladybug.rb
ADDED
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ladybug
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.alpha
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Geoffrey Litt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faye-websocket
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Debug Ruby code using Chrome Devtools
|
28
|
+
email: gklitt@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- README.md
|
34
|
+
- lib/ladybug.rb
|
35
|
+
- lib/ladybug/debugger.rb
|
36
|
+
- lib/ladybug/middleware.rb
|
37
|
+
- lib/ladybug/object_manager.rb
|
38
|
+
- lib/ladybug/script_repository.rb
|
39
|
+
homepage: http://rubygems.org/gems/ladybug
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
metadata: {}
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 1.3.1
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 2.6.10
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: Ladybug
|
63
|
+
test_files: []
|