Class: MacWifi::MacOsModel

Inherits:
BaseModel show all
Defined in:
lib/mac-wifi/mac_os_model.rb

Constant Summary collapse

AIRPORT_CMD =
'/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'

Instance Method Summary collapse

Methods inherited from BaseModel

#connect, #connected_to?, #connected_to_internet?, #cycle_network, #preferred_network_password, #remove_preferred_networks, #till, #try_os_command_until

Constructor Details

#initialize(verbose = false) ⇒ MacOsModel

Returns a new instance of MacOsModel.



11
12
13
# File 'lib/mac-wifi/mac_os_model.rb', line 11

def initialize(verbose = false)
  super
end

Instance Method Details

#available_network_infoObject

Returns data pertaining to available wireless networks. For some reason, this often returns no results, so I’ve put the operation in a loop. I was unable to detect a sort strategy in the airport utility’s output, so I sort the lines alphabetically, to show duplicates and for easier lookup.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/mac-wifi/mac_os_model.rb', line 51

def available_network_info
  return nil unless wifi_on? # no need to try
  command = "#{AIRPORT_CMD} -s"
  max_attempts = 50


  reformat_line = ->(line) do
    ssid = line[0..31].strip
    "%-32.32s%s" % [ssid, line[32..-1]]
  end


  process_tabular_data = ->(output) do
    lines = output.split("\n")
    header_line = lines[0]
    data_lines = lines[1..-1]
    data_lines.map! do |line|
      # Reformat the line so that the name is left instead of right justified
      reformat_line.(line)
    end
    data_lines.sort!
    [reformat_line.(header_line)] + data_lines
  end


  output = try_os_command_until(command, ->(output) do
    ! ([nil, ''].include?(output))
  end)

  if output
    process_tabular_data.(output)
  else
    raise "Unable to get available network information after #{max_attempts} attempts."
  end
end

#available_network_namesObject

Kludge alert: the tabular data does not differentiate between strings with and without leading whitespace Therefore, we get the data once in tabular format, and another time in XML format. The XML element will include any leading whitespace. However, it includes all <string> elements, many of which are not network names. As an improved approximation of the correct result, for each network name found in tabular mode, we look to see if there is a corresponding string element with leading whitespace, and, if so, replace it.

This will not behave correctly if a given name has occurrences with different amounts of whitespace, e.g. ‘ x’ and ‘ x’.

The reason we don’t use an XML parser to get the exactly correct result is that we don’t want users to need to install any external dependencies in order to run this script.

Returns:

  • an array of unique available network names only, sorted alphabetically



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/mac-wifi/mac_os_model.rb', line 114

def available_network_names

  # Parses the XML text (using grep, not XML parsing) to find
  # <string> elements, and extracts the network name candidates
  # containing leading spaces from it.
  get_leading_space_names = ->(text) do
    text.split("\n") \
      .grep(%r{<string>}) \
      .sort \
      .uniq \
      .map { |line| line.gsub("<string>", '').gsub('</string>', '').gsub("\t", '') } \
      .select { |s| s[0] == ' ' }
  end


  output_is_valid = ->(output) { ! ([nil, ''].include?(output)) }
  tabular_data = try_os_command_until("#{AIRPORT_CMD} -s", output_is_valid)
  xml_data     = try_os_command_until("#{AIRPORT_CMD} -s -x", output_is_valid)

  if tabular_data.nil? || xml_data.nil?
    raise "Unable to get available network information; please try again."
  end

  tabular_data_lines = tabular_data[1..-1] # omit header line
  names_no_spaces    = parse_network_names(tabular_data_lines.split("\n")).map(&:strip)
  names_maybe_spaces =  get_leading_space_names.(xml_data)

  names = names_no_spaces.map do |name_no_spaces|
    match = names_maybe_spaces.detect do |name_maybe_spaces|
      %r{[ \t]?#{name_no_spaces}$}.match(name_maybe_spaces)
    end

    match ? match : name_no_spaces
  end

  names.sort { |s1, s2| s1.casecmp(s2) }    # sort alphabetically, case insensitively
end

#connected_network_nameObject



191
192
193
# File 'lib/mac-wifi/mac_os_model.rb', line 191

def connected_network_name
  wifi_info['SSID']
end

#current_networkObject

Returns the network currently connected to, or nil if none.



248
249
250
251
252
# File 'lib/mac-wifi/mac_os_model.rb', line 248

def current_network
  lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
  ssid_lines = lines.grep(/ SSID:/)
  ssid_lines.empty? ? nil : ssid_lines.first.split('SSID: ').last.strip
end

#disconnectObject

Disconnects from the currently connected network. Does not turn off wifi.



256
257
258
259
# File 'lib/mac-wifi/mac_os_model.rb', line 256

def disconnect
  run_os_command("sudo #{AIRPORT_CMD} -z")
  nil
end

#ip_addressObject

Returns the IP address assigned to the wifi port, or nil if none.



227
228
229
230
231
232
233
234
235
236
237
# File 'lib/mac-wifi/mac_os_model.rb', line 227

def ip_address
  begin
    run_os_command("ipconfig getifaddr #{wifi_hardware_port}").chomp
  rescue OsCommandError => error
    if error.exitstatus == 1
      nil
    else
      raise
    end
  end
end

#os_level_connect(network_name, password = nil) ⇒ Object

This method is called by BaseModel#connect to do the OS-specific connection logic.



197
198
199
200
201
202
203
# File 'lib/mac-wifi/mac_os_model.rb', line 197

def os_level_connect(network_name, password = nil)
  command = "networksetup -setairportnetwork #{wifi_hardware_port} " + "#{Shellwords.shellescape(network_name)}"
  if password
    command << ' ' << Shellwords.shellescape(password)
  end
  run_os_command(command)
end

#os_level_preferred_network_password(preferred_network_name) ⇒ 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


212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/mac-wifi/mac_os_model.rb', line 212

def os_level_preferred_network_password(preferred_network_name)
  command = %Q{security find-generic-password -D "AirPort network password" -a "#{preferred_network_name}" -w 2>&1}
  begin
    return run_os_command(command).chomp
  rescue OsCommandError => error
    if error.exitstatus == 44 # network has no password stored
      nil
    else
      raise
    end
  end
end

#parse_network_names(info) ⇒ Object



88
89
90
91
92
93
94
95
96
97
# File 'lib/mac-wifi/mac_os_model.rb', line 88

def parse_network_names(info)
  if info.nil?
    nil
  else
    info[1..-1] \
    .map { |line| line[0..32].rstrip } \
    .uniq \
    .sort { |s1, s2| s1.casecmp(s2) }
  end
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
# File 'lib/mac-wifi/mac_os_model.rb', line 154

def preferred_networks
  lines = run_os_command("networksetup -listpreferredwirelessnetworks #{wifi_hardware_port}").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

#remove_preferred_network(network_name) ⇒ Object



240
241
242
243
244
# File 'lib/mac-wifi/mac_os_model.rb', line 240

def remove_preferred_network(network_name)
  network_name = network_name.to_s
  run_os_command("sudo networksetup -removepreferredwirelessnetwork " +
                     "#{wifi_hardware_port} #{Shellwords.shellescape(network_name)}")
end

#vpn_running?Boolean

This is determined by whether or not a line like the following appears in the output of ‘netstat -nr`: 0/1 10.137.0.41 UGSc 15 0 utun1

Returns:

  • (Boolean)


18
19
20
# File 'lib/mac-wifi/mac_os_model.rb', line 18

def vpn_running?
  run_os_command('netstat -nr').split("\n").grep(/^0\/1.*utun1/).any?
end

#wifi_hardware_portObject

Identifies the (first) wireless network hardware port in the system, e.g. en0 or en1



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/mac-wifi/mac_os_model.rb', line 24

def wifi_hardware_port
  @wifi_hardware_port ||= begin
    lines = run_os_command("networksetup -listallhardwareports").split("\n")
    # Produces something like this:
    # Hardware Port: Wi-Fi
    # Device: en0
    # Ethernet Address: ac:bc:32:b9:a9:9d
    #
    # Hardware Port: Bluetooth PAN
    # Device: en3
    # Ethernet Address: ac:bc:32:b9:a9:9e
    wifi_port_line_num = (0...lines.size).detect do |index|
      /: Wi-Fi$/.match(lines[index])
    end
    if wifi_port_line_num.nil?
      raise %Q{Wifi port (e.g. "en0") not found in output of: networksetup -listallhardwareports}
    else
      lines[wifi_port_line_num + 1].split(': ').last
    end
  end
end

#wifi_infoObject

Returns some useful wifi-related information.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/mac-wifi/mac_os_model.rb', line 263

def wifi_info

  info = {
      wifi_on:     wifi_on?,
      internet_on: connected_to_internet?,
      vpn_on:      vpn_running?,
      port:        wifi_hardware_port,
      network:     current_network,
      ip_address:  ip_address,
      timestamp:   Time.now,
  }
  more_output = run_os_command(AIRPORT_CMD + " -I")
  more_info   = colon_output_to_hash(more_output)
  info.merge!(more_info)
  info.delete('AirPort') # will be here if off, but info is already in wifi_on key
  info
end

#wifi_offObject

Turns wifi off.



184
185
186
187
188
# File 'lib/mac-wifi/mac_os_model.rb', line 184

def wifi_off
  return unless wifi_on?
  run_os_command("networksetup -setairportpower #{wifi_hardware_port} off")
  wifi_on? ? raise("Wifi could not be disabled.") : nil
end

#wifi_onObject

Turns wifi on.



176
177
178
179
180
# File 'lib/mac-wifi/mac_os_model.rb', line 176

def wifi_on
  return if wifi_on?
  run_os_command("networksetup -setairportpower #{wifi_hardware_port} on")
  wifi_on? ? nil : raise("Wifi could not be enabled.")
end

#wifi_on?Boolean

Returns true if wifi is on, else false.

Returns:

  • (Boolean)


169
170
171
172
# File 'lib/mac-wifi/mac_os_model.rb', line 169

def wifi_on?
  lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
  lines.grep("AirPort: Off").none?
end