Class: WifiWand::Platforms::Mac::Model

Inherits:
BaseModel
  • Object
show all
Defined in:
lib/wifi_wand/platforms/mac/model.rb

Constant Summary collapse

SYSTEM_PROFILER_TIMEOUT_SECONDS =
SystemProfilerWifiDataProvider::SYSTEM_PROFILER_TIMEOUT_SECONDS
KEYCHAIN_LOOKUP_TIMEOUT_SECONDS =
KeychainPasswordReader::DEFAULT_LOOKUP_TIMEOUT_SECONDS
SUDO_AUTH_CHECK_TIMEOUT_SECONDS =
1
SUDO_NETWORKSETUP_TIMEOUT_SECONDS =
5

Instance Attribute Summary

Attributes inherited from BaseModel

#command_executor, #connection_manager, #connectivity_tester, #disconnect_manager, #runtime_config, #state_manager, #status_waiter, #wifi_interface

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseModel

#available_network_names, #available_resources_help, #captive_portal_login_required, #capture_network_state, #command_available?, #connect, #connected_network_name, create_model, current_os_matches_this_model?, #cycle_network, #disassociated_stable?, #disconnect, #disconnect_associated?, #disconnect_stability_window_in_secs, #dns_working?, #err_stream, #err_stream=, #has_preferred_network?, inherited, #init, #init_wifi_interface, #internet_connectivity_state, #internet_tcp_connectivity?, #ipv4_addresses, #ipv6_addresses, #last_connection_used_saved_password?, #mac?, #open_resources_by_codes, #os, #out_stream, #out_stream=, #public_ip_address, #public_ip_country, #public_ip_info, #public_ip_lookup, #qr_code_generator, #random_mac_address, #remove_preferred_networks, #resource_manager, #restore_network_state, #run_command, #till, #ubuntu?, #verbose=, #verbose?, #wifi_info_builder

Methods included from Timing

monotonic_now, status_deadline, status_timeout_for

Methods included from StringPredicates

string_nil_or_blank?, string_nil_or_empty?

Constructor Details

#initialize(options = {}) ⇒ Model

Returns a new instance of Model.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/wifi_wand/platforms/mac/model.rb', line 38

def initialize(options = {})
  super
  # Defer macOS version detection until first needed to minimize incidental OS calls
  @macos_version = nil
  @helper_client = nil
  @swift_runtime = nil
  @system_profiler_wifi_data_provider = nil
  @dns_manager = nil
  @interface_detector = nil
  @wifi_service_name_source = nil
  @keychain_password_reader = nil
  @network_identity_reader = nil
  @network_scanner = nil
  @current_network_details = nil
  @status_queries = nil
  @system_network_info = nil
end

Class Method Details

.os_idObject



94
95
96
# File 'lib/wifi_wand/platforms/mac/model.rb', line 94

def self.os_id
  :mac
end

Instance Method Details

#_available_network_namesObject

Returns the network names sorted in descending order of signal strength.



139
140
141
# File 'lib/wifi_wand/platforms/mac/model.rb', line 139

def _available_network_names
  network_scanner.available_network_names
end

#_available_network_scanObject



149
150
151
# File 'lib/wifi_wand/platforms/mac/model.rb', line 149

def _available_network_scan
  network_scanner.scan
end

#_connect(network_name, password = nil) ⇒ Object

Connect mutations flow through the direct Swift-source transport path. That path owns the Swift/CoreWLAN attempt plus fallback to traditional macOS utilities when Swift is unavailable or the connect attempt fails in known ways.



231
232
233
234
235
236
# File 'lib/wifi_wand/platforms/mac/model.rb', line 231

def _connect(network_name, password = nil)
  invalidate_system_profiler_wifi_data_cache
  wifi_transport.connect(network_name, password)
ensure
  invalidate_system_profiler_wifi_data_cache
end

#_connected_network_nameObject

Fulfills BaseModel's abstract-method contract; BaseModel's connected_network_name wraps this with the shared wifi_on? guard.



307
308
309
# File 'lib/wifi_wand/platforms/mac/model.rb', line 307

def _connected_network_name
  network_identity_reader.connected_network_name
end

#_disconnectObject

Disconnects from the currently connected network. Does not turn off WiFi. Disconnect mutations use the direct Swift-source transport path first, with ifconfig fallback when the Swift/CoreWLAN path is unavailable or fails.



336
337
338
339
340
341
# File 'lib/wifi_wand/platforms/mac/model.rb', line 336

def _disconnect
  invalidate_system_profiler_wifi_data_cache
  wifi_transport.disconnect
ensure
  invalidate_system_profiler_wifi_data_cache
end

#_ipv4_addressesObject



256
257
258
# File 'lib/wifi_wand/platforms/mac/model.rb', line 256

def _ipv4_addresses
  system_network_info.ipv4_addresses
end

#_ipv6_addressesObject



260
261
262
# File 'lib/wifi_wand/platforms/mac/model.rb', line 260

def _ipv6_addresses
  system_network_info.ipv6_addresses
end

#_preferred_network_password(preferred_network_name, timeout_in_secs: KEYCHAIN_LOOKUP_TIMEOUT_SECONDS) ⇒ Object

@return:

If the network is in the preferred networks list
If a password is associated w/this network, return the password
If not, return nil
else
raise an error


251
252
253
254
# File 'lib/wifi_wand/platforms/mac/model.rb', line 251

def _preferred_network_password(preferred_network_name,
  timeout_in_secs: KEYCHAIN_LOOKUP_TIMEOUT_SECONDS)
  keychain_password_reader.password_for(preferred_network_name, timeout_in_secs: timeout_in_secs)
end

#associated?Boolean

Returns:

  • (Boolean)


179
180
181
# File 'lib/wifi_wand/platforms/mac/model.rb', line 179

def associated?
  network_identity_reader.associated?
end

#available_network_scanObject

Raises:



143
144
145
146
147
# File 'lib/wifi_wand/platforms/mac/model.rb', line 143

def available_network_scan
  raise WifiOffError, 'WiFi is off, cannot scan for available networks.' unless wifi_on?

  _available_network_scan
end

#bssidObject



347
348
349
350
# File 'lib/wifi_wand/platforms/mac/model.rb', line 347

def bssid
  result = helper_client.connected_network_bssid
  result.payload
end

#connected?Boolean

Returns:

  • (Boolean)


183
184
185
# File 'lib/wifi_wand/platforms/mac/model.rb', line 183

def connected?
  network_identity_reader.connected?
end

#connection_ready?(network_name) ⇒ Boolean

Returns:

  • (Boolean)


56
57
58
# File 'lib/wifi_wand/platforms/mac/model.rb', line 56

def connection_ready?(network_name)
  with_system_profiler_wifi_data_cache_scope { super }
end

#connection_security_typeString?

Gets the security type of the currently connected network.

Returns:

  • (String, nil)

    The security type: "WPA", "WPA2", "WPA3", "WEP", "NONE" for open networks, or nil if not connected/not found



536
537
538
# File 'lib/wifi_wand/platforms/mac/model.rb', line 536

public def connection_security_type
  current_network_details.connection_security_type
end

#default_interfaceObject

Returns the network interface used for default internet route on macOS



369
370
371
# File 'lib/wifi_wand/platforms/mac/model.rb', line 369

def default_interface
  system_network_info.default_interface
end

#detect_wifi_interface_using_networksetup(timeout_in_secs: nil) ⇒ Object

Identifies the (first) wireless network hardware interface in the system, e.g. en0 or en1 This may not detect WiFi ports with nonstandard names, such as USB WiFi devices.



114
115
116
117
118
119
120
121
122
# File 'lib/wifi_wand/platforms/mac/model.rb', line 114

def detect_wifi_interface_using_networksetup(timeout_in_secs: nil)
  result = interface_detector.detect_using_networksetup(
    timeout_in_secs: timeout_in_secs,
    known_interface: @wifi_interface
  )
  update_wifi_detection_state(result)

  result.interface
end

#generate_qr_code(filespec = nil, overwrite: false, password: nil, in_stream: $stdin) ⇒ Object



76
77
78
79
80
# File 'lib/wifi_wand/platforms/mac/model.rb', line 76

def generate_qr_code(filespec = nil, overwrite: false, password: nil, in_stream: $stdin)
  with_system_profiler_wifi_data_cache_scope do
    super(filespec, overwrite: overwrite, password: password, in_stream: in_stream)
  end
end

#helper_clientObject



323
324
325
326
327
328
329
330
# File 'lib/wifi_wand/platforms/mac/model.rb', line 323

def helper_client
  @helper_client ||= WifiWand::Platforms::Mac::Helper::Client.new(
    out_stream_provider:  -> { out_stream },
    err_stream_provider:  -> { err_stream },
    verbosity_provider:   -> { verbose? },
    macos_version_reader: ->(timeout_in_secs: nil) { macos_version(timeout_in_secs: timeout_in_secs) }
  )
end

#is_wifi_interface?(interface) ⇒ Boolean

Returns whether or not the specified interface is a WiFi interface.

Returns:

  • (Boolean)


171
172
173
# File 'lib/wifi_wand/platforms/mac/model.rb', line 171

def is_wifi_interface?(interface)
  interface_detector.is_wifi_interface?(interface)
end

#mac_addressObject



343
344
345
# File 'lib/wifi_wand/platforms/mac/model.rb', line 343

def mac_address
  system_network_info.mac_address
end

#macos_version(timeout_in_secs: nil) ⇒ Object

Lazily detected macOS version to avoid OS calls during initialization



34
35
36
# File 'lib/wifi_wand/platforms/mac/model.rb', line 34

def macos_version(timeout_in_secs: nil)
  @macos_version ||= system_network_info.detect_macos_version(timeout_in_secs: timeout_in_secs)
end

#nameserversObject



366
# File 'lib/wifi_wand/platforms/mac/model.rb', line 366

def nameservers = nameservers_using_scutil

#nameservers_using_networksetupObject



362
363
364
# File 'lib/wifi_wand/platforms/mac/model.rb', line 362

def nameservers_using_networksetup
  dns_manager.nameservers_using_networksetup
end

#nameservers_using_scutilObject



358
359
360
# File 'lib/wifi_wand/platforms/mac/model.rb', line 358

def nameservers_using_scutil
  dns_manager.nameservers_using_scutil
end

#network_hidden?Boolean

Checks if the currently connected network is a hidden network. A hidden network does not broadcast its SSID.

Returns:

  • (Boolean)

    true if connected to a hidden network, false otherwise

  • (Boolean)


553
554
555
# File 'lib/wifi_wand/platforms/mac/model.rb', line 553

public def network_hidden?
  current_network_details.network_hidden?
end

#network_identity_redacted?Boolean

Returns:

  • (Boolean)


315
316
317
# File 'lib/wifi_wand/platforms/mac/model.rb', line 315

def network_identity_redacted?
  network_identity_reader.network_identity_redacted?
end

#network_identity_redaction_reasonObject



319
320
321
# File 'lib/wifi_wand/platforms/mac/model.rb', line 319

def network_identity_redaction_reason
  network_identity_reader.network_identity_redaction_reason
end

#open_resource(resource_url) ⇒ Object



356
# File 'lib/wifi_wand/platforms/mac/model.rb', line 356

def open_resource(resource_url) = system_network_info.open_resource(resource_url)

#preferred_network_password(preferred_network_name, timeout_in_secs: nil) ⇒ Object

Password lookups on macOS may block on a user-facing keychain approval dialog, so the public lookup API defaults to waiting indefinitely unless a caller explicitly requests a timeout.



241
242
243
# File 'lib/wifi_wand/platforms/mac/model.rb', line 241

def preferred_network_password(preferred_network_name, timeout_in_secs: nil)
  super
end

#preferred_networksObject

Returns data pertaining to "preferred" networks, many/most of which will probably not be available.



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/wifi_wand/platforms/mac/model.rb', line 154

def preferred_networks
  iface = wifi_interface
  lines = run_command(
    ['networksetup', '-listpreferredwirelessnetworks', iface]
  ).stdout.split("\n")
  # Produces something like this, unsorted, and with leading tabs:
  # Preferred networks on en0:
  #         LibraryWiFi
  #         @thePAD/Magma

  lines.delete_at(0)                         # remove title line
  lines.map! { |line| line.gsub("\t", '') }  # remove leading tabs
  lines.sort! { |s1, s2| s1.casecmp(s2) }    # sort alphabetically, case insensitively
  lines
end


88
89
90
91
92
# File 'lib/wifi_wand/platforms/mac/model.rb', line 88

def print_qr_code(password: nil)
  with_system_profiler_wifi_data_cache_scope do
    super(password: password)
  end
end

#probe_wifi_interface(timeout_in_secs: nil) ⇒ Object

Identifies the (first) wireless network hardware interface in the system, e.g. en0 or en1 Prefer the faster networksetup path and fall back to system_profiler if needed. This may not detect WiFi ports with nonstandard names, such as USB WiFi devices.



127
128
129
130
131
132
133
134
135
136
# File 'lib/wifi_wand/platforms/mac/model.rb', line 127

def probe_wifi_interface(timeout_in_secs: nil)
  result = interface_detector.probe(
    timeout_in_secs:    timeout_in_secs,
    known_interface:    @wifi_interface,
    known_service_name: @wifi_service_name
  )
  update_wifi_detection_state(result)

  result.interface
end

#remove_preferred_network(network_name) ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
# File 'lib/wifi_wand/platforms/mac/model.rb', line 264

def remove_preferred_network(network_name)
  network_name = network_name.to_s
  iface = wifi_interface
  ensure_sudo_authenticated_for_preferred_network_removal!
  run_command(
    ['sudo', 'networksetup', '-removepreferredwirelessnetwork', iface, network_name],
    raise_on_error:  true,
    timeout_in_secs: SUDO_NETWORKSETUP_TIMEOUT_SECONDS
  )
  [network_name]
end

#render_qr_code(format: :ansi, password: nil) ⇒ Object



82
83
84
85
86
# File 'lib/wifi_wand/platforms/mac/model.rb', line 82

def render_qr_code(format: :ansi, password: nil)
  with_system_profiler_wifi_data_cache_scope do
    super(format: format, password: password)
  end
end

#set_nameservers(nameservers) ⇒ Object

rubocop:disable Naming/AccessorMethodName



352
353
354
# File 'lib/wifi_wand/platforms/mac/model.rb', line 352

def set_nameservers(nameservers) # rubocop:disable Naming/AccessorMethodName
  dns_manager.set_nameservers(nameservers)
end

#signal_qualityObject



540
541
542
543
544
545
546
547
548
# File 'lib/wifi_wand/platforms/mac/model.rb', line 540

public def signal_quality
  helper_result = helper_client.connected_network_name
  return helper_result.signal_quality if helper_result.signal_quality
  return nil if helper_result.not_connected?

  current_network_details.signal_quality
rescue WifiWand::Error
  current_network_details.signal_quality
end

#status_line_data(progress_callback: nil) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/wifi_wand/platforms/mac/model.rb', line 64

def status_line_data(progress_callback: nil)
  with_system_profiler_wifi_data_cache_scope do
    StatusLineDataBuilder.call(
      self,
      progress_callback:                          progress_callback,
      runtime_config:                             runtime_config,
      expected_network_errors:                    NetworkErrorConstants::EXPECTED_NETWORK_ERRORS,
      connectivity_worker_result_timeout_seconds: TimingConstants::OVERALL_CONNECTIVITY_TIMEOUT
    )
  end
end

#status_network_identity(timeout_in_secs: nil) ⇒ Object



187
188
189
# File 'lib/wifi_wand/platforms/mac/model.rb', line 187

def status_network_identity(timeout_in_secs: nil)
  status_queries.status_network_identity(timeout_in_secs: timeout_in_secs)
end

#status_wifi_on?(timeout_in_secs: nil) ⇒ Boolean

Returns:

  • (Boolean)


191
192
193
# File 'lib/wifi_wand/platforms/mac/model.rb', line 191

def status_wifi_on?(timeout_in_secs: nil)
  status_queries.status_wifi_on?(timeout_in_secs: timeout_in_secs)
end

#validate_os_preconditions(timeout_in_secs: nil) ⇒ Object



373
374
375
376
377
378
# File 'lib/wifi_wand/platforms/mac/model.rb', line 373

def validate_os_preconditions(timeout_in_secs: nil)
  # All core read/status commands are built into macOS. The Swift/CoreWLAN
  # source runtime is mutation-specific and is probed lazily by the
  # connect/disconnect transport.
  :ok
end

#wifi_infoObject



60
61
62
# File 'lib/wifi_wand/platforms/mac/model.rb', line 60

def wifi_info
  with_system_profiler_wifi_data_cache_scope { wifi_info_builder.build }
end

#wifi_offObject

Turns WiFi off.



212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/wifi_wand/platforms/mac/model.rb', line 212

def wifi_off
  return unless wifi_on?

  invalidate_system_profiler_wifi_data_cache

  begin
    iface = wifi_interface
    run_command(['networksetup', '-setairportpower', iface, 'off'])
  ensure
    invalidate_system_profiler_wifi_data_cache
  end

  wifi_on? ? raise(WifiDisableError) : nil
end

#wifi_onObject

Turns WiFi on.



196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/wifi_wand/platforms/mac/model.rb', line 196

def wifi_on
  return if wifi_on?

  invalidate_system_profiler_wifi_data_cache

  begin
    iface = wifi_interface
    run_command(['networksetup', '-setairportpower', iface, 'on'])
  ensure
    invalidate_system_profiler_wifi_data_cache
  end

  wifi_on? ? nil : raise(WifiEnableError)
end

#wifi_on?Boolean

Returns:

  • (Boolean)


175
176
177
# File 'lib/wifi_wand/platforms/mac/model.rb', line 175

def wifi_on?
  system_network_info.wifi_on?
end

#wifi_service_nameObject

Returns the Wi-Fi service name dynamically (e.g., "Wi-Fi", "AirPort", etc.)



99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/wifi_wand/platforms/mac/model.rb', line 99

def wifi_service_name
  return @wifi_service_name if @wifi_service_name && !@wifi_service_name.empty?

  service_name = interface_detector.detected_wifi_service_name(known_interface: @wifi_interface)
  if service_name && !service_name.empty?
    @wifi_service_name_source = :detected
    @wifi_service_name = service_name
  else
    @wifi_service_name_source = :fallback
    @wifi_service_name = 'Wi-Fi'
  end
end