vanken 0.1.0 → 0.2.0
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/CHANGELOG.md +15 -0
- data/README.md +26 -11
- data/data/coloring_rules.yml +67 -0
- data/docs/performance.md +10 -0
- data/docs/releases.md +2 -4
- data/docs/usage.md +51 -0
- data/lib/vanken/app/capture_controller.rb +66 -1
- data/lib/vanken/app/document.rb +102 -39
- data/lib/vanken/app/document_analysis.rb +75 -0
- data/lib/vanken/app/document_jobs.rb +52 -9
- data/lib/vanken/app/expert_info.rb +26 -0
- data/lib/vanken/app/io_graph.rb +47 -0
- data/lib/vanken/app/navigation.rb +77 -0
- data/lib/vanken/app/search_job.rb +62 -0
- data/lib/vanken/app/stats_job.rb +45 -0
- data/lib/vanken/capture/analyzer.rb +3 -1
- data/lib/vanken/capture/analyzer_process.rb +9 -6
- data/lib/vanken/capture/analyzer_worker.rb +2 -2
- data/lib/vanken/capture/filter_worker.rb +8 -2
- data/lib/vanken/capture/helper_main.rb +10 -3
- data/lib/vanken/capture/helper_options.rb +6 -2
- data/lib/vanken/capture/launcher.rb +1 -0
- data/lib/vanken/config/analysis_settings.rb +58 -0
- data/lib/vanken/config/columns.rb +138 -0
- data/lib/vanken/config/messages.rb +105 -0
- data/lib/vanken/config/preferences.rb +34 -8
- data/lib/vanken/config/profiles.rb +74 -0
- data/lib/vanken/config/sessions.rb +69 -0
- data/lib/vanken/config/yaml_file.rb +36 -0
- data/lib/vanken/core/coloring.rb +86 -0
- data/lib/vanken/core/frame_store.rb +2 -2
- data/lib/vanken/core/stores.rb +12 -0
- data/lib/vanken/gateway/detail_builder.rb +1 -1
- data/lib/vanken/gateway/display_capture_filter.rb +102 -0
- data/lib/vanken/gateway/dissector.rb +31 -6
- data/lib/vanken/gateway/exporter.rb +125 -0
- data/lib/vanken/gateway/file_writer.rb +26 -2
- data/lib/vanken/gateway/interfaces.rb +59 -0
- data/lib/vanken/gateway/resolver.rb +131 -0
- data/lib/vanken/gateway/statistics.rb +83 -0
- data/lib/vanken/gateway/stream.rb +159 -0
- data/lib/vanken/ui/actions.rb +54 -28
- data/lib/vanken/ui/analysis_dialogs.rb +371 -0
- data/lib/vanken/ui/application.rb +183 -9
- data/lib/vanken/ui/coloring_operations.rb +104 -0
- data/lib/vanken/ui/column_operations.rb +115 -0
- data/lib/vanken/ui/dialogs.rb +41 -26
- data/lib/vanken/ui/file_operations.rb +17 -12
- data/lib/vanken/ui/filter_operations.rb +43 -15
- data/lib/vanken/ui/main_view.rb +74 -29
- data/lib/vanken/ui/navigation_operations.rb +86 -0
- data/lib/vanken/ui/packet_source.rb +33 -1
- data/lib/vanken/ui/selection.rb +8 -2
- data/lib/vanken/ui/settings_operations.rb +233 -0
- data/lib/vanken/version.rb +1 -1
- data/sig/generated/vanken/app/capture_controller.rbs +16 -0
- data/sig/generated/vanken/app/document.rbs +23 -1
- data/sig/generated/vanken/app/document_analysis.rbs +21 -0
- data/sig/generated/vanken/app/document_jobs.rbs +6 -0
- data/sig/generated/vanken/app/expert_info.rbs +30 -0
- data/sig/generated/vanken/app/io_graph.rbs +45 -0
- data/sig/generated/vanken/app/navigation.rbs +31 -0
- data/sig/generated/vanken/app/search_job.rbs +21 -0
- data/sig/generated/vanken/app/stats_job.rbs +18 -0
- data/sig/generated/vanken/capture/helper_options.rbs +6 -0
- data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
- data/sig/generated/vanken/config/columns.rbs +37 -0
- data/sig/generated/vanken/config/messages.rbs +14 -0
- data/sig/generated/vanken/config/preferences.rbs +1 -1
- data/sig/generated/vanken/config/profiles.rbs +29 -0
- data/sig/generated/vanken/config/sessions.rbs +17 -0
- data/sig/generated/vanken/config/yaml_file.rbs +11 -0
- data/sig/generated/vanken/core/coloring.rbs +52 -0
- data/sig/generated/vanken/core/stores.rbs +4 -0
- data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
- data/sig/generated/vanken/gateway/dissector.rbs +15 -2
- data/sig/generated/vanken/gateway/exporter.rbs +27 -0
- data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
- data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
- data/sig/generated/vanken/gateway/resolver.rbs +52 -0
- data/sig/generated/vanken/gateway/statistics.rbs +104 -0
- data/sig/generated/vanken/gateway/stream.rbs +75 -0
- data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
- data/sig/generated/vanken/ui/application.rbs +23 -1
- data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
- data/sig/generated/vanken/ui/column_operations.rbs +29 -0
- data/sig/generated/vanken/ui/dialogs.rbs +5 -3
- data/sig/generated/vanken/ui/file_operations.rbs +2 -0
- data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
- data/sig/generated/vanken/ui/main_view.rbs +14 -2
- data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
- data/sig/generated/vanken/ui/packet_source.rbs +4 -0
- data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
- metadata +51 -3
data/lib/vanken/ui/main_view.rb
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../config/columns"
|
|
4
|
+
|
|
3
5
|
module Vanken
|
|
4
6
|
module UI
|
|
5
7
|
class MainView < Zaniah::UI::Component
|
|
6
|
-
COLUMNS =
|
|
7
|
-
|
|
8
|
+
COLUMNS = Config::Columns::BUILTINS
|
|
9
|
+
|
|
10
|
+
class ExpertBadge < Zaniah::UI::Badge
|
|
11
|
+
def initialize(label, variant:, &activate)
|
|
12
|
+
super(label, variant: variant)
|
|
13
|
+
@label, @activate = label, activate
|
|
14
|
+
end
|
|
15
|
+
def build(cx)
|
|
16
|
+
super.cursor(:pointer).on_click { @activate.call }
|
|
17
|
+
.focusable(context: {in_button: true}) { |action| action == :activate && (@activate.call; true) }
|
|
18
|
+
end
|
|
19
|
+
def accessibility_node(_cx) = node(:button, label: @label, actions: [:press])
|
|
20
|
+
def accessibility_action(_node, action) = action == :press && (@activate.call; true)
|
|
21
|
+
end
|
|
8
22
|
|
|
9
23
|
def initialize(ui)
|
|
10
24
|
super()
|
|
11
25
|
@ui = ui
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
26
|
+
@filter_container = Zaniah::Div.new.flex_1.focusable(context: {in_display_filter: true}).child(ui.filter_field)
|
|
27
|
+
@menu_bar = Zaniah::UI::MenuBar.from(ui.app.menu_bar)
|
|
28
|
+
settings = ui.respond_to?(:column_settings) ? ui.column_settings : Config::Columns.load(directory: ui.preferences.directory, legacy: ui.preferences.get("layout.columns"))
|
|
29
|
+
columns = settings.table_columns.map do |column|
|
|
30
|
+
default = COLUMNS.find { |key, *_| key == column[:key] }
|
|
31
|
+
default && column[:label] == default[1] ? column.merge(label: text(column[:label])) : column
|
|
19
32
|
end
|
|
20
|
-
|
|
33
|
+
ui.document.custom_columns = settings.custom if ui.document
|
|
34
|
+
table = Zaniah::UI::VirtualTable.new(ui.packet_source, columns: columns, follow_tail: ui.autoscroll?, label: text("パケット一覧"))
|
|
21
35
|
.test_id("vk.packet_list").on_select { |index, *_| number = ui.packet_source.row_id(index); ui.select_packet(number) if number.is_a?(Integer) }
|
|
22
36
|
.on_sort { |key, direction, _| ui.document&.sort(key, direction) }
|
|
23
|
-
.on_columns_change
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
.on_columns_change do |items, _|
|
|
38
|
+
if ui.respond_to?(:persist_columns_layout)
|
|
39
|
+
ui.persist_columns_layout(items)
|
|
40
|
+
else
|
|
41
|
+
settings.update_layout(items)
|
|
42
|
+
settings.save(ui.preferences.directory)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
tree = Zaniah::UI::TreeView.new([], label: text("パケット詳細")).test_id("vk.packet_details").on_select { |node, *_| ui.select_detail(node) }
|
|
46
|
+
.on_context_menu { |node, _| ui.respond_to?(:field_menu) ? ui.field_menu(node) : ui.selection_menu(node.filter) }
|
|
26
47
|
table.on_row_context_menu do |index, _|
|
|
27
48
|
number = ui.packet_source.row_id(index)
|
|
28
49
|
table.select(index) if number.is_a?(Integer)
|
|
29
|
-
ui.selection_menu(number.is_a?(Integer) ? "frame.number == #{number}" : nil)
|
|
50
|
+
ui.selection_menu(number.is_a?(Integer) ? "frame.number == #{number}" : nil, number: number.is_a?(Integer) ? number : nil)
|
|
30
51
|
end
|
|
31
|
-
hex = Zaniah::UI::HexView.new("".b).test_id("vk.packet_bytes").on_select { |range, _| ui.select_bytes(range) }
|
|
52
|
+
hex = Zaniah::UI::HexView.new("".b).accessibility_label(text("パケットバイト")).test_id("vk.packet_bytes").on_select { |range, _| ui.select_bytes(range) }
|
|
32
53
|
.on_copy { |range, _| ui.copy_text(ui.selected_bytes.byteslice(range).unpack1("H*").scan(/../).join(" ")) }
|
|
33
54
|
ui.install_panes(table, tree, hex)
|
|
34
55
|
ratios = ui.preferences.get("layout.ratios")
|
|
@@ -39,6 +60,7 @@ module Vanken
|
|
|
39
60
|
end
|
|
40
61
|
|
|
41
62
|
def build(cx)
|
|
63
|
+
@previous_buttons, @buttons = @buttons || {}, {}
|
|
42
64
|
ui = @ui
|
|
43
65
|
doc = ui.document
|
|
44
66
|
doc.frame_latency = cx.window.frame_stats.fetch(:frame_ms, 0) / 1000 if doc
|
|
@@ -53,7 +75,7 @@ module Vanken
|
|
|
53
75
|
@heights = heights
|
|
54
76
|
end
|
|
55
77
|
root = Zaniah::Div.new.w_full.h_full.flex_col.bg(cx.theme.colors.background)
|
|
56
|
-
root.child(
|
|
78
|
+
root.child(@menu_bar) unless RUBY_PLATFORM.include?("darwin") && ui.native?
|
|
57
79
|
toolbar_items = [
|
|
58
80
|
button("開く", "vk.open") { ui.open_dialog },
|
|
59
81
|
button("保存", "vk.save", disabled: !doc) { ui.save_dialog },
|
|
@@ -63,13 +85,22 @@ module Vanken
|
|
|
63
85
|
if doc&.loading? && doc.source == :file
|
|
64
86
|
toolbar_items << button("読み込みを中止", "vk.file.cancel") { doc.cancel }
|
|
65
87
|
end
|
|
66
|
-
root.child(Zaniah::UI::Toolbar.new(*toolbar_items))
|
|
88
|
+
root.child(Zaniah::UI::Toolbar.new(*toolbar_items).accessibility_label(text("キャプチャ操作")))
|
|
67
89
|
root.child(Zaniah::Div.new.flex_row.items_center.gap(6).p([4, 8])
|
|
68
|
-
.child(
|
|
90
|
+
.child(@filter_container).child(button("適用", "vk.filter.apply", disabled: !doc) { ui.apply_filter })
|
|
69
91
|
.child(button("クリア", "vk.filter.clear") { ui.clear_filter })
|
|
70
92
|
.child(button("履歴", "vk.filter.history") { ui.filter_history }))
|
|
71
93
|
root.child(doc ? @split.flex_1 : welcome(cx).flex_1)
|
|
72
|
-
|
|
94
|
+
status = [Zaniah::UI::Label.new(ui.status_text, size: :xs).flex_1]
|
|
95
|
+
summary = doc.respond_to?(:expert_summary) ? doc.expert_summary : nil
|
|
96
|
+
if summary && summary[:count].positive?
|
|
97
|
+
severity = %w[note note warning error].fetch(summary[:severity])
|
|
98
|
+
caption = {"note" => "注記", "warning" => "警告", "error" => "エラー"}.fetch(severity)
|
|
99
|
+
badge = ExpertBadge.new("#{text(caption)}: #{summary[:count]}", variant: {"note" => :neutral, "warning" => :warning, "error" => :danger}.fetch(severity)) { ui.expert_info }
|
|
100
|
+
.test_id("vk.expert.badge")
|
|
101
|
+
status << badge
|
|
102
|
+
end
|
|
103
|
+
root.child(Zaniah::UI::StatusBar.new(*status).test_id("vk.status"))
|
|
73
104
|
root.child(ui.dialog) if ui.dialog
|
|
74
105
|
root.on_drop(types: ["text/uri-list"]) do |event, _|
|
|
75
106
|
event.content.formats.fetch("text/uri-list", "").each_line do |line|
|
|
@@ -90,23 +121,37 @@ module Vanken
|
|
|
90
121
|
|
|
91
122
|
private
|
|
92
123
|
|
|
93
|
-
def
|
|
94
|
-
def
|
|
95
|
-
def
|
|
124
|
+
def text(value) = @ui.respond_to?(:t) ? @ui.t(value) : value
|
|
125
|
+
def pane(title, body) = Zaniah::Div.new.w_full.h_full.flex_col.child(Zaniah::UI::Label.new(text(title), size: :xs, tone: :muted).p([4, 8])).child(body)
|
|
126
|
+
def button(label, id, disabled: false, size: :sm, variant: :secondary, &block)
|
|
127
|
+
caption = text(label)
|
|
128
|
+
key = [id, caption, size, variant]
|
|
129
|
+
@buttons[key] = (@previous_buttons[key] || Zaniah::UI::Button.new(caption, size: size, variant: variant).test_id(id)).disabled(disabled).on_click(&block)
|
|
130
|
+
end
|
|
131
|
+
def welcome(cx)
|
|
96
132
|
content = Zaniah::Div.new.flex_col.p(24).gap(12).test_id("vk.welcome")
|
|
97
133
|
.child(Zaniah::UI::Label.new("Vanken", size: :xl))
|
|
98
|
-
.child(Zaniah::UI::Label.new("パケットキャプチャを開くか、インタフェースを選択してキャプチャを開始します。", tone: :muted, wrap: :word))
|
|
99
|
-
.child(
|
|
100
|
-
.child(
|
|
134
|
+
.child(Zaniah::UI::Label.new(text("パケットキャプチャを開くか、インタフェースを選択してキャプチャを開始します。"), tone: :muted, wrap: :word))
|
|
135
|
+
.child(button("キャプチャファイルを開く", "vk.welcome.open", size: :md, variant: :primary) { @ui.open_dialog })
|
|
136
|
+
.child(button("インタフェースを選択", "vk.welcome.capture", size: :md, variant: :primary) { @ui.capture_options })
|
|
101
137
|
@ui.preferences.recent_files.first(10).each do |path|
|
|
102
|
-
content.child(
|
|
138
|
+
content.child(button(path, "vk.recent.#{path}", variant: :ghost) { @ui.open_file(path) })
|
|
103
139
|
end
|
|
104
140
|
(@ui.interface_infos || []).each do |info|
|
|
105
141
|
name = info.fetch(:name)
|
|
106
|
-
|
|
107
|
-
|
|
142
|
+
traffic = @ui.respond_to?(:interface_traffic) ? (@ui.interface_traffic || {}) : {}
|
|
143
|
+
values = (traffic[:history] || {}).fetch(name, [])
|
|
144
|
+
values = [0] if values.empty?
|
|
145
|
+
rate = (traffic[:rates] || {}).fetch(name, 0)
|
|
146
|
+
row = Zaniah::Div.new.flex_row.items_center.gap(8)
|
|
147
|
+
.child(button("#{name} #{info[:description]}", "vk.interface.#{name}", variant: :ghost) { @ui.capture_options(interface: name) }.flex_1)
|
|
148
|
+
.child(Zaniah::UI::Sparkline.new(values, width: 100, height: 24, label: "#{name} #{text('トラフィック')}").test_id("vk.interface.traffic.#{name}"))
|
|
149
|
+
.child(Zaniah::UI::Label.new("#{rate} B/s", size: :xs, tone: :muted))
|
|
150
|
+
content.child(row)
|
|
108
151
|
end
|
|
109
|
-
|
|
152
|
+
@welcome_scroll ||= Zaniah::ScrollView.new
|
|
153
|
+
@welcome_scroll.children.clear
|
|
154
|
+
@welcome_scroll.h([cx.window.content_size.height - 132, 120].max).child(content)
|
|
110
155
|
end
|
|
111
156
|
end
|
|
112
157
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vanken
|
|
4
|
+
module UI
|
|
5
|
+
module NavigationOperations
|
|
6
|
+
def mark_packet = update_packet_state(:toggle_mark)
|
|
7
|
+
def ignore_packet = update_packet_state(:toggle_ignore)
|
|
8
|
+
def time_reference = update_packet_state(:toggle_time_reference)
|
|
9
|
+
def mark_all_displayed = update_packet_state(:mark_all_displayed, selected: false)
|
|
10
|
+
def unmark_all = update_packet_state(:unmark_all, selected: false)
|
|
11
|
+
def prev_in_conversation = navigate_conversation(-1)
|
|
12
|
+
def next_in_conversation = navigate_conversation(1)
|
|
13
|
+
|
|
14
|
+
def find_packet
|
|
15
|
+
mode = @search_mode || :filter
|
|
16
|
+
target = @search_target || :bytes
|
|
17
|
+
field = Zaniah::UI::TextField.new(@search_query || "", label: t("検索内容")).test_id("vk.search.query")
|
|
18
|
+
modes = Zaniah::UI::SegmentedControl.new([[t("表示フィルタ"), :filter], [t("16 進"), :hex], [t("文字列"), :string], [t("正規表現"), :regex]], value: mode, label: t("検索形式"))
|
|
19
|
+
.on_change { |value, *_| mode = value }.test_id("vk.search.mode")
|
|
20
|
+
targets = Zaniah::UI::Select.new([[t("パケット一覧"), :list], [t("パケット詳細"), :details], [t("パケットバイト"), :bytes]], label: t("検索対象"), value: target)
|
|
21
|
+
.on_change { |value, *_| target = value }.test_id("vk.search.target")
|
|
22
|
+
content = Zaniah::Div.new.flex_col.gap(12).child(modes).child(targets).child(field)
|
|
23
|
+
.child(Zaniah::Div.new.flex_row.gap(8)
|
|
24
|
+
.child(Zaniah::UI::Button.new(t("次を検索")).test_id("vk.search.next").on_click { search_packets(field.value, mode: mode, target: target) })
|
|
25
|
+
.child(Zaniah::UI::Button.new(t("前を検索"), variant: :secondary).on_click { search_packets(field.value, mode: mode, target: target, direction: -1) })
|
|
26
|
+
.child(Zaniah::UI::Button.new(t("キャンセル"), variant: :secondary).on_click { cancel_packet_search; dismiss_dialog }))
|
|
27
|
+
show_dialog(:search, t("パケット検索"), content, reopen: -> { @search_query, @search_mode, @search_target = field.value, mode, target; find_packet })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def search_result_dialog
|
|
31
|
+
show_dialog(:search_result, t("パケット検索"), Zaniah::UI::Label.new(t("一致するパケットはありません。"), wrap: :word), reopen: -> { search_result_dialog })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_next = repeat_packet_search(1)
|
|
35
|
+
def find_prev = repeat_packet_search(-1)
|
|
36
|
+
def cancel_packet_search
|
|
37
|
+
@search_generation = (@search_generation || 0) + 1
|
|
38
|
+
document&.cancel_search
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def search_packets(query, mode: :filter, target: :bytes, direction: 1)
|
|
42
|
+
doc = document
|
|
43
|
+
return unless doc
|
|
44
|
+
cancel_packet_search
|
|
45
|
+
generation = @search_generation
|
|
46
|
+
@search_query, @search_mode, @search_target = query, mode, target
|
|
47
|
+
doc.search(query, mode: mode, target: target, from: selected_number, direction: direction) do |number, error|
|
|
48
|
+
@app.executor.post do
|
|
49
|
+
next unless !@closing && generation == @search_generation && document.equal?(doc)
|
|
50
|
+
if error
|
|
51
|
+
show_error(error)
|
|
52
|
+
elsif number
|
|
53
|
+
select_found_packet(number)
|
|
54
|
+
else
|
|
55
|
+
search_result_dialog
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
dismiss_dialog if @dialog_kind == :search
|
|
60
|
+
rescue StandardError => error
|
|
61
|
+
show_error(error)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
def repeat_packet_search(direction)
|
|
66
|
+
@search_query && !@search_query.empty? ? search_packets(@search_query, mode: @search_mode, target: @search_target, direction: direction) : find_packet
|
|
67
|
+
end
|
|
68
|
+
def select_found_packet(number)
|
|
69
|
+
index = document&.displayed_index(number)
|
|
70
|
+
return unless index
|
|
71
|
+
@table.select(index)
|
|
72
|
+
@table.scroll_to(index, align: :center)
|
|
73
|
+
end
|
|
74
|
+
def navigate_conversation(direction)
|
|
75
|
+
number = document&.conversation_neighbor(selected_number, direction)
|
|
76
|
+
select_found_packet(number) if number
|
|
77
|
+
end
|
|
78
|
+
def update_packet_state(operation, selected: true, number: selected_number)
|
|
79
|
+
return unless document && (!selected || number)
|
|
80
|
+
selected ? document.public_send(operation, number) : document.public_send(operation)
|
|
81
|
+
@packet_source.reset
|
|
82
|
+
@window.request_frame
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -40,6 +40,14 @@ module Vanken
|
|
|
40
40
|
|
|
41
41
|
def count = @count
|
|
42
42
|
def packet_count = @packet_count
|
|
43
|
+
def invalidate_rows(numbers)
|
|
44
|
+
@generation += 1
|
|
45
|
+
@pending, @requests = {}, {}
|
|
46
|
+
@batch_scheduled = false
|
|
47
|
+
@refresh_pending = @refresh_again = nil
|
|
48
|
+
numbers.each { |number| @rows.delete(number) }
|
|
49
|
+
@ui.window.request_frame
|
|
50
|
+
end
|
|
43
51
|
def row_id(index)
|
|
44
52
|
return [:pending, index] unless index.is_a?(Integer) && index.between?(0, @count - 1)
|
|
45
53
|
@numbers[index] ||= @document.number_at(index)
|
|
@@ -51,7 +59,21 @@ module Vanken
|
|
|
51
59
|
text = value(index, key)
|
|
52
60
|
return nil if text.nil?
|
|
53
61
|
theme = @ui.app.global(:theme)
|
|
54
|
-
|
|
62
|
+
colors = row_style(index)
|
|
63
|
+
Zaniah::Text.new(text.to_s, font: @ui.monospace_font, size: theme.typography.size_sm, color: colors.fetch(:foreground, theme.colors.text))
|
|
64
|
+
end
|
|
65
|
+
def row_style(index)
|
|
66
|
+
number = row_id(index)
|
|
67
|
+
return {} unless number.is_a?(Integer)
|
|
68
|
+
if @ui.table.respond_to?(:selection) && @ui.table.selection.include?(number)
|
|
69
|
+
theme = @ui.app.global(:theme)
|
|
70
|
+
return {foreground: theme.colors.text, strikethrough: @document.ignored.include?(number)}
|
|
71
|
+
end
|
|
72
|
+
if @document.ignored.include?(number)
|
|
73
|
+
return {foreground: @ui.app.global(:theme).colors.text_muted, strikethrough: true}
|
|
74
|
+
end
|
|
75
|
+
return {foreground: Zaniah::Color.parse("#ffffff"), background: Zaniah::Color.parse("#000000")} if @document.marked.include?(number)
|
|
76
|
+
@rows[number]&.fetch(:style, {}) || {}
|
|
55
77
|
end
|
|
56
78
|
def value(index, key)
|
|
57
79
|
number = row_id(index)
|
|
@@ -139,7 +161,17 @@ module Vanken
|
|
|
139
161
|
def row_values(document, number, index, format, precision, previous = :current)
|
|
140
162
|
row = document.row(number)
|
|
141
163
|
values = row.slice(:source, :destination, :protocol, :length, :info).transform_values(&:to_s)
|
|
164
|
+
if @ui.respond_to?(:resolve_address)
|
|
165
|
+
%i[source destination].each do |key|
|
|
166
|
+
values[key] = @ui.resolve_address(values[key], document: document, number: number)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
values.merge!(row.select { |key, _| key.to_s.start_with?("field:") })
|
|
142
170
|
values[:time] = format_time(document, row, number, index, format, precision, previous)
|
|
171
|
+
if @ui.respond_to?(:coloring_enabled?) && @ui.coloring_enabled?
|
|
172
|
+
colors = @ui.coloring_rules.match(document.view(number), theme: @ui.app.global(:theme).name) || {}
|
|
173
|
+
values[:style] = colors.transform_values { |color| Zaniah::Color.parse(color) }
|
|
174
|
+
end
|
|
143
175
|
values
|
|
144
176
|
rescue StandardError
|
|
145
177
|
nil
|
data/lib/vanken/ui/selection.rb
CHANGED
|
@@ -8,6 +8,10 @@ module Vanken
|
|
|
8
8
|
return unless doc && number.is_a?(Integer) && number.between?(1, doc.count)
|
|
9
9
|
@selection_generation += 1
|
|
10
10
|
generation = @selection_generation
|
|
11
|
+
expanded = @pending_tree_expansion ||= @tree.expanded.dup
|
|
12
|
+
update { |state| state.merge!(number: number, node: nil, details: [], bytes: "".b) }
|
|
13
|
+
@tree.replace([])
|
|
14
|
+
@hex.bytes = "".b
|
|
11
15
|
@app.executor.background do
|
|
12
16
|
nodes = doc.details(number)
|
|
13
17
|
bytes = doc.store.read(number).bytes
|
|
@@ -15,6 +19,8 @@ module Vanken
|
|
|
15
19
|
next unless generation == @selection_generation && document.equal?(doc)
|
|
16
20
|
update { |state| state.merge!(number: number, node: nil, details: nodes, bytes: bytes) }
|
|
17
21
|
@tree.replace(nodes)
|
|
22
|
+
expanded.each { |id| @tree.expand(id) }
|
|
23
|
+
@pending_tree_expansion = nil
|
|
18
24
|
@hex.bytes = bytes
|
|
19
25
|
end
|
|
20
26
|
rescue StandardError => error
|
|
@@ -65,10 +71,10 @@ module Vanken
|
|
|
65
71
|
@table.scroll_to(document.displayed_count - 1)
|
|
66
72
|
end
|
|
67
73
|
def go_to_packet
|
|
68
|
-
path_dialog("パケットへ移動") do |value|
|
|
74
|
+
path_dialog(t("パケットへ移動")) do |value|
|
|
69
75
|
number = Integer(value, 10)
|
|
70
76
|
index = document&.display_numbers&.index(number)
|
|
71
|
-
raise Vanken::Error, "指定したパケットは表示されていません" unless index
|
|
77
|
+
raise Vanken::Error, t("指定したパケットは表示されていません") unless index
|
|
72
78
|
@table.select(index)
|
|
73
79
|
@table.scroll_to(index, align: :center)
|
|
74
80
|
dismiss_dialog
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config/profiles"
|
|
4
|
+
require_relative "../config/analysis_settings"
|
|
5
|
+
|
|
6
|
+
module Vanken
|
|
7
|
+
module UI
|
|
8
|
+
module SettingsOperations
|
|
9
|
+
SETTING_LABELS = {
|
|
10
|
+
"appearance.theme" => "テーマ", "appearance.language" => "言語", "appearance.font_size" => "文字サイズ",
|
|
11
|
+
"packet_list.time_format" => "時刻表示", "packet_list.time_precision" => "時刻の精度", "packet_list.autoscroll" => "自動スクロール",
|
|
12
|
+
"packet_list.row_cache_rows" => "行キャッシュ容量", "packet_list.coloring" => "色付け",
|
|
13
|
+
"capture.snaplen" => "Snaplen", "capture.promiscuous" => "プロミスキャスモード", "capture.buffer_size" => "取得バッファ (bytes)",
|
|
14
|
+
"capture.backend" => "キャプチャ方式", "capture.launcher" => "ヘルパー起動方式", "capture.direction" => "取得方向",
|
|
15
|
+
"capture.stop_count" => "自動停止: パケット数 (0: 無効)", "capture.stop_duration" => "自動停止: 秒 (0: 無効)", "capture.stop_bytes" => "自動停止: bytes (0: 無効)",
|
|
16
|
+
"capture.ring_path" => "リング保存先 (空欄: 無効)", "capture.ring_max_bytes" => "リング: ファイルごとのbytes", "capture.ring_interval" => "リング: 秒", "capture.ring_file_count" => "リング: 保持ファイル数",
|
|
17
|
+
"analysis.verify_checksums" => "チェックサム検証", "analysis.max_state_mib" => "解析メモリ上限(MiB)", "analysis.max_flows" => "フロー数上限", "analysis.workers" => "ワーカー数",
|
|
18
|
+
"name_resolution.enabled" => "名前解決", "name_resolution.timeout" => "解決タイムアウト(秒)", "name_resolution.cache_size" => "名前キャッシュ容量"
|
|
19
|
+
}.freeze
|
|
20
|
+
def analysis_settings = Config::AnalysisSettings.new(directory: @preferences.directory)
|
|
21
|
+
|
|
22
|
+
def preferences_dialog
|
|
23
|
+
groups = %w[appearance packet_list capture analysis name_resolution].map do |group|
|
|
24
|
+
defaults = Config::Preferences::DEFAULTS.fetch(group)
|
|
25
|
+
schema = defaults.map do |key, value|
|
|
26
|
+
path = "#{group}.#{key}"
|
|
27
|
+
options = {"appearance.theme" => %w[system dark light high_contrast], "appearance.language" => %w[ja en],
|
|
28
|
+
"packet_list.time_format" => %w[relative absolute delta delta_displayed epoch], "packet_list.time_precision" => %w[milli micro nano],
|
|
29
|
+
"capture.backend" => %w[auto ring socket bpf], "capture.launcher" => %w[auto direct sudo pkexec], "capture.direction" => %w[in out inout]}[path]
|
|
30
|
+
captions = {"system" => "システム設定", "dark" => "ダーク", "light" => "ライト", "high_contrast" => "ハイコントラスト",
|
|
31
|
+
"ja" => "日本語", "en" => "English", "relative" => "相対時刻", "absolute" => "絶対時刻", "delta" => "前パケットとの差", "delta_displayed" => "前表示パケットとの差", "epoch" => "エポック時刻",
|
|
32
|
+
"milli" => "ミリ秒", "micro" => "マイクロ秒", "nano" => "ナノ秒", "in" => "受信", "out" => "送信", "inout" => "双方向"}
|
|
33
|
+
options = options.map { |item| [t(captions.fetch(item, item)), item] } if options
|
|
34
|
+
type = options ? :select : [true, false].include?(value) ? :boolean : value.is_a?(Numeric) ? :number : :text
|
|
35
|
+
{key: path, label: setting_label(path), type: type, options: options || []}
|
|
36
|
+
end
|
|
37
|
+
values = defaults.keys.to_h { |key| [:"#{group}.#{key}", @preferences.get("#{group}.#{key}")] }
|
|
38
|
+
grid = Zaniah::UI::PropertyGrid.new(schema, values, height: 240, label: setting_label(group)).test_id("vk.preferences.#{group}")
|
|
39
|
+
.on_change { |key, value, *_| apply_preference(key.to_s, value) }
|
|
40
|
+
[setting_label(group), grid]
|
|
41
|
+
end
|
|
42
|
+
content = Zaniah::Div.new.flex_col.gap(8)
|
|
43
|
+
.child(Zaniah::UI::Button.new(t("プロファイルの管理")).on_click { profiles_dialog })
|
|
44
|
+
.child(Zaniah::UI::Tabs.new(groups))
|
|
45
|
+
show_dialog(:preferences, t("設定"), content, reopen: -> { preferences_dialog })
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def apply_preference(path, value)
|
|
49
|
+
old = @preferences.get(path)
|
|
50
|
+
value = Integer(value) if old.is_a?(Integer)
|
|
51
|
+
value = Float(value) if old.is_a?(Float)
|
|
52
|
+
@preferences.set(path, value)
|
|
53
|
+
if path.start_with?("appearance.")
|
|
54
|
+
change_theme(@preferences.get("appearance.theme"), persist: false)
|
|
55
|
+
rebuild_view if path == "appearance.language"
|
|
56
|
+
elsif path.start_with?("packet_list.", "name_resolution.")
|
|
57
|
+
reset_resolver if path.start_with?("name_resolution.")
|
|
58
|
+
if path == "packet_list.autoscroll"
|
|
59
|
+
@autoscroll = value
|
|
60
|
+
@table.follow_tail = value if @table
|
|
61
|
+
end
|
|
62
|
+
@packet_source.reset
|
|
63
|
+
elsif path.start_with?("analysis.")
|
|
64
|
+
@pending_analysis_settings = true
|
|
65
|
+
apply_analysis_settings
|
|
66
|
+
end
|
|
67
|
+
@window.request_frame
|
|
68
|
+
rescue StandardError => error
|
|
69
|
+
show_error(error)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def profiles_dialog
|
|
73
|
+
content = Zaniah::Div.new.flex_col.gap(8)
|
|
74
|
+
@profiles.names.each do |name|
|
|
75
|
+
row = Zaniah::Div.new.flex_row.gap(6)
|
|
76
|
+
.child(Zaniah::UI::Button.new(name, variant: name == @profiles.active ? :secondary : :ghost).on_click { switch_profile(name) }.flex_1)
|
|
77
|
+
.child(Zaniah::UI::Button.new(t("複製")).on_click { path_dialog(t("新しいプロファイル名")) { |value| @profiles.create(value, copy_from: name); profiles_dialog } })
|
|
78
|
+
row.child(Zaniah::UI::Button.new(t("削除")).disabled(name == @profiles.active || name == "default").on_click { @profiles.delete(name); profiles_dialog })
|
|
79
|
+
content.child(row)
|
|
80
|
+
end
|
|
81
|
+
content.child(Zaniah::UI::Button.new(t("新規作成")).on_click { path_dialog(t("新しいプロファイル名")) { |value| @profiles.create(value); profiles_dialog } })
|
|
82
|
+
show_dialog(:profiles, t("プロファイル"), Zaniah::ScrollView.new.h(240).child(content), reopen: -> { profiles_dialog })
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def switch_profile(name)
|
|
86
|
+
raise Vanken::Error, "stop capture before switching profiles" if @capture.running?
|
|
87
|
+
raise Vanken::Error, "finish loading before switching profiles" if document&.loading? && !@reanalysis_in_flight
|
|
88
|
+
close_analysis if respond_to?(:close_analysis)
|
|
89
|
+
@preferences = @profiles.switch(name)
|
|
90
|
+
@capture.preferences = @preferences
|
|
91
|
+
reset_resolver
|
|
92
|
+
reload_columns
|
|
93
|
+
reload_coloring_rules
|
|
94
|
+
change_theme(@preferences.get("appearance.theme"), persist: false)
|
|
95
|
+
rebuild_view
|
|
96
|
+
dismiss_dialog
|
|
97
|
+
reconfigure_analysis if document
|
|
98
|
+
rescue StandardError => error
|
|
99
|
+
show_error(error)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def decode_as
|
|
103
|
+
field = Zaniah::UI::TextField.new(analysis_settings.decode_as.join("; "), label: t("例: udp.port==8443,dns")).test_id("vk.decode_as.rules")
|
|
104
|
+
content = Zaniah::Div.new.flex_col.gap(12).child(field)
|
|
105
|
+
.child(Zaniah::UI::Button.new(t("適用")).on_click do
|
|
106
|
+
rules = field.value.split(";").map(&:strip).reject(&:empty?)
|
|
107
|
+
Gateway::Dissector.new(decode_as: rules, plugins: analysis_settings.plugins)
|
|
108
|
+
analysis_settings.decode_as = rules
|
|
109
|
+
dismiss_dialog
|
|
110
|
+
reconfigure_analysis
|
|
111
|
+
rescue StandardError => error
|
|
112
|
+
show_error(error)
|
|
113
|
+
end)
|
|
114
|
+
show_dialog(:decode_as, t("別プロトコルとして解析"), content, reopen: -> { decode_as })
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def plugins_dialog
|
|
118
|
+
content = Zaniah::Div.new.flex_col.gap(8)
|
|
119
|
+
analysis_settings.plugin_entries.each do |entry|
|
|
120
|
+
path = entry.fetch("path")
|
|
121
|
+
content.child(Zaniah::Div.new.flex_row.gap(8).child(Zaniah::UI::Label.new(path, wrap: :word).flex_1)
|
|
122
|
+
.child(Zaniah::UI::Button.new(t("削除")).on_click { analysis_settings.remove_plugin(path); plugins_dialog }))
|
|
123
|
+
end
|
|
124
|
+
content.child(Zaniah::UI::Button.new(t("追加")).on_click { path_dialog(t("Ruby ディセクタのパス")) { |path| analysis_settings.register_plugin(path); reload_plugins } })
|
|
125
|
+
content.child(Zaniah::UI::Button.new(t("再読み込み")).on_click { reload_plugins })
|
|
126
|
+
show_dialog(:plugins, t("ディセクタプラグイン"), Zaniah::ScrollView.new.h(240).child(content), reopen: -> { plugins_dialog })
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def reload_plugins
|
|
130
|
+
paths = analysis_settings.untrusted_plugins
|
|
131
|
+
return reconfigure_analysis if paths.empty?
|
|
132
|
+
content = Zaniah::Div.new.flex_col.gap(12).child(Zaniah::UI::Label.new(t("Ruby プラグインはユーザー権限で任意のコードを実行します。信頼するファイルだけを読み込んでください。\n%{paths}", paths: paths.join("\n")), wrap: :word))
|
|
133
|
+
.child(Zaniah::UI::Button.new(t("信頼して読み込む")).test_id("vk.plugins.trust").on_click { analysis_settings.trust_plugins(paths); dismiss_dialog; reconfigure_analysis })
|
|
134
|
+
.child(Zaniah::UI::Button.new(t("キャンセル"), variant: :secondary).on_click { dismiss_dialog })
|
|
135
|
+
show_dialog(:plugin_trust, t("プラグインの確認"), content, reopen: -> { reload_plugins })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def reconfigure_analysis
|
|
139
|
+
doc = document
|
|
140
|
+
return unless doc
|
|
141
|
+
raise Vanken::Error, "finish loading or stop capture before changing analysis" if @capture.running? || (doc.loading? && !@reanalysis_in_flight)
|
|
142
|
+
@pending_reanalysis = {document: doc, options: {decode_as: analysis_settings.decode_as, plugins: analysis_settings.plugins, preferences: @preferences}, number: selected_number}
|
|
143
|
+
begin_reanalysis
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def begin_reanalysis
|
|
147
|
+
request = @pending_reanalysis
|
|
148
|
+
return unless request && !document&.loading? && (!@reanalysis_task || @reanalysis_task.done?)
|
|
149
|
+
@pending_reanalysis = nil
|
|
150
|
+
doc = request[:document]
|
|
151
|
+
return unless document.equal?(doc) && !@closing
|
|
152
|
+
close_analysis if respond_to?(:close_analysis)
|
|
153
|
+
@selection_generation += 1
|
|
154
|
+
selection_generation = @selection_generation
|
|
155
|
+
@reanalysis_selection = nil
|
|
156
|
+
@reanalysis_in_flight = true
|
|
157
|
+
@active_reanalysis = request
|
|
158
|
+
@packet_source.reset
|
|
159
|
+
@reanalysis_task = @app.executor.background do
|
|
160
|
+
doc.reanalyze(**request[:options])
|
|
161
|
+
doc.cancel_reanalysis unless @active_reanalysis.equal?(request)
|
|
162
|
+
doc.wait
|
|
163
|
+
raise doc.error if doc.error
|
|
164
|
+
@app.executor.post do
|
|
165
|
+
next if @closing || !document.equal?(doc)
|
|
166
|
+
@packet_source.reset
|
|
167
|
+
@reanalysis_selection = [doc, request[:number], selection_generation]
|
|
168
|
+
changed
|
|
169
|
+
end
|
|
170
|
+
rescue StandardError => error
|
|
171
|
+
@app.executor.post do
|
|
172
|
+
next if @closing || !document.equal?(doc)
|
|
173
|
+
@reanalysis_in_flight = false
|
|
174
|
+
show_error(error)
|
|
175
|
+
begin_reanalysis
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
@reanalysis_task.on_complete { @app.executor.post { changed if !@closing && document.equal?(doc) } }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def wait_reanalysis
|
|
182
|
+
task, @reanalysis_task = @reanalysis_task, nil
|
|
183
|
+
@active_reanalysis = nil
|
|
184
|
+
document&.cancel_reanalysis if @reanalysis_in_flight && task && !task.done?
|
|
185
|
+
task&.await
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def apply_analysis_settings
|
|
189
|
+
return unless @pending_analysis_settings
|
|
190
|
+
return if @capture.running? || document&.loading?
|
|
191
|
+
@pending_analysis_settings = false
|
|
192
|
+
document&.cancel_scan
|
|
193
|
+
@pool_mutex.synchronize do
|
|
194
|
+
@pool&.shutdown
|
|
195
|
+
@pool = nil
|
|
196
|
+
end
|
|
197
|
+
reconfigure_analysis if document
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def command_palette
|
|
201
|
+
dismiss_dialog
|
|
202
|
+
@dialog_kind = :command_palette
|
|
203
|
+
@dialog_reopen = -> { command_palette }
|
|
204
|
+
@dialog = Zaniah::UI::CommandPalette.from(@app.actions, open: true, placeholder: t("コマンドを検索"), title: t("コマンドパレット"), close_label: t("閉じる")).test_id("vk.command_palette")
|
|
205
|
+
.on_close { dismiss_dialog }
|
|
206
|
+
@window.request_frame
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def rebuild_view
|
|
210
|
+
expanded = @tree&.expanded&.dup || []
|
|
211
|
+
text = @filter_field.value
|
|
212
|
+
@filter_field = Zaniah::UI::TextField.new(text, placeholder: t("表示フィルタ"), clearable: false).test_id("vk.filter.input")
|
|
213
|
+
@filter_field.completion(CompletionProvider.new(self)).on_change { |value, _| validate_filter(value) }
|
|
214
|
+
Actions.install(self)
|
|
215
|
+
@main_view = MainView.new(self).test_id("vk.main")
|
|
216
|
+
@tree.replace(detail_nodes)
|
|
217
|
+
expanded.each { |id| @tree.expand(id) }
|
|
218
|
+
@hex.bytes = selected_bytes
|
|
219
|
+
@table.selection.add(selected_number) if selected_number
|
|
220
|
+
@packet_source.reset
|
|
221
|
+
reopen_dialog
|
|
222
|
+
@window.request_frame
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def setting_label(path)
|
|
228
|
+
label = SETTING_LABELS[path] || {"appearance" => "表示", "packet_list" => "パケット一覧", "capture" => "キャプチャ", "analysis" => "解析", "name_resolution" => "名前解決"}.fetch(path, path)
|
|
229
|
+
t(label)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
data/lib/vanken/version.rb
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
module Vanken
|
|
4
4
|
module App
|
|
5
5
|
class CaptureController
|
|
6
|
+
class RingReader
|
|
7
|
+
def initialize: (untyped reader, untyped writer, on_error: untyped) -> untyped
|
|
8
|
+
|
|
9
|
+
def next_frame: (timeout: untyped) -> untyped
|
|
10
|
+
|
|
11
|
+
def eof?: () -> untyped
|
|
12
|
+
|
|
13
|
+
def stop: () -> untyped
|
|
14
|
+
|
|
15
|
+
def close: () -> untyped
|
|
16
|
+
end
|
|
17
|
+
|
|
6
18
|
class Error < Vanken::CaptureError
|
|
7
19
|
attr_reader code: untyped
|
|
8
20
|
|
|
@@ -29,6 +41,8 @@ module Vanken
|
|
|
29
41
|
|
|
30
42
|
def snapshot: () -> untyped
|
|
31
43
|
|
|
44
|
+
def preferences=: (untyped value) -> untyped
|
|
45
|
+
|
|
32
46
|
def start: (untyped options) -> untyped
|
|
33
47
|
|
|
34
48
|
def stop: () -> untyped
|
|
@@ -43,6 +57,8 @@ module Vanken
|
|
|
43
57
|
|
|
44
58
|
def run: () -> untyped
|
|
45
59
|
|
|
60
|
+
def open_ring: () -> untyped
|
|
61
|
+
|
|
46
62
|
def consume_control: (untyped io) -> untyped
|
|
47
63
|
|
|
48
64
|
# Called under @mutex; the helper stop runs separately so UI callbacks never wait for it.
|