pwn 0.5.740 → 0.5.741
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 +4 -4
- data/Gemfile +1 -1
- data/etc/default_skills/pwn/plugins/sandbox/driver/SKILL.md +8 -1
- data/lib/pwn/plugins/repl/mesh.rb +129 -8
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +6 -0
- data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +122 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3eb74de135994d78ce8af6b31729ef0622495f0cbe6ff72ddb5ecce5662a5aa7
|
|
4
|
+
data.tar.gz: 3f2d855056ef2cce6424ac399393e38492970850baa36fde37419d5e798ca2f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 995de4dfb6c55750ed90c2f8c282c4d6ca14e876f41ec207b71aecdc1d162ab69359a20d3d479115c4aea9b9eee7d04d9c3bd1678ff73d1e1c06dbefcbef7c46
|
|
7
|
+
data.tar.gz: 30c2d6602caa5d1fef740508751d4457d69c7f51d861a6260e57f65251c5050d2425c21dad3e946181aa1776345f413e2a035ec3bcf411ae4eedce21441d61d4
|
data/Gemfile
CHANGED
|
@@ -28,13 +28,20 @@ Class methods take `(opts = {})` and read `opts`.
|
|
|
28
28
|
|
|
29
29
|
```ruby
|
|
30
30
|
PWNSandboxDriver.help
|
|
31
|
-
PWNSandboxDriver.
|
|
31
|
+
PWNSandboxDriver.available?(opts)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Public methods
|
|
35
35
|
|
|
36
36
|
- `authors`
|
|
37
37
|
- `help`
|
|
38
|
+
- `available?`
|
|
39
|
+
- `bounded`
|
|
40
|
+
- `execute`
|
|
41
|
+
- `fuzz`
|
|
42
|
+
- `inside`
|
|
43
|
+
- `main`
|
|
44
|
+
- `snapshot`
|
|
38
45
|
|
|
39
46
|
## Source
|
|
40
47
|
|
|
@@ -6,6 +6,7 @@ require 'json'
|
|
|
6
6
|
require 'base64'
|
|
7
7
|
require 'pry'
|
|
8
8
|
require 'reline'
|
|
9
|
+
require 'io/wait'
|
|
9
10
|
|
|
10
11
|
module PWN
|
|
11
12
|
module Plugins
|
|
@@ -134,10 +135,12 @@ module PWN
|
|
|
134
135
|
|
|
135
136
|
# Curses owns input and rendering until this command returns to Pry.
|
|
136
137
|
PWN.const_set(:MeshEvents, Queue.new)
|
|
137
|
-
PWN::Plugins::REPL.send(:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
PWN::Plugins::REPL.send(:mesh_capture_output, run: proc {
|
|
139
|
+
PWN::Plugins::REPL.send(:mesh_start_rx!, env: meshtastic_env, obj: mesh_obj)
|
|
140
|
+
PWN::Plugins::REPL.send(:mesh_refresh_ui!, env: meshtastic_env)
|
|
141
|
+
PWN::Plugins::REPL.send(:mesh_ui_puts, text: 'Ready. /menu opens settings. Incoming channel messages appear here.')
|
|
142
|
+
PWN::Plugins::REPL.send(:mesh_console_loop, pry: pi)
|
|
143
|
+
})
|
|
141
144
|
rescue StandardError => e
|
|
142
145
|
raise e
|
|
143
146
|
ensure
|
|
@@ -146,6 +149,15 @@ module PWN
|
|
|
146
149
|
end
|
|
147
150
|
end
|
|
148
151
|
PWN_MESH_TRANSPORTS = %i[serial bluetooth tcp mqtt].freeze
|
|
152
|
+
# meshtastic 0.0.184 loses protobuf defaults in Data#to_h. Empty
|
|
153
|
+
# decoded data is not an unsupported application payload.
|
|
154
|
+
EMPTY_DATA_DECODER = Module.new do
|
|
155
|
+
define_method(:decode_payload) do |opts = {}|
|
|
156
|
+
return nil if opts[:payload].nil? && opts[:msg_type].nil?
|
|
157
|
+
|
|
158
|
+
super(opts)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
149
161
|
|
|
150
162
|
# Meshtastic transport for pwn-mesh: auto (probe order) or a pinned kind.
|
|
151
163
|
class << PWN::Plugins::REPL # rubocop:disable Metrics/ClassLength
|
|
@@ -557,6 +569,7 @@ module PWN
|
|
|
557
569
|
|
|
558
570
|
# Subscribe for inbound TEXT_MESSAGE_APP frames.
|
|
559
571
|
def mesh_subscribe(opts = {})
|
|
572
|
+
Meshtastic::MeshInterface.prepend(Mesh::EMPTY_DATA_DECODER) unless Meshtastic::MeshInterface.ancestors.include?(Mesh::EMPTY_DATA_DECODER)
|
|
560
573
|
env = opts[:env] || {}
|
|
561
574
|
obj = opts[:obj]
|
|
562
575
|
psks = opts[:psks]
|
|
@@ -1002,7 +1015,13 @@ module PWN
|
|
|
1002
1015
|
event = PWN::MeshEvents.pop(true)
|
|
1003
1016
|
next if event[:obj] && (!PWN.const_defined?(:MeshObj) || !event[:obj].equal?(PWN::MeshObj))
|
|
1004
1017
|
|
|
1005
|
-
|
|
1018
|
+
if event[:notice]
|
|
1019
|
+
mesh_notice(text: event[:notice])
|
|
1020
|
+
elsif event[:msg]
|
|
1021
|
+
mesh_handle_rx(msg: event[:msg])
|
|
1022
|
+
else
|
|
1023
|
+
mesh_ui_puts(text: event[:text])
|
|
1024
|
+
end
|
|
1006
1025
|
end
|
|
1007
1026
|
rescue ThreadError
|
|
1008
1027
|
nil
|
|
@@ -1059,6 +1078,10 @@ module PWN
|
|
|
1059
1078
|
|
|
1060
1079
|
def mesh_ui_puts(opts = {})
|
|
1061
1080
|
text = opts[:text].to_s
|
|
1081
|
+
if PWN.const_defined?(:MeshEvents) && text.match?(/warning|error|failed|disconnect|persist skipped|unknown command/i)
|
|
1082
|
+
PWN::MeshEvents << { notice: text }
|
|
1083
|
+
return text
|
|
1084
|
+
end
|
|
1062
1085
|
if PWN.const_defined?(:MeshRxBodyWin) && PWN.const_defined?(:MeshMutex)
|
|
1063
1086
|
win = PWN.const_get(:MeshRxBodyWin)
|
|
1064
1087
|
mutex = PWN.const_get(:MeshMutex)
|
|
@@ -1079,6 +1102,79 @@ module PWN
|
|
|
1079
1102
|
text
|
|
1080
1103
|
end
|
|
1081
1104
|
|
|
1105
|
+
def mesh_capture_output(opts = {})
|
|
1106
|
+
original_out = $stdout
|
|
1107
|
+
original_err = $stderr
|
|
1108
|
+
input, output = IO.pipe
|
|
1109
|
+
output.sync = true
|
|
1110
|
+
events = PWN::MeshEvents
|
|
1111
|
+
collector = Thread.new do
|
|
1112
|
+
loop do
|
|
1113
|
+
text = input.readpartial(4096)
|
|
1114
|
+
begin
|
|
1115
|
+
text << input.readpartial(4096) while text.bytesize < 16_384 && input.wait_readable(0.02)
|
|
1116
|
+
rescue EOFError
|
|
1117
|
+
# Flush the last diagnostic even when the TUI is closing.
|
|
1118
|
+
end
|
|
1119
|
+
events << { notice: text.force_encoding(Encoding::UTF_8).scrub }
|
|
1120
|
+
end
|
|
1121
|
+
rescue IOError
|
|
1122
|
+
nil
|
|
1123
|
+
end
|
|
1124
|
+
$stdout = output
|
|
1125
|
+
$stderr = output
|
|
1126
|
+
opts[:run].call
|
|
1127
|
+
ensure
|
|
1128
|
+
$stdout = original_out
|
|
1129
|
+
$stderr = original_err
|
|
1130
|
+
output&.close
|
|
1131
|
+
collector&.join(1)
|
|
1132
|
+
collector&.kill
|
|
1133
|
+
input&.close
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
def mesh_notice(opts = {})
|
|
1137
|
+
width = [Curses.cols - 2, 80].min
|
|
1138
|
+
lines = mesh_wrap_text(text: opts[:text].to_s, width: [width - 4, 1].max)
|
|
1139
|
+
height = [lines.size + 5, Curses.lines - 2].min
|
|
1140
|
+
win = Curses::Window.new(height, width, (Curses.lines - height) / 2, (Curses.cols - width) / 2)
|
|
1141
|
+
win.keypad(true)
|
|
1142
|
+
offset = 0
|
|
1143
|
+
loop do
|
|
1144
|
+
win.erase
|
|
1145
|
+
mesh_box!(win: win)
|
|
1146
|
+
win.setpos(0, 2)
|
|
1147
|
+
win.attron(Curses.color_pair(20) | Curses::A_BOLD)
|
|
1148
|
+
win.addstr(' NOTICE ')
|
|
1149
|
+
win.attroff(Curses.color_pair(20) | Curses::A_BOLD)
|
|
1150
|
+
lines.drop(offset).first([height - 4, 1].max).each_with_index do |line, index|
|
|
1151
|
+
win.setpos(index + 1, 2)
|
|
1152
|
+
win.addstr(line)
|
|
1153
|
+
end
|
|
1154
|
+
win.setpos(height - 2, (width - 2) / 2)
|
|
1155
|
+
win.attron(Curses.color_pair(20) | Curses::A_BOLD | Curses::A_REVERSE)
|
|
1156
|
+
win.addstr('Ok')
|
|
1157
|
+
win.attroff(Curses.color_pair(20) | Curses::A_BOLD | Curses::A_REVERSE)
|
|
1158
|
+
win.refresh
|
|
1159
|
+
key = opts[:getch] ? opts[:getch].call : win.getch
|
|
1160
|
+
break if [10, 13, "\n", "\r", Curses::KEY_ENTER].include?(key)
|
|
1161
|
+
|
|
1162
|
+
offset = [offset - 1, 0].max if key == Curses::KEY_UP
|
|
1163
|
+
offset += 1 if key == Curses::KEY_DOWN && offset < lines.size - (height - 4)
|
|
1164
|
+
end
|
|
1165
|
+
ensure
|
|
1166
|
+
win&.close
|
|
1167
|
+
%i[MeshRxHeaderWin MeshRxFrameWin MeshRxBodyWin MeshTxWin].each do |name|
|
|
1168
|
+
next unless PWN.const_defined?(name)
|
|
1169
|
+
|
|
1170
|
+
pane = PWN.const_get(name)
|
|
1171
|
+
pane.touch
|
|
1172
|
+
pane.refresh
|
|
1173
|
+
end
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
private :mesh_capture_output, :mesh_notice
|
|
1177
|
+
|
|
1082
1178
|
# Drop the submitted TX buffer so the next prompt is empty (Reline keeps the old line).
|
|
1083
1179
|
def mesh_reset_input!(opts = {})
|
|
1084
1180
|
return opts if PWN.const_defined?(:MeshEvents)
|
|
@@ -1264,7 +1360,25 @@ module PWN
|
|
|
1264
1360
|
psks = mesh_active_psks(env: env) if psks.empty?
|
|
1265
1361
|
PWN.const_set(:MeshRxState, { last_from: nil, last_line: nil }) unless PWN.const_defined?(:MeshRxState)
|
|
1266
1362
|
events = PWN.const_defined?(:MeshEvents) ? PWN::MeshEvents : Queue.new
|
|
1363
|
+
kind = mesh_bound_transport(env: env)
|
|
1364
|
+
interval = Float(opts.fetch(:heartbeat_interval, 30))
|
|
1365
|
+
raise ArgumentError, 'heartbeat interval must be positive and finite' unless interval.positive? && interval.finite?
|
|
1366
|
+
|
|
1267
1367
|
thread = Thread.new do
|
|
1368
|
+
heartbeat = if obj.is_a?(Hash) && %i[serial bluetooth tcp].include?(kind)
|
|
1369
|
+
Thread.new do
|
|
1370
|
+
transport = { serial: Meshtastic::Serial, bluetooth: Meshtastic::Bluetooth, tcp: Meshtastic::TCP }.fetch(kind)
|
|
1371
|
+
bytes = Meshtastic::ToRadio.new(heartbeat: Meshtastic::Heartbeat.new(nonce: 0)).to_proto
|
|
1372
|
+
loop do
|
|
1373
|
+
break if obj[:closing]
|
|
1374
|
+
|
|
1375
|
+
transport.send_to_radio({ "#{kind}_obj": obj, to_radio: bytes })
|
|
1376
|
+
sleep interval
|
|
1377
|
+
end
|
|
1378
|
+
rescue StandardError => e
|
|
1379
|
+
events << { text: "RX keepalive failed: #{e.class}: #{e.message}", obj: obj }
|
|
1380
|
+
end
|
|
1381
|
+
end
|
|
1268
1382
|
mesh_subscribe(
|
|
1269
1383
|
env: env,
|
|
1270
1384
|
obj: obj,
|
|
@@ -1276,6 +1390,9 @@ module PWN
|
|
|
1276
1390
|
events << { text: 'RX stopped: transport stream closed. Use /transport to reconnect.', obj: obj }
|
|
1277
1391
|
rescue StandardError => e
|
|
1278
1392
|
events << { text: "RX failed: #{e.class}: #{e.message}", obj: obj }
|
|
1393
|
+
ensure
|
|
1394
|
+
heartbeat&.kill
|
|
1395
|
+
heartbeat&.join
|
|
1279
1396
|
end
|
|
1280
1397
|
PWN.send(:remove_const, :MeshSubThread) if PWN.const_defined?(:MeshSubThread)
|
|
1281
1398
|
PWN.const_set(:MeshSubThread, thread)
|
|
@@ -1546,13 +1663,17 @@ module PWN
|
|
|
1546
1663
|
if decoded[:emoji].to_i.zero?
|
|
1547
1664
|
messages[[scope, id]] = { from: opts[:from], text: text } if id.positive?
|
|
1548
1665
|
messages.shift while messages.size > 1000
|
|
1549
|
-
return text
|
|
1666
|
+
return text unless decoded[:reply_id].to_i.positive?
|
|
1550
1667
|
end
|
|
1551
1668
|
|
|
1552
1669
|
original = messages[[scope, decoded[:reply_id].to_i]]
|
|
1553
1670
|
original ||= mesh_reaction_original(reply_id: decoded[:reply_id], index: opts[:index])
|
|
1554
1671
|
target = original ? "#{original[:from]} >> #{original[:text]}" : "original message unavailable (packet #{decoded[:reply_id].to_i})"
|
|
1555
|
-
|
|
1672
|
+
if decoded[:emoji].to_i.positive?
|
|
1673
|
+
"Reacted to: \"#{target}\" with: #{text}."
|
|
1674
|
+
else
|
|
1675
|
+
"Replied to: \"#{target}\" with: #{text}"
|
|
1676
|
+
end
|
|
1556
1677
|
end
|
|
1557
1678
|
|
|
1558
1679
|
def mesh_reaction_original(opts = {})
|
|
@@ -2058,7 +2179,7 @@ module PWN
|
|
|
2058
2179
|
end
|
|
2059
2180
|
true
|
|
2060
2181
|
rescue StandardError => e
|
|
2061
|
-
|
|
2182
|
+
mesh_ui_puts(text: "[pwn-mesh] persist skipped: #{e.class}: #{e.message}")
|
|
2062
2183
|
false
|
|
2063
2184
|
end
|
|
2064
2185
|
|
data/lib/pwn/version.rb
CHANGED
|
@@ -80,6 +80,10 @@ describe PWN::Plugins::REPL, 'mesh packet channel routing' do
|
|
|
80
80
|
expect(win).to have_received(:addstr).with(" Reacted to: \"!aabbccdd >> Meet at noon\" with: 👍.\n")
|
|
81
81
|
expect(win).to have_received(:attron).with((local ? 23 : 21) | Curses::A_BOLD).at_least(:once)
|
|
82
82
|
expect(described_class).to have_received(:mesh_maybe_dispatch_to_pwn_ai).once
|
|
83
|
+
reply = Meshtastic::MeshPacket.new(id: 125, from: 0xb0b, to: 0xffffffff, channel: 1,
|
|
84
|
+
decoded: { portnum: :TEXT_MESSAGE_APP, payload: 'See you there!', reply_id: 123 }).to_h
|
|
85
|
+
described_class.send(:mesh_handle_rx, local: local, msg: { packet: reply })
|
|
86
|
+
expect(win).to have_received(:addstr).with(" Replied to: \"!aabbccdd >> Meet at noon\" with: See you there!\n")
|
|
83
87
|
end
|
|
84
88
|
end
|
|
85
89
|
|
|
@@ -140,6 +144,8 @@ describe PWN::Plugins::REPL, 'mesh packet channel routing' do
|
|
|
140
144
|
text = described_class.send(:mesh_reaction_text, packet: packet, text: '👍', channel: 'LongFast', index: 1)
|
|
141
145
|
expect(text).to eq('Reacted to: "original message unavailable (packet 999)" with: 👍.')
|
|
142
146
|
packet[:decoded][:emoji] = 0
|
|
147
|
+
expect(described_class.send(:mesh_reaction_text, packet: packet, text: 'Reply')).to eq('Replied to: "original message unavailable (packet 999)" with: Reply')
|
|
148
|
+
packet[:decoded].delete(:reply_id)
|
|
143
149
|
expect(described_class.send(:mesh_reaction_text, packet: packet, text: '👍')).to eq('👍')
|
|
144
150
|
end
|
|
145
151
|
|
|
@@ -1,8 +1,130 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'meshtastic'
|
|
4
5
|
|
|
5
6
|
describe PWN::Plugins::REPL, 'mesh compose history' do
|
|
7
|
+
it 'does not warn when the real subscriber sees an empty protobuf Data message' do
|
|
8
|
+
stub_const('PWN::MeshTransport', :serial)
|
|
9
|
+
queue = Queue.new
|
|
10
|
+
encoded = Meshtastic::FromRadio.new(packet: { decoded: Meshtastic::Data.new }).to_proto
|
|
11
|
+
queue << Meshtastic::FromRadio.decode(encoded)
|
|
12
|
+
queue << Meshtastic::FromRadio.new(queueStatus: { free: 1 })
|
|
13
|
+
queue.close
|
|
14
|
+
received = []
|
|
15
|
+
output = StringIO.new
|
|
16
|
+
old = $stdout
|
|
17
|
+
$stdout = output
|
|
18
|
+
described_class.send(:mesh_subscribe, obj: { from_radio_queue: queue }, psks: {}, on_message: proc { |message| received << message })
|
|
19
|
+
expect(received.size).to eq(2)
|
|
20
|
+
expect(output.string).not_to include("Can't decode")
|
|
21
|
+
Meshtastic::MeshInterface.new.decode_payload(payload: 'unsupported', msg_type: :PRIVATE_APP)
|
|
22
|
+
expect(output.string).to include("Can't decode", 'unsupported')
|
|
23
|
+
ensure
|
|
24
|
+
$stdout = old
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'queues warnings for acknowledgement instead of writing through the TUI' do
|
|
28
|
+
events = Queue.new
|
|
29
|
+
stub_const('PWN::MeshEvents', events)
|
|
30
|
+
expect(described_class).not_to receive(:puts)
|
|
31
|
+
described_class.send(:mesh_ui_puts, text: 'WARNING: fixture warning')
|
|
32
|
+
expect(events.pop(true)).to eq(notice: 'WARNING: fixture warning')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'displays queued diagnostics on the UI thread' do
|
|
36
|
+
events = Queue.new
|
|
37
|
+
stub_const('PWN::MeshEvents', events)
|
|
38
|
+
events << { notice: 'RX failed: fixture' }
|
|
39
|
+
expect(described_class).to receive(:mesh_notice).with(text: 'RX failed: fixture')
|
|
40
|
+
described_class.send(:mesh_drain_events)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'captures dependency stdout and stderr and restores both streams on failure' do
|
|
44
|
+
events = Queue.new
|
|
45
|
+
stub_const('PWN::MeshEvents', events)
|
|
46
|
+
original = [$stdout, $stderr]
|
|
47
|
+
expect do
|
|
48
|
+
described_class.send(:mesh_capture_output, run: proc {
|
|
49
|
+
puts 'WARNING: decoder fixture'
|
|
50
|
+
warn 'ERROR: transport fixture'
|
|
51
|
+
raise IOError, 'fixture'
|
|
52
|
+
})
|
|
53
|
+
end.to raise_error(IOError, 'fixture')
|
|
54
|
+
expect([$stdout, $stderr]).to eq(original)
|
|
55
|
+
output = []
|
|
56
|
+
output << events.pop(true)[:notice] until events.empty?
|
|
57
|
+
expect(output.join).to include('WARNING: decoder fixture', 'ERROR: transport fixture')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'centers a reverse-colored Ok button and repaints the underlying panes' do
|
|
61
|
+
win = double('notice', maxx: 78, maxy: 6)
|
|
62
|
+
%i[keypad erase setpos addstr refresh close attron attroff].each { |method| allow(win).to receive(method) }
|
|
63
|
+
allow(Curses).to receive(:color_pair).with(20).and_return(5120)
|
|
64
|
+
allow(Curses).to receive(:cols).and_return(80)
|
|
65
|
+
allow(Curses).to receive(:lines).and_return(24)
|
|
66
|
+
allow(Curses::Window).to receive(:new).and_return(win)
|
|
67
|
+
pane = double('pane', touch: nil, refresh: nil)
|
|
68
|
+
stub_const('PWN::MeshTxWin', pane)
|
|
69
|
+
keys = ["\e", 'q', "\n"]
|
|
70
|
+
described_class.send(:mesh_notice, text: 'Decoder warning', getch: proc { keys.shift })
|
|
71
|
+
expect(keys).to be_empty
|
|
72
|
+
expect(win).to have_received(:addstr).with('Ok').exactly(3).times
|
|
73
|
+
expect(win).not_to have_received(:addstr).with(a_string_matching(/Acknowledged|Enter/))
|
|
74
|
+
expect(win).to have_received(:setpos).with(4, 38).exactly(3).times
|
|
75
|
+
expect(win).to have_received(:attron).with(5120 | Curses::A_BOLD | Curses::A_REVERSE).exactly(3).times
|
|
76
|
+
expect(win).to have_received(:attroff).with(5120 | Curses::A_BOLD | Curses::A_REVERSE).exactly(3).times
|
|
77
|
+
expect(win).to have_received(:close)
|
|
78
|
+
expect(pane).to have_received(:touch)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'keeps an idle PhoneAPI session alive and stops its heartbeat with the subscriber' do
|
|
82
|
+
events = Queue.new
|
|
83
|
+
beats = Queue.new
|
|
84
|
+
release = Queue.new
|
|
85
|
+
obj = {}
|
|
86
|
+
stub_const('PWN::MeshEvents', events)
|
|
87
|
+
stub_const('PWN::MeshObj', obj)
|
|
88
|
+
stub_const('PWN::MeshTransport', :serial)
|
|
89
|
+
allow(Meshtastic::Serial).to receive(:send_to_radio) do |args|
|
|
90
|
+
beats << [Thread.current, Meshtastic::ToRadio.decode(args[:to_radio])]
|
|
91
|
+
end
|
|
92
|
+
allow(described_class).to receive(:mesh_subscribe) do |args|
|
|
93
|
+
release.pop
|
|
94
|
+
args[:on_message].call(packet: { decoded: { portnum: 1, payload: 'after idle' } })
|
|
95
|
+
end
|
|
96
|
+
thread = described_class.send(:mesh_start_rx!, env: { channel: {} }, obj: obj, heartbeat_interval: 0.01)
|
|
97
|
+
first = beats.pop(timeout: 1)
|
|
98
|
+
second = beats.pop(timeout: 1)
|
|
99
|
+
expect(first).not_to be_nil
|
|
100
|
+
expect(second).not_to be_nil
|
|
101
|
+
expect(second.last.payload_variant).to eq(:heartbeat)
|
|
102
|
+
expect(second.last.heartbeat.nonce).to eq(0)
|
|
103
|
+
release << true
|
|
104
|
+
expect(thread.join(1)).to eq(thread)
|
|
105
|
+
expect(first.first).not_to be_alive
|
|
106
|
+
expect(events.pop[:msg].dig(:packet, :decoded, :payload)).to eq('after idle')
|
|
107
|
+
ensure
|
|
108
|
+
thread&.kill
|
|
109
|
+
thread&.join(1)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'drains incoming messages during idle input polls without a keypress' do
|
|
113
|
+
pi = double('pry', config: double('config', pwn_mesh: true))
|
|
114
|
+
events = Queue.new
|
|
115
|
+
stub_const('PWN::MeshEvents', events)
|
|
116
|
+
message = { packet: { decoded: { portnum: 1, payload: 'idle RX' } } }
|
|
117
|
+
polls = 0
|
|
118
|
+
allow(described_class).to receive(:mesh_draw_input)
|
|
119
|
+
expect(described_class).to receive(:mesh_handle_rx).with(msg: message)
|
|
120
|
+
reader = proc do
|
|
121
|
+
polls += 1
|
|
122
|
+
events << { msg: message } if polls == 20
|
|
123
|
+
polls > 20 ? "\u0004" : nil
|
|
124
|
+
end
|
|
125
|
+
described_class.send(:mesh_console_loop, pry: pi, getch: reader)
|
|
126
|
+
end
|
|
127
|
+
|
|
6
128
|
it 'recalls submissions with Up/Down and restores the unfinished draft and cursor' do
|
|
7
129
|
config = double('config', pwn_mesh: true)
|
|
8
130
|
pry = double('pry', config: config)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pwn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.741
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -169,14 +169,14 @@ dependencies:
|
|
|
169
169
|
requirements:
|
|
170
170
|
- - '='
|
|
171
171
|
- !ruby/object:Gem::Version
|
|
172
|
-
version: 3.
|
|
172
|
+
version: 3.4.0
|
|
173
173
|
type: :runtime
|
|
174
174
|
prerelease: false
|
|
175
175
|
version_requirements: !ruby/object:Gem::Requirement
|
|
176
176
|
requirements:
|
|
177
177
|
- - '='
|
|
178
178
|
- !ruby/object:Gem::Version
|
|
179
|
-
version: 3.
|
|
179
|
+
version: 3.4.0
|
|
180
180
|
- !ruby/object:Gem::Dependency
|
|
181
181
|
name: colorize
|
|
182
182
|
requirement: !ruby/object:Gem::Requirement
|