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
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "ipaddr"
5
+ require "resolv"
6
+ require "timeout"
7
+
8
+ module Vanken
9
+ module Gateway
10
+ # Reverse DNS never runs on the requesting thread. Callbacks run on the
11
+ # worker; UI callers must post their publication to the UI executor.
12
+ class Resolver
13
+ NEGATIVE_TTL = 30.0
14
+
15
+ # @rbs @timeout: Float
16
+ # @rbs @limit: Integer
17
+ # @rbs @resolver: untyped
18
+ # @rbs @queue: SizedQueue[String?]
19
+ # @rbs @cache: Hash[String, [String?, Float?]]
20
+ # @rbs @pending: Hash[String, Array[^(String) -> void]]
21
+ # @rbs @mutex: Mutex
22
+ # @rbs @closed: bool
23
+ # @rbs @worker: Thread
24
+
25
+ # @rbs (?timeout: Integer | Float, ?limit: Integer, ?resolver: untyped) -> void
26
+ def initialize(timeout: 1.0, limit: 256, resolver: nil)
27
+ unless timeout.is_a?(Numeric) && timeout.to_f.finite? && timeout.positive?
28
+ raise ArgumentError, "timeout must be finite and positive"
29
+ end
30
+ raise ArgumentError, "limit must be a positive integer" unless limit.is_a?(Integer) && limit.positive?
31
+ @timeout, @limit = timeout.to_f, limit
32
+ @resolver = resolver || Resolv::DNS.new
33
+ @resolver.timeouts = [@timeout] if @resolver.respond_to?(:timeouts=)
34
+ @queue = SizedQueue.new(@limit) #: SizedQueue[String?]
35
+ @cache = {} #: Hash[String, [String?, Float?]]
36
+ @pending = {} #: Hash[String, Array[^(String) -> void]]
37
+ @mutex = Mutex.new
38
+ @closed = false
39
+ @worker = Thread.new { work }
40
+ @worker.report_on_exception = false
41
+ end
42
+
43
+ # @rbs (String address) ?{ (String) -> void } -> String
44
+ def request(address, &callback)
45
+ key = numeric_address(address)
46
+ return address unless key
47
+ @mutex.synchronize do
48
+ return address if @closed
49
+ entry = @cache.delete(key)
50
+ if entry && (!entry[1] || entry[1] > now)
51
+ @cache[key] = entry
52
+ return entry[0] || address
53
+ end
54
+ if (waiting = @pending[key])
55
+ waiting << callback if callback
56
+ else
57
+ begin
58
+ @queue.push(key, true)
59
+ @pending[key] = callback ? [callback] : []
60
+ rescue ThreadError
61
+ # A future viewport request may retry after capacity is available.
62
+ end
63
+ end
64
+ address
65
+ end
66
+ end
67
+
68
+ # @rbs () -> void
69
+ def close
70
+ @mutex.synchronize do
71
+ return if @closed
72
+ @closed = true
73
+ @pending.clear
74
+ @queue.clear
75
+ @queue << nil
76
+ end
77
+ @worker.join(@timeout + 0.1) unless Thread.current == @worker
78
+ nil
79
+ end
80
+
81
+ private
82
+
83
+ # @rbs (String address) -> String?
84
+ def numeric_address(address)
85
+ return nil if address.include?("/")
86
+ IPAddr.new(address).to_s
87
+ rescue IPAddr::Error
88
+ nil
89
+ end
90
+
91
+ # @rbs () -> Float
92
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
93
+
94
+ # @rbs (String address) -> String?
95
+ def lookup(address)
96
+ name = Timeout.timeout(@timeout) { @resolver.getname(address) }
97
+ name.to_s.dup.freeze unless name.nil? || name.to_s.empty?
98
+ rescue StandardError
99
+ nil
100
+ end
101
+
102
+ # @rbs () -> void
103
+ def work
104
+ while (address = @queue.pop)
105
+ name = lookup(address)
106
+ callbacks = @mutex.synchronize do
107
+ return if @closed
108
+ @cache[address] = [name, name ? nil : now + NEGATIVE_TTL]
109
+ @cache.shift while @cache.size > @limit
110
+ @pending.delete(address) || []
111
+ end
112
+ next unless name
113
+ callbacks.each do |callback|
114
+ break if @mutex.synchronize { @closed }
115
+ begin
116
+ callback.call(name)
117
+ rescue StandardError
118
+ # One consumer must not prevent other consumers or DNS requests.
119
+ end
120
+ end
121
+ end
122
+ ensure
123
+ begin
124
+ @resolver.close if @resolver.respond_to?(:close)
125
+ rescue StandardError
126
+ nil
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative "dissector"
5
+
6
+ module Vanken
7
+ module Gateway
8
+ HierarchyRow = Data.define(
9
+ :path, #: Array[Symbol]
10
+ :packets, #: Integer
11
+ :bytes #: Integer
12
+ )
13
+ ConversationRow = Data.define(
14
+ :addr_a, #: String
15
+ :port_a, #: Integer
16
+ :addr_b, #: String
17
+ :port_b, #: Integer
18
+ :packets_ab, #: Integer
19
+ :packets_ba, #: Integer
20
+ :bytes_ab, #: Integer
21
+ :bytes_ba, #: Integer
22
+ :start_ns, #: Integer
23
+ :end_ns, #: Integer
24
+ :duration_ns #: Integer
25
+ )
26
+ EndpointRow = Data.define(
27
+ :address, #: String
28
+ :port, #: Integer
29
+ :packets_tx, #: Integer
30
+ :packets_rx, #: Integer
31
+ :bytes_tx, #: Integer
32
+ :bytes_rx #: Integer
33
+ )
34
+ StatisticsTable = Data.define(
35
+ :kind, #: Symbol
36
+ :type, #: Symbol?
37
+ :rows, #: Array[HierarchyRow | ConversationRow | EndpointRow]
38
+ :evicted #: Integer
39
+ )
40
+
41
+ # The upstream session and its private table schema stay inside the Gateway.
42
+ class Statistics
43
+ # @rbs (?specs: Array[String], ?registry: Redhound::Registry, **untyped options) -> void
44
+ def initialize(specs: ["phs"], registry: Redhound::Registry.default, **options)
45
+ @session = Redhound::Analysis::Session.new(stats: specs, registry: registry, **options)
46
+ end
47
+
48
+ # @rbs (PacketView packet) -> void
49
+ def update(packet) = @session.update(packet.raw_packet)
50
+
51
+ # @rbs () -> Array[StatisticsTable]
52
+ def tables
53
+ @session.statistics.map do |table|
54
+ value = table.to_h
55
+ rows = value[:rows].map do |row|
56
+ fields = row.dup #: Hash[Symbol, untyped]
57
+ fields[:path] = fields[:path].dup.freeze if fields[:path]
58
+ fields.each_value { |item| item.freeze if item.is_a?(String) }
59
+ case value[:kind]
60
+ when :phs
61
+ HierarchyRow.new(path: fields.fetch(:path), packets: fields.fetch(:packets), bytes: fields.fetch(:bytes))
62
+ when :conv
63
+ ConversationRow.new(addr_a: fields.fetch(:addr_a), port_a: fields.fetch(:port_a),
64
+ addr_b: fields.fetch(:addr_b), port_b: fields.fetch(:port_b),
65
+ packets_ab: fields.fetch(:packets_ab), packets_ba: fields.fetch(:packets_ba),
66
+ bytes_ab: fields.fetch(:bytes_ab), bytes_ba: fields.fetch(:bytes_ba),
67
+ start_ns: fields.fetch(:start_ns), end_ns: fields.fetch(:end_ns), duration_ns: fields.fetch(:duration_ns))
68
+ when :endpoints
69
+ EndpointRow.new(address: fields.fetch(:address), port: fields.fetch(:port),
70
+ packets_tx: fields.fetch(:packets_tx), packets_rx: fields.fetch(:packets_rx),
71
+ bytes_tx: fields.fetch(:bytes_tx), bytes_rx: fields.fetch(:bytes_rx))
72
+ else raise Vanken::Error, "invalid upstream statistics table"
73
+ end
74
+ end
75
+ StatisticsTable.new(kind: value[:kind], type: value[:type], rows: rows.freeze, evicted: table.evicted)
76
+ end.freeze
77
+ end
78
+
79
+ # @rbs () -> void
80
+ def close = @session.finish(StringIO.new, StringIO.new)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "tempfile"
5
+ require_relative "dissector"
6
+
7
+ module Vanken
8
+ module Gateway
9
+ StreamChunk = Data.define(
10
+ :direction, #: Integer
11
+ :bytes, #: String
12
+ :missing, #: Integer
13
+ :number, #: Integer
14
+ :sequence #: Integer
15
+ )
16
+ StreamText = Data.define(
17
+ :direction, #: Integer
18
+ :text #: String
19
+ )
20
+
21
+ class Stream
22
+ DISPLAY_LIMIT = 16 << 20 #: Integer
23
+ SEQUENCE_MOD = 1 << 32 #: Integer
24
+ attr_reader :nodes #: Array[String]
25
+ attr_reader :chunks #: Array[StreamChunk]
26
+ attr_reader :stream_id #: Integer
27
+
28
+ # @rbs (untyped document, Integer stream_id, ?cancelled: (^() -> bool)?) -> void
29
+ def initialize(document, stream_id, cancelled: nil)
30
+ raise ArgumentError, "invalid stream number" unless stream_id.is_a?(Integer) && stream_id >= 0
31
+ @stream_id, @nodes, @chunks = stream_id, [], []
32
+ pieces = [[], []] #: Array[Array[StreamChunk]]
33
+ starts = [nil, nil] #: Array[Integer?]
34
+ frontiers = [0, 0]
35
+ frames = document.stream_frames(stream_id)
36
+ frames.each do |number|
37
+ raise Vanken::Error, "operation cancelled" if cancelled&.call || document.closing?
38
+ packet = document.packet(number)
39
+ tcp = packet.layers.find { |layer| layer.protocol == :tcp && !layer.embedded }
40
+ next unless tcp
41
+ columns = packet.columns
42
+ endpoints = [endpoint(columns[:source], columns[:src_port]), endpoint(columns[:destination], columns[:dst_port])]
43
+ @nodes = tcp[:flags] & 0x12 == 0x12 ? endpoints.reverse : endpoints if @nodes.empty?
44
+ direction = endpoints == @nodes ? 0 : 1
45
+ sequence = document.annotations[number][:seq_rel]
46
+ difference = (sequence - frontiers[direction]) % SEQUENCE_MOD
47
+ difference -= SEQUENCE_MOD if difference >= SEQUENCE_MOD / 2
48
+ sequence = frontiers[direction] + difference
49
+ sequence += 1 if tcp[:flags] & 2 != 0
50
+ starts[direction] ||= sequence if tcp[:flags] & 2 != 0
51
+ data = packet.bytes.byteslice(tcp.payload_offset, tcp.payload_end - tcp.payload_offset)
52
+ frontiers[direction] = [frontiers[direction], sequence + data.bytesize].max
53
+ next if data.empty?
54
+ insert(pieces[direction], StreamChunk.new(direction: direction, bytes: data, missing: 0, number: number, sequence: sequence))
55
+ end
56
+ directions = pieces.each_with_index.map do |segments, direction|
57
+ cursor = starts[direction]
58
+ segments.flat_map do |segment|
59
+ missing = cursor && segment.sequence > cursor ? segment.sequence - cursor : 0
60
+ cursor = segment.sequence + segment.bytes.bytesize
61
+ gap = StreamChunk.new(direction: direction, bytes: "".b, missing: missing, number: segment.number, sequence: segment.sequence - missing)
62
+ missing.positive? ? [gap, segment] : [segment]
63
+ end
64
+ end
65
+ positions = [0, 0]
66
+ until positions.each_with_index.all? { |index, direction| index == directions[direction].size }
67
+ direction = [0, 1].reject { |value| positions[value] == directions[value].size }.min_by do |value|
68
+ directions[value][positions[value]].number
69
+ end
70
+ raise Vanken::Error, "invalid stream direction" unless direction
71
+ @chunks << directions[direction][positions[direction]]
72
+ positions[direction] += 1
73
+ end
74
+ @nodes.freeze
75
+ @chunks.freeze
76
+ end
77
+
78
+ # @rbs (?direction: Integer?) -> Integer
79
+ def byte_size(direction: nil) = selected(direction).sum { |chunk| chunk.bytes.bytesize }
80
+
81
+ # @rbs (?format: Symbol, ?direction: Integer?, ?limit: Integer) -> Array[StreamText]
82
+ def preview(format: :ascii, direction: nil, limit: DISPLAY_LIMIT)
83
+ validate(format, direction)
84
+ raise ArgumentError, "invalid display limit" unless limit.is_a?(Integer) && limit >= 0
85
+ remaining = limit
86
+ result = [] #: Array[StreamText]
87
+ selected(direction).each do |chunk|
88
+ break if remaining.zero?
89
+ bytes = chunk.bytes.byteslice(0, remaining) || "".b
90
+ remaining -= bytes.bytesize
91
+ result << StreamText.new(direction: chunk.direction, text: render(chunk, bytes, format))
92
+ end
93
+ result
94
+ end
95
+
96
+ # @rbs (String path, ?format: Symbol, ?direction: Integer?, ?cancelled: (^() -> bool)?) -> String
97
+ def save(path, format: :raw, direction: nil, cancelled: nil)
98
+ validate(format, direction)
99
+ file = Tempfile.create([".vanken-stream-", ".tmp"], File.dirname(File.expand_path(path)))
100
+ begin
101
+ file.binmode
102
+ selected(direction).each do |chunk|
103
+ raise Vanken::Error, "operation cancelled" if cancelled&.call
104
+ file.write(render(chunk, chunk.bytes, format))
105
+ end
106
+ file.flush
107
+ file.fsync
108
+ file.close
109
+ File.rename(file.path, File.expand_path(path))
110
+ ensure
111
+ file.close unless file.closed?
112
+ FileUtils.rm_f(file.path)
113
+ end
114
+ path
115
+ end
116
+
117
+ private
118
+
119
+ # @rbs (Integer? direction) -> Array[StreamChunk]
120
+ def selected(direction) = @chunks.select { |chunk| direction.nil? || chunk.direction == direction }
121
+ def endpoint(address, port) = address.include?(":") ? "[#{address}]:#{port}" : "#{address}:#{port}"
122
+ def validate(format, direction)
123
+ raise ArgumentError, "invalid stream format" unless %i[ascii hex raw].include?(format)
124
+ raise ArgumentError, "invalid stream direction" unless [nil, 0, 1].include?(direction)
125
+ end
126
+
127
+ def render(chunk, bytes, format)
128
+ return "".b if format == :raw && chunk.missing.positive?
129
+ return "[#{chunk.missing} bytes missing]" if chunk.missing.positive?
130
+ return bytes if format == :raw
131
+ return bytes.gsub(/[^\x20-\x7e\r\n\t]/n) { |byte| Kernel.format("\\x%02x", byte.getbyte(0)) } if format == :ascii
132
+ bytes.bytes.each_slice(16).with_index.map do |row, index|
133
+ Kernel.format("%08x %s\n", chunk.sequence + (index * 16), row.map { |byte| Kernel.format("%02x", byte) }.join(" "))
134
+ end.join
135
+ end
136
+
137
+ # Keep capture arrival precedence while inserting only previously unseen ranges.
138
+ def insert(segments, chunk)
139
+ first, last = chunk.sequence, chunk.sequence + chunk.bytes.bytesize
140
+ index = segments.bsearch_index { |segment| segment.sequence + segment.bytes.bytesize > first } || segments.size
141
+ uncovered = [] #: Array[[Integer, Integer]]
142
+ cursor = first
143
+ while index < segments.size && segments[index].sequence < last
144
+ previous = segments[index]
145
+ uncovered << [cursor, [previous.sequence, last].min] if previous.sequence > cursor
146
+ cursor = [cursor, previous.sequence + previous.bytes.bytesize].max
147
+ index += 1
148
+ end
149
+ uncovered << [cursor, last] if cursor < last
150
+ uncovered.each do |start, finish|
151
+ segment = chunk.with(sequence: start, bytes: chunk.bytes.byteslice(start - first, finish - start))
152
+ position = segments.bsearch_index { |previous| previous.sequence > start } || segments.size
153
+ # ponytail: array insertion is O(n) for heavily reordered streams; use an interval tree if measured.
154
+ segments.insert(position, segment)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -5,44 +5,70 @@ module Vanken
5
5
  module Actions
6
6
  def self.install(ui)
7
7
  commands = {
8
- open: ["開く", "cmd-o", -> { ui.open_dialog }], save: ["名前を付けて保存", "cmd-shift-s", -> { ui.save_dialog }],
9
- close_file: ["ファイルを閉じる", "cmd-w", -> { ui.close_document }], reload: ["再読み込み", "cmd-r", -> { ui.reload_file }],
10
- quit: ["終了", "cmd-q", -> { ui.quit }], preferences: ["設定", "cmd-,", -> { ui.preferences_dialog }],
11
- capture: ["キャプチャ開始/停止", "cmd-e", -> { ui.capture_toggle }], restart: ["キャプチャ再開", nil, -> { ui.restart_capture }],
12
- apply_filter: ["フィルタを適用", "enter", -> { ui.apply_filter }], clear_filter: ["フィルタをクリア", nil, -> { ui.clear_filter }],
13
- filter_history: ["フィルタ履歴", nil, -> { ui.filter_history }], bookmark: ["フィルタを保存", nil, -> { ui.bookmark_filter }],
14
- filter_selected: ["選択した項目でフィルタ", nil, -> { ui.filter_from_selection }],
15
- prepare_selected: ["選択した項目のフィルタを準備", nil, -> { ui.filter_from_selection(prepare: true) }],
16
- columns: ["表示する列", nil, -> { ui.columns_dialog }], autoscroll: ["末尾追従", nil, -> { ui.toggle_autoscroll }],
17
- zoom_in: ["拡大", "cmd-+", -> { ui.zoom(1) }], zoom_out: ["縮小", "cmd--", -> { ui.zoom(-1) }], zoom_reset: ["標準サイズ", "cmd-0", -> { ui.zoom }],
18
- previous_packet: ["前のパケット", "alt-up", -> { ui.navigate(-1) }], next_packet: ["次のパケット", "alt-down", -> { ui.navigate(1) }],
19
- first_packet: ["最初のパケット", nil, -> { ui.first_packet }], last_packet: ["最後のパケット", nil, -> { ui.last_packet }],
20
- go_to_packet: ["パケットへ移動", "cmd-g", -> { ui.go_to_packet }], copy_filter: ["項目をフィルタとしてコピー", nil, -> { ui.copy_text(ui.selected_node.filter) if ui.selected_node&.filter }]
8
+ open: [ui.t("開く"), "cmd-o", -> { ui.open_dialog }], save: [ui.t("名前を付けて保存"), "cmd-shift-s", -> { ui.save_dialog }],
9
+ close_file: [ui.t("ファイルを閉じる"), "cmd-w", -> { ui.close_document }], reload: [ui.t("再読み込み"), "cmd-r", -> { ui.reload_file }],
10
+ quit: [ui.t("終了"), "cmd-q", -> { ui.quit }], preferences: [ui.t("設定"), "cmd-,", -> { ui.preferences_dialog }],
11
+ capture: [ui.t("キャプチャ開始/停止"), "cmd-e", -> { ui.capture_toggle }], restart: [ui.t("キャプチャ再開"), nil, -> { ui.restart_capture }],
12
+ apply_filter: [ui.t("フィルタを適用"), "enter", -> { ui.apply_filter }], clear_filter: [ui.t("フィルタをクリア"), nil, -> { ui.clear_filter }],
13
+ filter_history: [ui.t("フィルタ履歴"), nil, -> { ui.filter_history }], bookmark: [ui.t("フィルタを保存"), nil, -> { ui.bookmark_filter }],
14
+ filter_selected: [ui.t("選択した項目でフィルタ"), nil, -> { ui.filter_from_selection }],
15
+ prepare_selected: [ui.t("選択した項目のフィルタを準備"), nil, -> { ui.filter_from_selection(prepare: true) }],
16
+ columns: [ui.t("表示する列"), nil, -> { ui.columns_dialog }], autoscroll: [ui.t("末尾追従"), nil, -> { ui.toggle_autoscroll }],
17
+ zoom_in: [ui.t("拡大"), "cmd-+", -> { ui.zoom(1) }], zoom_out: [ui.t("縮小"), "cmd--", -> { ui.zoom(-1) }], zoom_reset: [ui.t("標準サイズ"), "cmd-0", -> { ui.zoom }],
18
+ previous_packet: [ui.t("前のパケット"), "alt-up", -> { ui.navigate(-1) }], next_packet: [ui.t("次のパケット"), "alt-down", -> { ui.navigate(1) }],
19
+ first_packet: [ui.t("最初のパケット"), nil, -> { ui.first_packet }], last_packet: [ui.t("最後のパケット"), nil, -> { ui.last_packet }],
20
+ go_to_packet: [ui.t("パケットへ移動"), "cmd-g", -> { ui.go_to_packet }], copy_filter: [ui.t("項目をフィルタとしてコピー"), nil, -> { ui.copy_text(ui.selected_node.filter) if ui.selected_node&.filter }]
21
21
  }
22
- %i[selected not and and_not or or_not].zip(["選択項目", "選択項目を除外", "AND", "AND NOT", "OR", "OR NOT"]).each do |mode, label|
23
- commands[:"filter_#{mode}"] = ["適用: #{label}", nil, -> { ui.filter_from_selection(mode) }]
24
- commands[:"prepare_#{mode}"] = ["準備: #{label}", nil, -> { ui.filter_from_selection(mode, prepare: true) }]
22
+ commands.merge!(
23
+ find_packet: [ui.t("パケット検索"), "cmd-f", -> { ui.find_packet }], find_next: [ui.t("次を検索"), "cmd-n", -> { ui.find_next }], find_prev: [ui.t("前を検索"), "cmd-b", -> { ui.find_prev }],
24
+ mark_packet: [ui.t("マーク/解除"), "cmd-m", -> { ui.mark_packet }], mark_all_displayed: [ui.t("表示中をすべてマーク"), "cmd-shift-m", -> { ui.mark_all_displayed }], unmark_all: [ui.t("すべてのマークを解除"), "cmd-alt-m", -> { ui.unmark_all }],
25
+ ignore_packet: [ui.t("無視/解除"), "cmd-d", -> { ui.ignore_packet }], time_reference: [ui.t("時刻基準/解除"), "cmd-t", -> { ui.time_reference }],
26
+ prev_in_conversation: [ui.t("同一会話の前"), "cmd-,", -> { ui.prev_in_conversation }], next_in_conversation: [ui.t("同一会話の次"), "cmd-.", -> { ui.next_in_conversation }],
27
+ coloring_rules: [ui.t("色付けルール"), nil, -> { ui.coloring_dialog }], toggle_coloring: [ui.t("色付け"), nil, -> { ui.toggle_coloring }],
28
+ follow_tcp_stream: [ui.t("TCP ストリームを追跡"), "cmd-alt-shift-t", -> { ui.follow_tcp_stream }], decode_as: [ui.t("別プロトコルとして解析"), "cmd-shift-u", -> { ui.decode_as }],
29
+ expert_info: [ui.t("エキスパート情報"), nil, -> { ui.expert_info }], reload_plugins: [ui.t("プラグイン再読み込み"), "cmd-shift-l", -> { ui.reload_plugins }], plugins: [ui.t("ディセクタプラグイン"), nil, -> { ui.plugins_dialog }],
30
+ file_properties: [ui.t("キャプチャファイルのプロパティ"), "cmd-alt-shift-c", -> { ui.file_properties }], protocol_hierarchy: [ui.t("プロトコル階層"), nil, -> { ui.protocol_hierarchy }],
31
+ conversations: [ui.t("会話"), nil, -> { ui.conversations }], endpoints: [ui.t("端点"), nil, -> { ui.endpoints }], io_graph: [ui.t("I/O グラフ"), nil, -> { ui.io_graph }],
32
+ export_packets: [ui.t("指定パケットのエクスポート"), nil, -> { ui.export_packets }], export_dissections: [ui.t("解析結果のエクスポート"), nil, -> { ui.export_dissections }],
33
+ command_palette: [ui.t("コマンドパレット"), "cmd-shift-k", -> { ui.command_palette }], profiles: [ui.t("プロファイル"), nil, -> { ui.profiles_dialog }])
34
+ commands[:preferences][1] = "cmd-shift-p"
35
+ commands[:restart][1] = "cmd-shift-r"
36
+ {"relative" => "相対時刻", "absolute" => "絶対時刻", "delta" => "前パケットとの差", "delta_displayed" => "前表示パケットとの差", "epoch" => "エポック時刻"}.each { |name, label| commands[:"time_format_#{name}"] = [ui.t("時刻: %{name}", name: ui.t(label)), nil, -> { ui.time_format(name) }] }
37
+ {"system" => "システム設定", "dark" => "ダーク", "light" => "ライト", "high_contrast" => "ハイコントラスト"}.each { |name, label| commands[:"theme_#{name}"] = [ui.t("テーマ: %{name}", name: ui.t(label)), nil, -> { ui.change_theme(name) }] }
38
+ %i[selected not and and_not or or_not].zip([ui.t("選択項目"), ui.t("選択項目を除外"), "AND", "AND NOT", "OR", "OR NOT"]).each do |mode, label|
39
+ commands[:"filter_#{mode}"] = [ui.t("適用: %{label}", label: label), nil, -> { ui.filter_from_selection(mode) }]
40
+ commands[:"prepare_#{mode}"] = [ui.t("準備: %{label}", label: label), nil, -> { ui.filter_from_selection(mode, prepare: true) }]
25
41
  end
26
- {hex: "16 進数", text: "テキスト", dump: "ダンプ", escaped: "エスケープした文字列"}.each do |format, label|
27
- commands[:"copy_#{format}"] = ["バイトをコピー: #{label}", nil, -> { ui.copy_bytes(format) }]
42
+ {hex: ui.t("16 進数"), text: ui.t("テキスト"), dump: ui.t("ダンプ"), escaped: ui.t("エスケープした文字列")}.each do |format, label|
43
+ commands[:"copy_#{format}"] = [ui.t("バイトをコピー: %{label}", label: label), nil, -> { ui.copy_bytes(format) }]
28
44
  end
29
45
  commands.each do |name, (title, keys, operation)|
30
- ui.app.actions.register(name, title: title) { |_| operation.call }
46
+ needs_document = %i[find_packet find_next find_prev mark_packet mark_all_displayed unmark_all ignore_packet time_reference prev_in_conversation next_in_conversation follow_tcp_stream expert_info file_properties protocol_hierarchy conversations endpoints io_graph export_packets export_dissections]
47
+ enabled = ->(*) { !needs_document.include?(name) || !!ui.document }
48
+ checked = name == :autoscroll ? ->(*) { ui.autoscroll? } : name == :toggle_coloring ? ->(*) { ui.coloring_enabled? } : nil
49
+ ui.app.actions.register(name, title: title, enabled: enabled, checked: checked) { |_| operation.call }
31
50
  next unless keys
32
- key = RUBY_PLATFORM.include?("darwin") ? keys : keys.sub("cmd", "ctrl")
51
+ key = ui.native? && RUBY_PLATFORM.include?("darwin") ? keys : keys.sub("cmd", "ctrl")
33
52
  ui.window.dispatcher.keymap.bind(key, name, context: name == :apply_filter ? "in_display_filter && !in_completion" : "")
34
53
  end
35
54
  ui.app.menu_bar = Zaniah::Menu.build do
36
- submenu("ファイル") { %i[open save close_file reload quit].each { |action| item(action) } }
37
- submenu("編集") { %i[copy_filter copy_hex copy_text copy_dump copy_escaped preferences].each { |action| item(action) } }
38
- submenu("表示") { %i[columns autoscroll zoom_in zoom_out zoom_reset].each { |action| item(action) } }
39
- submenu("移動") { %i[previous_packet next_packet first_packet last_packet go_to_packet].each { |action| item(action) } }
40
- submenu("キャプチャ") { %i[capture restart].each { |action| item(action) } }
41
- submenu("フィルタ") do
55
+ submenu(ui.t("ファイル")) { %i[open save close_file reload export_packets export_dissections quit].each { |action| item(action) } }
56
+ submenu(ui.t("編集")) { %i[find_packet find_next find_prev mark_packet mark_all_displayed unmark_all ignore_packet time_reference copy_filter copy_hex copy_text copy_dump copy_escaped preferences profiles].each { |action| item(action) } }
57
+ submenu(ui.t("表示")) do
58
+ %i[columns autoscroll coloring_rules toggle_coloring zoom_in zoom_out zoom_reset].each { |action| item(action) }
59
+ submenu(ui.t("時刻形式")) { %w[relative absolute delta delta_displayed epoch].each { |name| item(:"time_format_#{name}") } }
60
+ submenu(ui.t("テーマ")) { %w[system dark light high_contrast].each { |name| item(:"theme_#{name}") } }
61
+ end
62
+ submenu(ui.t("移動")) { %i[previous_packet next_packet first_packet last_packet go_to_packet prev_in_conversation next_in_conversation].each { |action| item(action) } }
63
+ submenu(ui.t("キャプチャ")) { %i[capture restart].each { |action| item(action) } }
64
+ submenu(ui.t("フィルタ")) do
42
65
  %i[apply_filter clear_filter filter_history bookmark].each { |action| item(action) }
43
- submenu("選択項目を適用") { %i[selected not and and_not or or_not].each { |mode| item(:"filter_#{mode}") } }
44
- submenu("選択項目から準備") { %i[selected not and and_not or or_not].each { |mode| item(:"prepare_#{mode}") } }
66
+ submenu(ui.t("選択項目を適用")) { %i[selected not and and_not or or_not].each { |mode| item(:"filter_#{mode}") } }
67
+ submenu(ui.t("選択項目から準備")) { %i[selected not and and_not or or_not].each { |mode| item(:"prepare_#{mode}") } }
45
68
  end
69
+ submenu(ui.t("分析")) { %i[follow_tcp_stream decode_as expert_info plugins reload_plugins].each { |action| item(action) } }
70
+ submenu(ui.t("統計")) { %i[file_properties protocol_hierarchy conversations endpoints io_graph].each { |action| item(action) } }
71
+ submenu(ui.t("ヘルプ")) { item(:command_palette) }
46
72
  end
47
73
  end
48
74
  end