Class: Canopus::Debug::Breakpoints

Inherits:
Object
  • Object
show all
Defined in:
lib/canopus/debug/breakpoints.rb

Defined Under Namespace

Classes: Entry

Constant Summary collapse

VERSION =
2
RELATIVE_PATH =
File.join(".canopus", "breakpoints.json")
MAX_BYTES =
1_048_576
MAX_ENTRIES =
10_000
MAX_PATH_BYTES =
4_096
MAX_CONDITION_BYTES =
4_096
MAX_HIT_CONDITION_BYTES =
4_096
MAX_LOG_MESSAGE_BYTES =
4_096
MAX_LINE =
2_147_483_647

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root:, &on_change) ⇒ Breakpoints

Returns a new instance of Breakpoints.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/canopus/debug/breakpoints.rb', line 29

def initialize(root:, &on_change)
  @root = canonical_root(root)
  @on_change = on_change
  @attachments = {}
  @closed = false
  @entries = load_entries
  @state_lock = Mutex.new
  @save_lock, @save_wake = Mutex.new, ConditionVariable.new
  @save_generation = @saved_generation = 0
  @pending_save = @save_error = nil
  @saving = @save_stopping = false
  @save_worker = Thread.new { save_loop }
  @save_worker.name = "canopus-breakpoints"
end

Instance Attribute Details

#entries ⇒ Object (readonly)

Returns the value of attribute entries.



27
28
29
# File 'lib/canopus/debug/breakpoints.rb', line 27

def entries
  @entries
end

#root ⇒ Object (readonly)

Returns the value of attribute root.



27
28
29
# File 'lib/canopus/debug/breakpoints.rb', line 27

def root
  @root
end

Instance Method Details

#add(path, line, condition: nil, hit_condition: nil, log_message: nil, enabled: true) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
# File 'lib/canopus/debug/breakpoints.rb', line 49

def add(path, line, condition: nil, hit_condition: nil, log_message: nil, enabled: true)
  @state_lock.synchronize do
    ensure_open!
    entry = build_entry(path, line, condition, hit_condition, log_message, enabled)
    existing = entries.find { |item| item.path == entry.path && item.line == entry.line }
    return existing if existing == entry
    replacement = entries.reject { |item| item.path == entry.path && item.line == entry.line }
    commit([*replacement, entry], changed_path: entry.path)
    entry
  end
end

#attach(buffer) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/canopus/debug/breakpoints.rb', line 97

def attach(buffer)
  @state_lock.synchronize do
    ensure_open!
    raise Error, "breakpoints require a file-backed buffer" unless buffer.is_a?(Buffer) && buffer.path
    return self if @attachments.key?(buffer)
    path = normalize_path(buffer.path)
    ensure_attachment_path_available!(buffer, path)
    attachment = {snapshots: ObjectSpace::WeakMap.new, states: {}.compare_by_identity,
                  path: path, revision: 0, gc_count: GC.count}
    store_snapshot(attachment, buffer.rope, entries_for(path))
    subscription = buffer.on_edit { |patch| edited(buffer, patch) }
    attachment[:subscription] = subscription
    @attachments[buffer] = attachment
  end
  self
end

#close ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/canopus/debug/breakpoints.rb', line 121

def close
  attachments = @state_lock.synchronize do
    return nil if @closed
    @closed = true
    values = @attachments.values
    @attachments.clear
    values
  end
  attachments.each { |attachment| attachment.fetch(:subscription).detach }
  flush
  nil
ensure
  stop_save_worker if attachments
end

#detach(buffer) ⇒ Object



114
115
116
117
118
119
# File 'lib/canopus/debug/breakpoints.rb', line 114

def detach(buffer)
  attachment = @state_lock.synchronize { @attachments.delete(buffer) }
  attachment&.fetch(:subscription)&.detach
  flush if attachment
  !attachment.nil?
end

#error ⇒ Object



149
150
151
# File 'lib/canopus/debug/breakpoints.rb', line 149

def error
  @save_lock.synchronize { @save_error&.last }
end

#flush ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/canopus/debug/breakpoints.rb', line 136

def flush
  target = @save_lock.synchronize { @save_generation }
  @save_lock.synchronize do
    until @saved_generation >= target
      if !@saving && !@pending_save && @save_error && @save_error.first >= target
        raise @save_error.last
      end
      @save_wake.wait(@save_lock)
    end
  end
  self
end

#for_path(path) ⇒ Object



44
45
46
47
# File 'lib/canopus/debug/breakpoints.rb', line 44

def for_path(path)
  relative = normalize_path(path)
  entries.select { |entry| entry.path == relative }.freeze
end

#remove(path, line) ⇒ Object



61
62
63
64
65
66
67
68
69
70
# File 'lib/canopus/debug/breakpoints.rb', line 61

def remove(path, line)
  @state_lock.synchronize do
    ensure_open!
    relative, line = normalize_path(path), validate_line(line)
    replacement = entries.reject { |entry| entry.path == relative && entry.line == line }
    return false if replacement.length == entries.length
    commit(replacement, changed_path: relative)
    true
  end
end

#toggle(path, line, condition: nil, hit_condition: nil, log_message: nil, enabled: true) ⇒ Object



72
73
74
75
76
# File 'lib/canopus/debug/breakpoints.rb', line 72

def toggle(path, line, condition: nil, hit_condition: nil, log_message: nil, enabled: true)
  existing = for_path(path).find { |entry| entry.line == line }
  return nil if existing && remove(path, line)
  add(path, line, condition: condition, hit_condition: hit_condition, log_message: log_message, enabled: enabled)
end

#update(path, line, new_line: line, condition: UNSET, hit_condition: UNSET, log_message: UNSET, enabled: UNSET) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/canopus/debug/breakpoints.rb', line 78

def update(path, line, new_line: line, condition: UNSET, hit_condition: UNSET, log_message: UNSET, enabled: UNSET)
  @state_lock.synchronize do
    ensure_open!
    relative, line = normalize_path(path), validate_line(line)
    current = entries.find { |entry| entry.path == relative && entry.line == line }
    raise Error, "breakpoint does not exist" unless current
    condition = current.condition if condition.equal?(UNSET)
    hit_condition = current.hit_condition if hit_condition.equal?(UNSET)
    log_message = current.log_message if log_message.equal?(UNSET)
    enabled = current.enabled if enabled.equal?(UNSET)
    replacement = build_entry(relative, new_line, condition, hit_condition, log_message, enabled)
    collision = entries.any? { |entry| !entry.equal?(current) && entry.path == relative && entry.line == replacement.line }
    raise Error, "breakpoint already exists" if collision
    return current if current == replacement
    commit(entries.map { |entry| entry.equal?(current) ? replacement : entry }, changed_path: relative)
    replacement
  end
end