ladybug 0.0.1.alpha
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|