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, #nameservers, #preferred_network_password, #public_ip_address_info, #remove_preferred_networks, #run_os_command, #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.



44
45
46
47
48
49
50
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
# File 'lib/mac-wifi/mac_os_model.rb', line 44

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



107
108
109
110
111
112
113
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
# File 'lib/mac-wifi/mac_os_model.rb', line 107

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

#current_networkObject

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



236
237
238
239
240
# File 'lib/mac-wifi/mac_os_model.rb', line 236

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.



244
245
246
247
# File 'lib/mac-wifi/mac_os_model.rb', line 244

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.



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

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.



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

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


200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/mac-wifi/mac_os_model.rb', line 200

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



81
82
83
84
85
86
87
88
89
90
# File 'lib/mac-wifi/mac_os_model.rb', line 81

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.



147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/mac-wifi/mac_os_model.rb', line 147

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



228
229
230
231
232
# File 'lib/mac-wifi/mac_os_model.rb', line 228

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

#wifi_hardware_portObject

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



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/mac-wifi/mac_os_model.rb', line 17

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.



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/mac-wifi/mac_os_model.rb', line 251

def wifi_info

  info = {
      'wifi_on'     =>    wifi_on?,
      'internet_on' => connected_to_internet?,
      'port'        => wifi_hardware_port,
      'network'     => current_network,
      'ip_address'  => ip_address,
      'nameservers' => nameservers,
      '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

  if info['wifi_on']
    begin
      info['public_ip'] = public_ip_address_info
    rescue => e
      puts "Error obtaining public IP address info, proceeding with everything else:"
      puts e.to_s
    end
  end
  info
end

#wifi_offObject

Turns wifi off.



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

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.



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

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)


162
163
164
165
# File 'lib/mac-wifi/mac_os_model.rb', line 162

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