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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +26 -11
  4. data/data/coloring_rules.yml +67 -0
  5. data/docs/performance.md +10 -0
  6. data/docs/releases.md +2 -4
  7. data/docs/usage.md +51 -0
  8. data/lib/vanken/app/capture_controller.rb +66 -1
  9. data/lib/vanken/app/document.rb +102 -39
  10. data/lib/vanken/app/document_analysis.rb +75 -0
  11. data/lib/vanken/app/document_jobs.rb +52 -9
  12. data/lib/vanken/app/expert_info.rb +26 -0
  13. data/lib/vanken/app/io_graph.rb +47 -0
  14. data/lib/vanken/app/navigation.rb +77 -0
  15. data/lib/vanken/app/search_job.rb +62 -0
  16. data/lib/vanken/app/stats_job.rb +45 -0
  17. data/lib/vanken/capture/analyzer.rb +3 -1
  18. data/lib/vanken/capture/analyzer_process.rb +9 -6
  19. data/lib/vanken/capture/analyzer_worker.rb +2 -2
  20. data/lib/vanken/capture/filter_worker.rb +8 -2
  21. data/lib/vanken/capture/helper_main.rb +10 -3
  22. data/lib/vanken/capture/helper_options.rb +6 -2
  23. data/lib/vanken/capture/launcher.rb +1 -0
  24. data/lib/vanken/config/analysis_settings.rb +58 -0
  25. data/lib/vanken/config/columns.rb +138 -0
  26. data/lib/vanken/config/messages.rb +105 -0
  27. data/lib/vanken/config/preferences.rb +34 -8
  28. data/lib/vanken/config/profiles.rb +74 -0
  29. data/lib/vanken/config/sessions.rb +69 -0
  30. data/lib/vanken/config/yaml_file.rb +36 -0
  31. data/lib/vanken/core/coloring.rb +86 -0
  32. data/lib/vanken/core/frame_store.rb +2 -2
  33. data/lib/vanken/core/stores.rb +12 -0
  34. data/lib/vanken/gateway/detail_builder.rb +1 -1
  35. data/lib/vanken/gateway/display_capture_filter.rb +102 -0
  36. data/lib/vanken/gateway/dissector.rb +31 -6
  37. data/lib/vanken/gateway/exporter.rb +125 -0
  38. data/lib/vanken/gateway/file_writer.rb +26 -2
  39. data/lib/vanken/gateway/interfaces.rb +59 -0
  40. data/lib/vanken/gateway/resolver.rb +131 -0
  41. data/lib/vanken/gateway/statistics.rb +83 -0
  42. data/lib/vanken/gateway/stream.rb +159 -0
  43. data/lib/vanken/ui/actions.rb +54 -28
  44. data/lib/vanken/ui/analysis_dialogs.rb +371 -0
  45. data/lib/vanken/ui/application.rb +183 -9
  46. data/lib/vanken/ui/coloring_operations.rb +104 -0
  47. data/lib/vanken/ui/column_operations.rb +115 -0
  48. data/lib/vanken/ui/dialogs.rb +41 -26
  49. data/lib/vanken/ui/file_operations.rb +17 -12
  50. data/lib/vanken/ui/filter_operations.rb +43 -15
  51. data/lib/vanken/ui/main_view.rb +74 -29
  52. data/lib/vanken/ui/navigation_operations.rb +86 -0
  53. data/lib/vanken/ui/packet_source.rb +33 -1
  54. data/lib/vanken/ui/selection.rb +8 -2
  55. data/lib/vanken/ui/settings_operations.rb +233 -0
  56. data/lib/vanken/version.rb +1 -1
  57. data/sig/generated/vanken/app/capture_controller.rbs +16 -0
  58. data/sig/generated/vanken/app/document.rbs +23 -1
  59. data/sig/generated/vanken/app/document_analysis.rbs +21 -0
  60. data/sig/generated/vanken/app/document_jobs.rbs +6 -0
  61. data/sig/generated/vanken/app/expert_info.rbs +30 -0
  62. data/sig/generated/vanken/app/io_graph.rbs +45 -0
  63. data/sig/generated/vanken/app/navigation.rbs +31 -0
  64. data/sig/generated/vanken/app/search_job.rbs +21 -0
  65. data/sig/generated/vanken/app/stats_job.rbs +18 -0
  66. data/sig/generated/vanken/capture/helper_options.rbs +6 -0
  67. data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
  68. data/sig/generated/vanken/config/columns.rbs +37 -0
  69. data/sig/generated/vanken/config/messages.rbs +14 -0
  70. data/sig/generated/vanken/config/preferences.rbs +1 -1
  71. data/sig/generated/vanken/config/profiles.rbs +29 -0
  72. data/sig/generated/vanken/config/sessions.rbs +17 -0
  73. data/sig/generated/vanken/config/yaml_file.rbs +11 -0
  74. data/sig/generated/vanken/core/coloring.rbs +52 -0
  75. data/sig/generated/vanken/core/stores.rbs +4 -0
  76. data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
  77. data/sig/generated/vanken/gateway/dissector.rbs +15 -2
  78. data/sig/generated/vanken/gateway/exporter.rbs +27 -0
  79. data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
  80. data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
  81. data/sig/generated/vanken/gateway/resolver.rbs +52 -0
  82. data/sig/generated/vanken/gateway/statistics.rbs +104 -0
  83. data/sig/generated/vanken/gateway/stream.rbs +75 -0
  84. data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
  85. data/sig/generated/vanken/ui/application.rbs +23 -1
  86. data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
  87. data/sig/generated/vanken/ui/column_operations.rbs +29 -0
  88. data/sig/generated/vanken/ui/dialogs.rbs +5 -3
  89. data/sig/generated/vanken/ui/file_operations.rbs +2 -0
  90. data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
  91. data/sig/generated/vanken/ui/main_view.rbs +14 -2
  92. data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
  93. data/sig/generated/vanken/ui/packet_source.rbs +4 -0
  94. data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
  95. metadata +51 -3
@@ -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 = [[:no, "No.", 64], [:time, "Time", 128], [:source, "Source", 164],
7
- [:destination, "Destination", 164], [:protocol, "Protocol", 90], [:length, "Length", 76], [:info, "Info", 560]].freeze
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
- columns = COLUMNS.map { |key, label, width| {key: key, label: label, width: width, align: %i[no time length].include?(key) ? :end : :start} }
13
- saved = ui.preferences.get("layout.columns")
14
- unless saved.empty?
15
- columns = columns.sort_by { |column| saved.index { |item| item["key"] == column[:key].to_s } || columns.size }.map do |column|
16
- value = saved.find { |item| item["key"] == column[:key].to_s }
17
- value ? column.merge(width: value["width"], visible: value["visible"]) : column
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
- table = Zaniah::UI::VirtualTable.new(ui.packet_source, columns: columns, follow_tail: ui.autoscroll?)
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 { |items, _| ui.preferences.set("layout.columns", items.map { |item| {"key" => item[:key].to_s, "width" => item[:width], "visible" => item[:visible]} }) }
24
- tree = Zaniah::UI::TreeView.new([]).test_id("vk.packet_details").on_select { |node, *_| ui.select_detail(node) }
25
- .on_context_menu { |node, _| ui.selection_menu(node.filter) }
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(Zaniah::UI::MenuBar.from(ui.app.menu_bar)) unless RUBY_PLATFORM.include?("darwin") && ui.native?
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(Zaniah::Div.new.flex_1.focusable(context: {in_display_filter: true}).child(ui.filter_field)).child(button("適用", "vk.filter.apply", disabled: !doc) { ui.apply_filter })
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
- root.child(Zaniah::UI::StatusBar.new(Zaniah::UI::Label.new(ui.status_text, size: :xs)).test_id("vk.status"))
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 pane(title, body) = Zaniah::Div.new.w_full.h_full.flex_col.child(Zaniah::UI::Label.new(title, size: :xs, tone: :muted).p([4, 8])).child(body)
94
- def button(label, id, disabled: false, &block) = Zaniah::UI::Button.new(label, size: :sm, variant: :secondary).disabled(disabled).test_id(id).on_click(&block)
95
- def welcome(_cx)
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(Zaniah::UI::Button.new("キャプチャファイルを開く").on_click { @ui.open_dialog })
100
- .child(Zaniah::UI::Button.new("インタフェースを選択").on_click { @ui.capture_options })
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(Zaniah::UI::Button.new(path, size: :sm, variant: :ghost).on_click { @ui.open_file(path) })
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
- content.child(Zaniah::UI::Button.new("#{name} #{info[:description]}", size: :sm, variant: :ghost)
107
- .on_click { @ui.capture_options(interface: name) })
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
- content
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
- Zaniah::Text.new(text.to_s, font: @ui.monospace_font, size: theme.typography.size_sm, color: theme.colors.text)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vanken
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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.