wifi-wand 2.4.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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +363 -0
- data/RELEASE_NOTES.md +81 -0
- data/exe/wifi-wand +24 -0
- data/lib/wifi-wand.rb +3 -0
- data/lib/wifi-wand/command_line_interface.rb +462 -0
- data/lib/wifi-wand/main.rb +66 -0
- data/lib/wifi-wand/models/base_model.rb +250 -0
- data/lib/wifi-wand/models/mac_os_model.rb +299 -0
- data/lib/wifi-wand/operating_systems.rb +51 -0
- data/lib/wifi-wand/os/base_os.rb +25 -0
- data/lib/wifi-wand/os/imaginary_os.rb +19 -0
- data/lib/wifi-wand/os/mac_os.rb +20 -0
- data/lib/wifi-wand/version.rb +5 -0
- data/sample-avail-network-data.xml +4042 -0
- data/sample-available-networks.json +27 -0
- data/sample-available-networks.yaml +26 -0
- data/spec/wifi-wand_spec.rb +114 -0
- data/wifi-wand.gemspec +28 -0
- metadata +108 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require_relative 'command_line_interface'
|
|
2
|
+
require_relative 'operating_systems'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
module WifiWand
|
|
6
|
+
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'optparse'
|
|
9
|
+
require 'ostruct'
|
|
10
|
+
require 'yaml'
|
|
11
|
+
|
|
12
|
+
class Main
|
|
13
|
+
|
|
14
|
+
# Parses the command line with Ruby's internal 'optparse'.
|
|
15
|
+
# Looks for "-v" flag to set verbosity to true.
|
|
16
|
+
# optparse removes what it processes from ARGV, which simplifies our command parsing.
|
|
17
|
+
def parse_command_line
|
|
18
|
+
options = OpenStruct.new
|
|
19
|
+
OptionParser.new do |parser|
|
|
20
|
+
parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
|
21
|
+
options.verbose = v
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
parser.on("-s", "--shell", "Start interactive shell") do |v|
|
|
25
|
+
options.interactive_mode = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
parser.on("-o", "--output_format FORMAT", "Format output data") do |v|
|
|
29
|
+
|
|
30
|
+
transformers = {
|
|
31
|
+
'i' => ->(object) { object.inspect },
|
|
32
|
+
'j' => ->(object) { JSON.pretty_generate(object) },
|
|
33
|
+
'p' => ->(object) { sio = StringIO.new; sio.puts(object); sio.string },
|
|
34
|
+
'y' => ->(object) { object.to_yaml }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
choice = v[0].downcase
|
|
38
|
+
|
|
39
|
+
unless transformers.keys.include?(choice)
|
|
40
|
+
raise %Q{Output format "#{choice}" not in list of available formats} +
|
|
41
|
+
" (#{transformers.keys.inspect})."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
options.post_processor = transformers[choice]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
parser.on("-h", "--help", "Show help") do |_help_requested|
|
|
48
|
+
ARGV << 'h' # pass on the request to the command processor
|
|
49
|
+
end
|
|
50
|
+
end.parse!
|
|
51
|
+
options
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def call
|
|
56
|
+
operating_systems = OperatingSystems.new
|
|
57
|
+
unless operating_systems.current_id == :mac
|
|
58
|
+
raise "OS not in supported list of #{operating_systems.supported_os_names.inspect}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
options = parse_command_line
|
|
62
|
+
|
|
63
|
+
WifiWand::CommandLineInterface.new(options).call
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
require_relative '../../wifi-wand'
|
|
4
|
+
|
|
5
|
+
module WifiWand
|
|
6
|
+
|
|
7
|
+
class BaseModel
|
|
8
|
+
|
|
9
|
+
class OsCommandError < RuntimeError
|
|
10
|
+
attr_reader :exitstatus, :command, :text
|
|
11
|
+
|
|
12
|
+
def initialize(exitstatus, command, text)
|
|
13
|
+
@exitstatus = exitstatus
|
|
14
|
+
@command = command
|
|
15
|
+
@text = text
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def initialize(verbose = false)
|
|
21
|
+
@verbose_mode = verbose
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_os_command(command)
|
|
26
|
+
output = `#{command} 2>&1` # join stderr with stdout
|
|
27
|
+
if $?.exitstatus != 0
|
|
28
|
+
raise OsCommandError.new($?.exitstatus, command, output)
|
|
29
|
+
end
|
|
30
|
+
if @verbose_mode
|
|
31
|
+
puts "\n\n#{'-' * 79}\nCommand: #{command}\n\nOutput:\n#{output}#{'-' * 79}\n\n"
|
|
32
|
+
end
|
|
33
|
+
output
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# This method returns whether or not there is a working Internet connection.
|
|
38
|
+
# Because of a Mac issue which causes a request to hang if the network is turned
|
|
39
|
+
# off during its lifetime, we give it only 5 seconds per try,
|
|
40
|
+
# and limit the number of tries to 3.
|
|
41
|
+
#
|
|
42
|
+
# This implementation will probably strike you as overly complex. The following
|
|
43
|
+
# code looks like it is all that should be necessary, but unfortunately
|
|
44
|
+
# this implementation often hangs when wifi is turned off while curl is active
|
|
45
|
+
#
|
|
46
|
+
# def connected_to_internet?
|
|
47
|
+
# script = "curl --silent --head http://www.google.com/ > /dev/null ; echo $?"
|
|
48
|
+
# result = `#{script}`.chomp
|
|
49
|
+
# puts result
|
|
50
|
+
# result == '0'
|
|
51
|
+
# end
|
|
52
|
+
|
|
53
|
+
# TODO Investigate using Curl options: --connect-timeout 1 --max-time 2 --retry 0
|
|
54
|
+
# to greatly simplify this method.
|
|
55
|
+
def connected_to_internet?
|
|
56
|
+
|
|
57
|
+
tempfile = Tempfile.open('wifi-wand-')
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
start_status_script = -> do
|
|
61
|
+
script = "curl --silent --head http://www.google.com/ > /dev/null ; echo $? > #{tempfile.path} &"
|
|
62
|
+
pid = Process.spawn(script)
|
|
63
|
+
Process.detach(pid)
|
|
64
|
+
pid
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
process_is_running = ->(pid) do
|
|
68
|
+
script = %Q{ps -p #{pid} > /dev/null; echo $?}
|
|
69
|
+
output = `#{script}`.chomp
|
|
70
|
+
output == "0"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
get_connected_state_from_curl = -> do
|
|
74
|
+
tempfile.close
|
|
75
|
+
File.read(tempfile.path).chomp == '0'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Do one run, iterating during the timeout period to see if the command has completed
|
|
79
|
+
do_one_run = -> do
|
|
80
|
+
end_time = Time.now + 3
|
|
81
|
+
pid = start_status_script.()
|
|
82
|
+
while Time.now < end_time
|
|
83
|
+
if process_is_running.(pid)
|
|
84
|
+
sleep 0.5
|
|
85
|
+
else
|
|
86
|
+
return get_connected_state_from_curl.()
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
Process.kill('KILL', pid) if process_is_running.(pid)
|
|
90
|
+
:hung
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
3.times do
|
|
94
|
+
connected = do_one_run.()
|
|
95
|
+
return connected if connected != :hung
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
raise "Could not determine Internet status."
|
|
99
|
+
|
|
100
|
+
ensure
|
|
101
|
+
tempfile.unlink
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Turns wifi off and then on, reconnecting to the originally connecting network.
|
|
108
|
+
def cycle_network
|
|
109
|
+
# TODO: Make this network name saving and restoring conditional on it not having a password.
|
|
110
|
+
# If the disabled code below is enabled, an error will be raised if a password is required,
|
|
111
|
+
# even though it is stored.
|
|
112
|
+
# network_name = current_network
|
|
113
|
+
wifi_off
|
|
114
|
+
wifi_on
|
|
115
|
+
# connect(network_name) if network_name
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def connected_to?(network_name)
|
|
120
|
+
network_name == connected_network_name
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Connects to the passed network name, optionally with password.
|
|
125
|
+
# Turns wifi on first, in case it was turned off.
|
|
126
|
+
# Relies on subclass implementation of os_level_connect().
|
|
127
|
+
def connect(network_name, password = nil)
|
|
128
|
+
# Allow symbols and anything responding to to_s for user convenience
|
|
129
|
+
network_name = network_name.to_s if network_name
|
|
130
|
+
password = password.to_s if password
|
|
131
|
+
|
|
132
|
+
if network_name.nil? || network_name.empty?
|
|
133
|
+
raise "A network name is required but was not provided."
|
|
134
|
+
end
|
|
135
|
+
wifi_on
|
|
136
|
+
os_level_connect(network_name, password)
|
|
137
|
+
|
|
138
|
+
# Verify that the network is now connected:
|
|
139
|
+
actual_network_name = connected_network_name
|
|
140
|
+
unless actual_network_name == network_name
|
|
141
|
+
message = %Q{Expected to connect to "#{network_name}" but }
|
|
142
|
+
if actual_network_name
|
|
143
|
+
message << %Q{connected to "#{connected_network_name}" instead.}
|
|
144
|
+
else
|
|
145
|
+
message << "unable to connect to any network. Did you "
|
|
146
|
+
end
|
|
147
|
+
message << (password ? "provide the correct password?" : "need to provide a password?")
|
|
148
|
+
raise message
|
|
149
|
+
end
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Removes the specified network(s) from the preferred network list.
|
|
155
|
+
# @param network_names names of networks to remove; may be empty or contain nonexistent networks
|
|
156
|
+
# @return names of the networks that were removed (excludes non-preexisting networks)
|
|
157
|
+
def remove_preferred_networks(*network_names)
|
|
158
|
+
networks_to_remove = network_names & preferred_networks # exclude any nonexistent networks
|
|
159
|
+
networks_to_remove.each { |name| remove_preferred_network(name) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def preferred_network_password(preferred_network_name)
|
|
164
|
+
preferred_network_name = preferred_network_name.to_s
|
|
165
|
+
if preferred_networks.include?(preferred_network_name)
|
|
166
|
+
os_level_preferred_network_password(preferred_network_name)
|
|
167
|
+
else
|
|
168
|
+
raise "Network #{preferred_network_name} not in preferred networks list."
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Waits for the Internet connection to be in the desired state.
|
|
174
|
+
# @param target_status must be in [:conn, :disc, :off, :on]; waits for that state
|
|
175
|
+
# @param wait_interval_in_secs sleeps this interval between retries; if nil or absent,
|
|
176
|
+
# a default will be provided
|
|
177
|
+
#
|
|
178
|
+
def till(target_status, wait_interval_in_secs = nil)
|
|
179
|
+
|
|
180
|
+
# One might ask, why not just put the 0.5 up there as the default argument.
|
|
181
|
+
# We could do that, but we'd still need the line below in case nil
|
|
182
|
+
# was explicitly specified. The default argument of nil above emphasizes that
|
|
183
|
+
# the absence of an argument and a specification of nil will behave identically.
|
|
184
|
+
wait_interval_in_secs ||= 0.5
|
|
185
|
+
|
|
186
|
+
finished_predicates = {
|
|
187
|
+
conn: -> { connected_to_internet? },
|
|
188
|
+
disc: -> { ! connected_to_internet? },
|
|
189
|
+
on: -> { wifi_on? },
|
|
190
|
+
off: -> { ! wifi_on? }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
finished_predicate = finished_predicates[target_status]
|
|
194
|
+
|
|
195
|
+
if finished_predicate.nil?
|
|
196
|
+
raise ArgumentError.new(
|
|
197
|
+
"Option must be one of #{finished_predicates.keys.inspect}. Was: #{target_status.inspect}")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
loop do
|
|
201
|
+
return if finished_predicate.()
|
|
202
|
+
sleep(wait_interval_in_secs)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Tries an OS command until the stop condition is true.
|
|
208
|
+
# @command the command to run in the OS
|
|
209
|
+
# @stop_condition a lambda taking the commands stdout as its sole parameter
|
|
210
|
+
# @return the stdout produced by the command
|
|
211
|
+
def try_os_command_until(command, stop_condition, max_tries = 100)
|
|
212
|
+
max_tries.times do
|
|
213
|
+
stdout = run_os_command(command)
|
|
214
|
+
if stop_condition.(stdout)
|
|
215
|
+
return stdout
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Reaches out to ipinfo.io to get public IP address information
|
|
223
|
+
# in the form of a hash.
|
|
224
|
+
# You may need to enclose this call in a begin/rescue.
|
|
225
|
+
def public_ip_address_info
|
|
226
|
+
JSON.parse(`curl -s ipinfo.io`)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# @return array of nameserver IP addresses from /etc/resolv.conf, or nil if not found
|
|
231
|
+
# Though this is strictly *not* OS-agnostic, it will be used by most OS's,
|
|
232
|
+
# and can be overridden by subclasses (e.g. Windows).
|
|
233
|
+
def nameservers_using_resolv_conf
|
|
234
|
+
begin
|
|
235
|
+
File.readlines('/etc/resolv.conf').grep(/^nameserver /).map { |line| line.split.last }
|
|
236
|
+
rescue Errno::ENOENT
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def nameservers_using_scutil
|
|
243
|
+
output = run_os_command('scutil --dns')
|
|
244
|
+
nameserver_lines_scoped_and_unscoped = output.split("\n").grep(/^\s*nameserver\[/)
|
|
245
|
+
unique_nameserver_lines = nameserver_lines_scoped_and_unscoped.uniq # take the union
|
|
246
|
+
nameservers = unique_nameserver_lines.map { |line| line.split(' : ').last.strip }
|
|
247
|
+
nameservers
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
require 'shellwords'
|
|
2
|
+
|
|
3
|
+
require_relative 'base_model'
|
|
4
|
+
|
|
5
|
+
module WifiWand
|
|
6
|
+
|
|
7
|
+
class MacOsModel < BaseModel
|
|
8
|
+
|
|
9
|
+
AIRPORT_CMD = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
|
|
10
|
+
|
|
11
|
+
def initialize(verbose = false)
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Identifies the (first) wireless network hardware port in the system, e.g. en0 or en1
|
|
17
|
+
def wifi_hardware_port
|
|
18
|
+
@wifi_hardware_port ||= begin
|
|
19
|
+
lines = run_os_command("networksetup -listallhardwareports").split("\n")
|
|
20
|
+
# Produces something like this:
|
|
21
|
+
# Hardware Port: Wi-Fi
|
|
22
|
+
# Device: en0
|
|
23
|
+
# Ethernet Address: ac:bc:32:b9:a9:9d
|
|
24
|
+
#
|
|
25
|
+
# Hardware Port: Bluetooth PAN
|
|
26
|
+
# Device: en3
|
|
27
|
+
# Ethernet Address: ac:bc:32:b9:a9:9e
|
|
28
|
+
wifi_port_line_num = (0...lines.size).detect do |index|
|
|
29
|
+
/: Wi-Fi$/.match(lines[index])
|
|
30
|
+
end
|
|
31
|
+
if wifi_port_line_num.nil?
|
|
32
|
+
raise %Q{Wifi port (e.g. "en0") not found in output of: networksetup -listallhardwareports}
|
|
33
|
+
else
|
|
34
|
+
lines[wifi_port_line_num + 1].split(': ').last
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Returns data pertaining to available wireless networks.
|
|
41
|
+
# For some reason, this often returns no results, so I've put the operation in a loop.
|
|
42
|
+
# I was unable to detect a sort strategy in the airport utility's output, so I sort
|
|
43
|
+
# the lines alphabetically, to show duplicates and for easier lookup.
|
|
44
|
+
def available_network_info
|
|
45
|
+
return nil unless wifi_on? # no need to try
|
|
46
|
+
command = "#{AIRPORT_CMD} -s"
|
|
47
|
+
max_attempts = 50
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
reformat_line = ->(line) do
|
|
51
|
+
ssid = line[0..31].strip
|
|
52
|
+
"%-32.32s%s" % [ssid, line[32..-1]]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
process_tabular_data = ->(output) do
|
|
57
|
+
lines = output.split("\n")
|
|
58
|
+
header_line = lines[0]
|
|
59
|
+
data_lines = lines[1..-1]
|
|
60
|
+
data_lines.map! do |line|
|
|
61
|
+
# Reformat the line so that the name is left instead of right justified
|
|
62
|
+
reformat_line.(line)
|
|
63
|
+
end
|
|
64
|
+
data_lines.sort!
|
|
65
|
+
[reformat_line.(header_line)] + data_lines
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
output = try_os_command_until(command, ->(output) do
|
|
70
|
+
! ([nil, ''].include?(output))
|
|
71
|
+
end)
|
|
72
|
+
|
|
73
|
+
if output
|
|
74
|
+
process_tabular_data.(output)
|
|
75
|
+
else
|
|
76
|
+
raise "Unable to get available network information after #{max_attempts} attempts."
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_network_names(info)
|
|
82
|
+
if info.nil?
|
|
83
|
+
nil
|
|
84
|
+
else
|
|
85
|
+
info[1..-1] \
|
|
86
|
+
.map { |line| line[0..32].rstrip } \
|
|
87
|
+
.uniq \
|
|
88
|
+
.sort { |s1, s2| s1.casecmp(s2) }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# @return an array of unique available network names only, sorted alphabetically
|
|
94
|
+
# Kludge alert: the tabular data does not differentiate between strings with and without leading whitespace
|
|
95
|
+
# Therefore, we get the data once in tabular format, and another time in XML format.
|
|
96
|
+
# The XML element will include any leading whitespace. However, it includes all <string> elements,
|
|
97
|
+
# many of which are not network names.
|
|
98
|
+
# As an improved approximation of the correct result, for each network name found in tabular mode,
|
|
99
|
+
# we look to see if there is a corresponding string element with leading whitespace, and, if so,
|
|
100
|
+
# replace it.
|
|
101
|
+
#
|
|
102
|
+
# This will not behave correctly if a given name has occurrences with different amounts of whitespace,
|
|
103
|
+
# e.g. ' x' and ' x'.
|
|
104
|
+
#
|
|
105
|
+
# The reason we don't use an XML parser to get the exactly correct result is that we don't want
|
|
106
|
+
# users to need to install any external dependencies in order to run this script.
|
|
107
|
+
def available_network_names
|
|
108
|
+
|
|
109
|
+
# Parses the XML text (using grep, not XML parsing) to find
|
|
110
|
+
# <string> elements, and extracts the network name candidates
|
|
111
|
+
# containing leading spaces from it.
|
|
112
|
+
get_leading_space_names = ->(text) do
|
|
113
|
+
text.split("\n") \
|
|
114
|
+
.grep(%r{<string>}) \
|
|
115
|
+
.sort \
|
|
116
|
+
.uniq \
|
|
117
|
+
.map { |line| line.gsub("<string>", '').gsub('</string>', '').gsub("\t", '') } \
|
|
118
|
+
.select { |s| s[0] == ' ' }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
output_is_valid = ->(output) { ! ([nil, ''].include?(output)) }
|
|
123
|
+
tabular_data = try_os_command_until("#{AIRPORT_CMD} -s", output_is_valid)
|
|
124
|
+
xml_data = try_os_command_until("#{AIRPORT_CMD} -s -x", output_is_valid)
|
|
125
|
+
|
|
126
|
+
if tabular_data.nil? || xml_data.nil?
|
|
127
|
+
raise "Unable to get available network information; please try again."
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
tabular_data_lines = tabular_data[1..-1] # omit header line
|
|
131
|
+
names_no_spaces = parse_network_names(tabular_data_lines.split("\n")).map(&:strip)
|
|
132
|
+
names_maybe_spaces = get_leading_space_names.(xml_data)
|
|
133
|
+
|
|
134
|
+
names = names_no_spaces.map do |name_no_spaces|
|
|
135
|
+
match = names_maybe_spaces.detect do |name_maybe_spaces|
|
|
136
|
+
%r{[ \t]?#{name_no_spaces}$}.match(name_maybe_spaces)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
match ? match : name_no_spaces
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
names.sort { |s1, s2| s1.casecmp(s2) } # sort alphabetically, case insensitively
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Returns data pertaining to "preferred" networks, many/most of which will probably not be available.
|
|
147
|
+
def preferred_networks
|
|
148
|
+
lines = run_os_command("networksetup -listpreferredwirelessnetworks #{wifi_hardware_port}").split("\n")
|
|
149
|
+
# Produces something like this, unsorted, and with leading tabs:
|
|
150
|
+
# Preferred networks on en0:
|
|
151
|
+
# LibraryWiFi
|
|
152
|
+
# @thePAD/Magma
|
|
153
|
+
|
|
154
|
+
lines.delete_at(0) # remove title line
|
|
155
|
+
lines.map! { |line| line.gsub("\t", '') } # remove leading tabs
|
|
156
|
+
lines.sort! { |s1, s2| s1.casecmp(s2) } # sort alphabetically, case insensitively
|
|
157
|
+
lines
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# Returns true if wifi is on, else false.
|
|
162
|
+
def wifi_on?
|
|
163
|
+
lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
|
|
164
|
+
lines.grep("AirPort: Off").none?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Turns wifi on.
|
|
169
|
+
def wifi_on
|
|
170
|
+
return if wifi_on?
|
|
171
|
+
run_os_command("networksetup -setairportpower #{wifi_hardware_port} on")
|
|
172
|
+
wifi_on? ? nil : raise("Wifi could not be enabled.")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Turns wifi off.
|
|
177
|
+
def wifi_off
|
|
178
|
+
return unless wifi_on?
|
|
179
|
+
run_os_command("networksetup -setairportpower #{wifi_hardware_port} off")
|
|
180
|
+
wifi_on? ? raise("Wifi could not be disabled.") : nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# This method is called by BaseModel#connect to do the OS-specific connection logic.
|
|
185
|
+
def os_level_connect(network_name, password = nil)
|
|
186
|
+
command = "networksetup -setairportnetwork #{wifi_hardware_port} " + "#{Shellwords.shellescape(network_name)}"
|
|
187
|
+
if password
|
|
188
|
+
command << ' ' << Shellwords.shellescape(password)
|
|
189
|
+
end
|
|
190
|
+
run_os_command(command)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# @return:
|
|
195
|
+
# If the network is in the preferred networks list
|
|
196
|
+
# If a password is associated w/this network, return the password
|
|
197
|
+
# If not, return nil
|
|
198
|
+
# else
|
|
199
|
+
# raise an error
|
|
200
|
+
def os_level_preferred_network_password(preferred_network_name)
|
|
201
|
+
command = %Q{security find-generic-password -D "AirPort network password" -a "#{preferred_network_name}" -w 2>&1}
|
|
202
|
+
begin
|
|
203
|
+
return run_os_command(command).chomp
|
|
204
|
+
rescue OsCommandError => error
|
|
205
|
+
if error.exitstatus == 44 # network has no password stored
|
|
206
|
+
nil
|
|
207
|
+
else
|
|
208
|
+
raise
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# Returns the IP address assigned to the wifi port, or nil if none.
|
|
215
|
+
def ip_address
|
|
216
|
+
begin
|
|
217
|
+
run_os_command("ipconfig getifaddr #{wifi_hardware_port}").chomp
|
|
218
|
+
rescue OsCommandError => error
|
|
219
|
+
if error.exitstatus == 1
|
|
220
|
+
nil
|
|
221
|
+
else
|
|
222
|
+
raise
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def remove_preferred_network(network_name)
|
|
229
|
+
network_name = network_name.to_s
|
|
230
|
+
run_os_command("sudo networksetup -removepreferredwirelessnetwork " +
|
|
231
|
+
"#{wifi_hardware_port} #{Shellwords.shellescape(network_name)}")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# Returns the network currently connected to, or nil if none.
|
|
236
|
+
def current_network
|
|
237
|
+
lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
|
|
238
|
+
ssid_lines = lines.grep(/ SSID:/)
|
|
239
|
+
ssid_lines.empty? ? nil : ssid_lines.first.split('SSID: ').last.strip
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# Disconnects from the currently connected network. Does not turn off wifi.
|
|
244
|
+
def disconnect
|
|
245
|
+
run_os_command("sudo #{AIRPORT_CMD} -z")
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# Returns some useful wifi-related information.
|
|
251
|
+
def wifi_info
|
|
252
|
+
|
|
253
|
+
info = {
|
|
254
|
+
'wifi_on' => wifi_on?,
|
|
255
|
+
'internet_on' => connected_to_internet?,
|
|
256
|
+
'port' => wifi_hardware_port,
|
|
257
|
+
'network' => current_network,
|
|
258
|
+
'ip_address' => ip_address,
|
|
259
|
+
'nameservers' => nameservers_using_scutil,
|
|
260
|
+
'timestamp' => Time.now,
|
|
261
|
+
}
|
|
262
|
+
more_output = run_os_command(AIRPORT_CMD + " -I")
|
|
263
|
+
more_info = colon_output_to_hash(more_output)
|
|
264
|
+
info.merge!(more_info)
|
|
265
|
+
info.delete('AirPort') # will be here if off, but info is already in wifi_on key
|
|
266
|
+
|
|
267
|
+
if info['wifi_on']
|
|
268
|
+
begin
|
|
269
|
+
info['public_ip'] = public_ip_address_info
|
|
270
|
+
rescue => e
|
|
271
|
+
puts "Error obtaining public IP address info, proceeding with everything else:"
|
|
272
|
+
puts e.to_s
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
info
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def open_resource(resource_url)
|
|
280
|
+
run_os_command('open ' + resource_url)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Parses output like the text below into a hash:
|
|
285
|
+
# SSID: Pattara211
|
|
286
|
+
# MCS: 5
|
|
287
|
+
# channel: 7
|
|
288
|
+
def colon_output_to_hash(output)
|
|
289
|
+
lines = output.split("\n")
|
|
290
|
+
lines.each_with_object({}) do |line, new_hash|
|
|
291
|
+
key, value = line.split(': ')
|
|
292
|
+
key = key.strip
|
|
293
|
+
value.strip! if value
|
|
294
|
+
new_hash[key] = value
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
private :colon_output_to_hash
|
|
298
|
+
end
|
|
299
|
+
end
|