mac-wifi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5763b7c0bc11d558c50e82e2e7fd52ba6bd5e3cb
4
+ data.tar.gz: 29878b297569049bff109647d9583ad6afad5a65
5
+ SHA512:
6
+ metadata.gz: 5c70472f1d31785cae9a124ef5641f1157eb63549267c97773f120e4ce033588faad198bfded7f56ddb8e2eb8fbbeaae0d1ed31e20ddceaf5d0c7bd00140f8a6
7
+ data.tar.gz: 32007be9ff30e8fd38996ba433386a29ee11193c868d1b37afb1bc534e11c93173c9658a116bbc66d95692a26809b8864d7a78f1e8f1d28426e3c393ba223e9b
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea/
19
+
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in mac-wifi-2.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2017 Keith Bennett
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # mac-wifi
2
+
3
+ This script enables the query and management of wifi configuration and environment on a Mac.
4
+
5
+ It can be run in single-command or interactive mode. Interactive mode uses the `pry` gem,
6
+ providing an interface familiar to Rubyists and other
7
+ [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) users.
8
+
9
+ It is not necessary to download this repo; this script file is all you need to run the application.
10
+
11
+ ### Usage
12
+
13
+ Available commands can be seen by using the `h` (or `help`) option. Here is its
14
+ output at the time of this writing:
15
+
16
+ ```
17
+ ➜ mac-wifi git:(master) ✗  ./mac-wifi h
18
+
19
+ Available commands are:
20
+
21
+ ci - connected to Internet (not just wifi on)?
22
+ co[nnect] network-name - turns wifi on, connects to network-name
23
+ cy[cle] - turns wifi off, then on, preserving network selection
24
+ d[isconnect] - disconnects from current network, does not turn off wifi
25
+ h[elp] - prints this help
26
+ i[nfo] - prints wifi-related information
27
+ lsp[referred] - lists preferred (not necessarily available) networks
28
+ lsa[vailable] - lists available networks
29
+ n[etwork_name] - name (SSID) of currently connected network
30
+ on - turns wifi on
31
+ of[f] - turns wifi off
32
+ pa[ssword] network-name - shows password for preferred network-name
33
+ q[uit] - exits this program (interactive shell mode only)
34
+ r[m] network-name - removes network-name from the preferred networks list
35
+ s[hell] - opens an interactive pry shell (command line only)
36
+ t[ill] - (experimental!) returns when the desired Internet connection state is true. Options:
37
+ 'on'/:on or 'off'/:off
38
+ wait interval, in seconds (optional, defaults to 0.5 seconds)
39
+ w[ifion] - is the wifi on?
40
+ x[it] - exits this program (interactive shell mode only)
41
+
42
+ When in interactive shell mode:
43
+ * use quotes for string parameters such as method names.
44
+ * for pry commands, use prefix `%`.
45
+ ```
46
+
47
+ Internally, it uses several Mac command line utilities. This is not ideal,
48
+ I would have preferred OS system calls, but the current approach enabled me to develop
49
+ this script quickly and simply.
50
+
51
+ ### Pretty Output
52
+
53
+ For nicely formatted output of the `info` command in non-interactive mode,
54
+ the `awesome_print` gem is used if it is installed;
55
+ otherwise, the somewhat less awesome pretty print (`pp`) is used. Therefore,
56
+ installation of the `awesome_print` gem is recommended.
57
+ This is accomplished by the following command:
58
+
59
+ `gem install awesome_print`
60
+
61
+
62
+ ### Seeing the Underlying OS Commands and Output
63
+
64
+ If you would like to see the Mac OS commands and their output, you can do so by setting the
65
+ environment variable MAC_WIFI_OPTS to include `-v` (for _verbose_).
66
+ This can be done in the following ways:
67
+
68
+ ```
69
+ export MAC_WIFI_OPTS=-v
70
+ ./mac-wifi i
71
+ ```
72
+
73
+ ```
74
+ MAC_WIFI_OPTS=-v ./mac-wifi i
75
+ ```
76
+
77
+ You may notice that some commands are executed more than once. This is to simplify the application logic
78
+ and eliminate the need for the complexity of balancing the speed that a cache offers and the risk
79
+ of stale data.
80
+
81
+
82
+ ### Troubleshooting
83
+
84
+ If you try to run the shell, the script will require the `pry` gem, so that will need to be installed.
85
+ `pry` in turn requires access to a `readline` library. If you encounter an error relating to finding a
86
+ `readline` library, this can be fixed by installing the `pry-coolline` gem: `gem install pry-coolline`.
87
+ If you are using the Ruby packaged with Mac OS, or for some other reason require root access to install
88
+ gems, you will need to precede those commands with `sudo`:
89
+
90
+ ```
91
+ sudo gem install pry
92
+ sudo gem install pry-coolline
93
+ ```
94
+
95
+
96
+ ### Using the Shell
97
+
98
+ The shell, invoked with the `s` command on the command line, provides an interactive
99
+ session. It can be useful when:
100
+
101
+ * you want to issue multiple commands
102
+ * you want to combine commands
103
+ * you want the data in a format not provided by this application
104
+ * you want to incorporate these commands into other Ruby code interactively
105
+ * you want to combine the results of commands with other OS commands
106
+ (you can shell out to run other command line programs by preceding the command with a period (`.`).
107
+
108
+ ### Using Variables in the Shell
109
+
110
+ There are a couple of things (that may be surprising) to keep in mind
111
+ when using the shell. They relate to the fact that local variables
112
+ and method calls use the same notation in Ruby (use of parentheses
113
+ in a method call is optional):
114
+
115
+ 1) In Ruby, when both a method and a local variable have the same name,
116
+ the local variable will override the method name. Therefore, local variables
117
+ may override this app's commands. For example:
118
+
119
+ ```
120
+ [1] pry(#<MacWifiView>)> n # network_name command
121
+ => ".@ AIS SUPER WiFi"
122
+ [2] pry(#<MacWifiView>)> n = 123 # override it with a local variable
123
+ => 123
124
+ [3] pry(#<MacWifiView>)> n # 'n' no longer calls the method
125
+ => 123
126
+ [4] pry(#<MacWifiView>)> ne # but any other name starting with 'ne' will still call the method
127
+ => ".@ AIS SUPER WiFi"
128
+ [5] pry(#<MacWifiView>)> network_name
129
+ => ".@ AIS SUPER WiFi"
130
+ [6] pry(#<MacWifiView>)> ne_xzy123
131
+ => ".@ AIS SUPER WiFi"
132
+ ```
133
+
134
+ If you don't want to deal with this, you could use global variables, instance variables,
135
+ or constants, which will _not_ hide the methods:
136
+
137
+ ```
138
+ [7] pry(#<MacWifiView>)> N = 123
139
+ [8] pry(#<MacWifiView>)> @n = 456
140
+ [9] pry(#<MacWifiView>)> $n = 789
141
+ [10] pry(#<MacWifiView>)> puts n, N, @n, $n
142
+ .@ AIS SUPER WiFi
143
+ 123
144
+ 456
145
+ 789
146
+ => nil
147
+ ```
148
+
149
+ 2) If you accidentally refer to a nonexistent variable or method name,
150
+ the result may be mysterious. For example, if I were write the wifi information
151
+ to a file, this would work:
152
+
153
+
154
+ ```
155
+ [1] pry(#<MacWifiView>)> File.write('x.txt', info)
156
+ => 431
157
+ ```
158
+
159
+ However, if I forget to quote the filename:
160
+
161
+ ```
162
+ [2] pry(#<MacWifiView>)> File.write(x.txt, info)
163
+ ➜ mac-wifi git:(master) ✗ 
164
+ ```
165
+
166
+ What happened? `x.txt` was assumed by Ruby to be a method name.
167
+ `method_missing` was called, and since `x.txt` starts with `x`,
168
+ the exit method was called, exiting the program.
169
+
170
+ Bottom line is, be careful to quote your strings, and you're probably better off using
171
+ constants or instance variables if you want to create variables in your shell.
172
+
173
+
174
+
175
+ ### Examples
176
+
177
+ #### Single Command Invocations
178
+
179
+ ```
180
+ mac-wifi i # prints out wifi info
181
+ mac-wifi lsa # prints available networks
182
+ mac-wifi lsp # prints preferred networks
183
+ mac-wifi cy # cycles the wifi off and on
184
+ mac-wifi co a-network a-password # connects to a network requiring a password
185
+ mac-wifi co a-network # connects to a network _not_ requiring a password
186
+ mac-wifi t on && say "Internet connected" # Play audible message when Internet becomes connected
187
+ ```
188
+
189
+ #### Interactive Shell Commands
190
+
191
+ (For brevity, semicolons are used here to put multiple commands on one line,
192
+ but these commands could also each be specified on a line of its own.)
193
+
194
+ ```
195
+ # Print out wifi info:
196
+ i
197
+
198
+ # Cycle (off/on) the network then connect to the specified network not requiring a password
199
+ > cycle; connect 'my-network'
200
+
201
+ # Cycle (off/on) the network, then connect to the same network not requiring a password
202
+ > @name = network_name; cycle; connect @name
203
+
204
+ # Cycle (off/on) the network then connect to the specified network using the specified password
205
+ > cycle; connect 'my-network', 'my-password'
206
+
207
+ > @i = i; puts "You are connected on port #{@i[:port]} to #{@i[:network]} on IP address #{@i[:ip_address]}."
208
+ You are connected on port en0 to .@ AIS SUPER WiFi on IP address 172.27.145.225.
209
+
210
+ > puts "There are #{lsp.size} preferred networks."
211
+ There are 341 preferred networks.
212
+
213
+ # Delete all preferred networks whose names begin with "TOTTGUEST":
214
+ > lsp.grep(/^TOTTGUEST/).each { |n| puts "Deleting preferred network #{n}."; rm(n) }
215
+
216
+ # Define a method to wait for the Internet connection to be active.
217
+ # (This functionality is included in the `till` command.)
218
+ # Call it, then output celebration message:
219
+ [17] pry(#<MacWifiView>)> def wait_for_internet; loop do; break if ci; sleep 0.5; end; end
220
+ [18] pry(#<MacWifiView>)> wait_for_internet; puts "Connected!"
221
+ Connected!
222
+
223
+ # Same, but using a lambda instead of a method so we can use a variable name
224
+ # and not need to worry about method name collision:
225
+ @wait_for_internet = -> { loop do; break if ci; sleep 0.5; end }
226
+ @wait_for_internet.() ; puts "Connected!"
227
+ Connected!
228
+ ```
229
+
230
+ ### Cautions
231
+
232
+ If the wifi networking changes from on to off while `till(:off)` is waiting,
233
+ the program will hang indefinitely. I am currently looking into this problem.
234
+ It also happens outside of this program; you can reproduce it by running `curl`,
235
+ preferably on a longer running request, then turning off your wifi. This is pretty
236
+ serious, and I'm considering removing the `:off` option from the `till` command.
237
+
238
+
239
+
240
+ ### Password Lookup Oddity
241
+
242
+ You may find it odd (I did, anyway) that even if you issue the password command
243
+ (`mac_wifi pa a-network-name`) using sudo, you will still be prompted
244
+ with a graphical dialog for both a user id and password. This is no doubt
245
+ for better security, but it's unfortunate in that it makes it impossible to fully automate this task.
246
+
247
+ In particular, it would be nice for the `cycle` command to be able to reconnect to the original
248
+ network after turning the network on. This is not possible where that network required a password.
249
+ If you don't mind storing the network password in plain text somewhere, then you could easily
250
+ automate it (e.g. `mac-wifi cycle && mac-wifi connect a-network a-password`).
251
+
252
+
253
+ ### License
254
+
255
+ MIT License (see LICENSE.txt)
256
+
257
+ ### Shameless Ad
258
+
259
+ I am available for consulting, development, tutoring, training, troubleshooting, etc.
260
+
261
+ You can contact me via GMail, Twitter, and Github as _keithrbennett_.
data/bin/mac-wifi ADDED
@@ -0,0 +1,771 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script brings together several useful wifi-related functions.
4
+ #
5
+ # It is a bit of a kludge in that it calls Mac OS commands and uses
6
+ # the text output for its data. At some point I would like to replace
7
+ # that with system calls. Currently this script can break if Apple
8
+ # decides to modify the name, options, behavior, and/or format of its utilities.
9
+ #
10
+ # What would be *really* nice, would be for Apple to retrofit all
11
+ # system commands to optionally output JSON and/or YAML. Some offer XML, but that
12
+ # is not convenient to use.
13
+ #
14
+ # Mac OS commands currently used are: airport, ipconfig, networksetup, security.
15
+ #
16
+ # Author: keithrbennett (on Github, GMail, Twitter)
17
+ # I am available for Ruby development, troubleshooting, training, tutoring, etc.
18
+ #
19
+ # License: MIT License
20
+
21
+
22
+
23
+ require 'shellwords'
24
+ require 'tempfile'
25
+
26
+ module MacWifi
27
+
28
+ # This version must be kept in sync with the version in the gemspec file.
29
+ VERSION = '0.0.1'
30
+
31
+ class Model
32
+
33
+ AIRPORT_CMD = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
34
+
35
+
36
+ def initialize(verbose = false)
37
+ @verbose_mode = verbose
38
+ end
39
+
40
+
41
+ def connected_to_internet_curl_unused?
42
+
43
+ connected = false
44
+
45
+ begin
46
+ run_os_command('curl --silent --head http://www.google.com/')
47
+ connected = true
48
+ rescue OsCommandError => e
49
+ if e.exitstatus == 6
50
+ connected = false
51
+ else
52
+ raise
53
+ end
54
+ end
55
+ connected
56
+ end
57
+
58
+
59
+ # This method returns whether or not there is a working Internet connection.
60
+ # Because of a Mac issue which causes a request to hang if the network is turned
61
+ # off during its lifetime, we give it only 5 seconds per try,
62
+ # and limit the number of tries to 3.
63
+ def connected_to_internet?
64
+
65
+ tempfile = Tempfile.open('mac-wifi-')
66
+
67
+ begin
68
+ start_status_script = -> do
69
+ script = "curl --silent --head http://www.google.com/ > /dev/null ; echo $? > #{tempfile.path} &"
70
+ pid = Process.spawn(script)
71
+ Process.detach(pid)
72
+ pid
73
+ end
74
+
75
+ process_is_running = ->(pid) do
76
+ script = %Q{ps -p #{pid} > /dev/null; echo $?}
77
+ output = `#{script}`.chomp
78
+ output == "0"
79
+ end
80
+
81
+ get_connected_state_from_curl = -> do
82
+ tempfile.close
83
+ File.read(tempfile.path).chomp == '0'
84
+ end
85
+
86
+ # Do one run, iterating during the timeout period to see if the command has completed
87
+ do_one_run = -> do
88
+ end_time = Time.now + 3
89
+ pid = start_status_script.()
90
+ while Time.now < end_time
91
+ if process_is_running.(pid)
92
+ sleep 0.5
93
+ else
94
+ return get_connected_state_from_curl.()
95
+ end
96
+ end
97
+ Process.kill('KILL', pid)
98
+ :hung
99
+ end
100
+
101
+ 3.times do
102
+ connected = do_one_run.()
103
+ return connected if connected != :hung
104
+ end
105
+
106
+ raise "Could not determine Internet status."
107
+
108
+ ensure
109
+ tempfile.unlink
110
+ end
111
+
112
+ end
113
+
114
+ # This implementation often hangs when wifi is turned off while curl is active
115
+ # def connected_to_internet?
116
+ # script = "curl --silent --head http://www.google.com/ > /dev/null ; echo $?"
117
+ # result = `#{script}`.chomp
118
+ # puts result
119
+ # result == '0'
120
+ # end
121
+
122
+
123
+ # This is determined by whether or not a line like the following appears in the output of `netstat -nr`:
124
+ # 0/1 10.137.0.41 UGSc 15 0 utun1
125
+ def vpn_running?
126
+ run_os_command('netstat -nr').split("\n").grep(/^0\/1.*utun1/).any?
127
+ end
128
+
129
+
130
+ # Identifies the (first) wireless network hardware port in the system, e.g. en0 or en1
131
+ def wifi_hardware_port
132
+ @wifi_hardware_port ||= begin
133
+ lines = run_os_command("networksetup -listallhardwareports").split("\n")
134
+ # Produces something like this:
135
+ # Hardware Port: Wi-Fi
136
+ # Device: en0
137
+ # Ethernet Address: ac:bc:32:b9:a9:9d
138
+ #
139
+ # Hardware Port: Bluetooth PAN
140
+ # Device: en3
141
+ # Ethernet Address: ac:bc:32:b9:a9:9e
142
+ wifi_port_line_num = (0...lines.size).detect do |index|
143
+ /: Wi-Fi$/.match(lines[index])
144
+ end
145
+ if wifi_port_line_num.nil?
146
+ raise %Q{Wifi port (e.g. "en0") not found in output of: networksetup -listallhardwareports}
147
+ else
148
+ lines[wifi_port_line_num + 1].split(': ').last
149
+ end
150
+ end
151
+ end
152
+
153
+
154
+ # Returns data pertaining to available wireless networks.
155
+ # For some reason, this often returns no results, so I've put the operation in a loop.
156
+ def available_network_info
157
+ return nil unless wifi_on? # no need to try
158
+ command = "#{AIRPORT_CMD} -s"
159
+ max_attempts = 50
160
+ max_attempts.times do
161
+ output = run_os_command(command)
162
+ if output.size > 0
163
+ return output.split("\n")
164
+ end
165
+ end
166
+ raise "Unable to get available network information after #{max_attempts} attempts."
167
+ end
168
+
169
+
170
+ # Returns data pertaining to "preferred" networks, many/most of which will probably not be available.
171
+ def preferred_networks
172
+ lines = run_os_command("networksetup -listpreferredwirelessnetworks #{wifi_hardware_port}").split("\n")
173
+ # Produces something like this, unsorted, and with leading tabs:
174
+ # Preferred networks on en0:
175
+ # LibraryWiFi
176
+ # @thePAD/Magma
177
+
178
+ lines.delete_at(0) # remove title line
179
+ lines.map! { |line| line.gsub("\t", '') } # remove leading tabs
180
+ lines.sort! { |s1, s2| s1.casecmp(s2) } # sort alphabetically, case insensitively
181
+ lines
182
+ end
183
+
184
+
185
+ # Returns true if wifi is on, else false.
186
+ def wifi_on?
187
+ lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
188
+ ! lines.grep("AirPort: Off").any?
189
+ end
190
+
191
+
192
+ # Turns wifi on.
193
+ def wifi_on
194
+ return if wifi_on?
195
+ run_os_command("networksetup -setairportpower #{wifi_hardware_port} on")
196
+ wifi_on? ? nil : raise("Wifi could not be enabled.")
197
+ end
198
+
199
+
200
+ # Turns wifi off.
201
+ def wifi_off
202
+ return unless wifi_on?
203
+ run_os_command("networksetup -setairportpower #{wifi_hardware_port} off")
204
+ wifi_on? ? raise("Wifi could not be disabled.") : nil
205
+ end
206
+
207
+
208
+ # Turns wifi off and then on, reconnecting to the originally connecting network.
209
+ def cycle_network
210
+ # TODO: Make this network name saving and restoring conditional on it not having a password.
211
+ # If the disabled code below is enabled, an error will be raised if a password is required,
212
+ # even though it is stored.
213
+ # network_name = current_network
214
+ wifi_off
215
+ wifi_on
216
+ # connect(network_name) if network_name
217
+ end
218
+
219
+
220
+ def connected_network_name
221
+ wifi_info['SSID']
222
+ end
223
+
224
+
225
+ def connected_to?(network_name)
226
+ network_name == connected_network_name
227
+ end
228
+
229
+
230
+ # Connects to the passed network name, optionally with password.
231
+ # Turns wifi on first, in case it was turned off.
232
+ def connect(network_name, password = nil)
233
+ if network_name.nil? || network_name.empty?
234
+ raise "A network name is required but was not provided."
235
+ end
236
+ wifi_on
237
+ command = "networksetup -setairportnetwork #{wifi_hardware_port} " + "#{Shellwords.shellescape(network_name)}"
238
+ if password
239
+ command << ' ' << Shellwords.shellescape(password)
240
+ end
241
+ run_os_command(command)
242
+
243
+ # Verify that the network is now connected:
244
+ actual_network_name = connected_network_name
245
+ unless actual_network_name == network_name
246
+ message = %Q{Expected to connect to "#{network_name}" but }
247
+ if actual_network_name
248
+ message << %Q{connected to "#{connected_network_name}" instead.}
249
+ else
250
+ message << "unable to connect to any network. Did you "
251
+ end
252
+ message << (password ? "provide the correct password?" : "need to provide a password?")
253
+ raise message
254
+ end
255
+ nil
256
+ end
257
+
258
+
259
+ # @return:
260
+ # If the network is in the preferred networks list
261
+ # If a password is associated w/this network, return the password
262
+ # If not, return nil
263
+ # else
264
+ # raise an error
265
+ def preferred_network_password(preferred_network_name)
266
+ if preferred_networks.include?(preferred_network_name)
267
+ command = %Q{security find-generic-password -D "AirPort network password" -a "#{preferred_network_name}" -w 2>&1}
268
+ begin
269
+ return run_os_command(command).chomp
270
+ rescue OsCommandError => error
271
+ if error.exitstatus == 44 # network has no password stored
272
+ nil
273
+ else
274
+ raise
275
+ end
276
+ end
277
+ else
278
+ raise "Network #{preferred_network_name} not in preferred networks list."
279
+ end
280
+ end
281
+
282
+
283
+ # Returns the IP address assigned to the wifi port, or nil if none.
284
+ def ip_address
285
+ begin
286
+ run_os_command("ipconfig getifaddr #{wifi_hardware_port}").chomp
287
+ rescue OsCommandError => error
288
+ if error.exitstatus == 1
289
+ nil
290
+ else
291
+ raise
292
+ end
293
+ end
294
+ end
295
+
296
+
297
+ # Removes the specified network(s) from the preferred network list.
298
+ # @param network_names names of networks to remove; may be empty or contain nonexistent networks
299
+ # @return names of the networks that were removed (excludes non-preexisting networks)
300
+ def remove_preferred_networks(*network_names)
301
+ networks_to_remove = network_names & preferred_networks # exclude any nonexistent networks
302
+ networks_to_remove.each do |name|
303
+ run_os_command("sudo networksetup -removepreferredwirelessnetwork " +
304
+ "#{wifi_hardware_port} #{Shellwords.shellescape(name)}")
305
+ end
306
+ networks_to_remove
307
+ end
308
+
309
+
310
+ # Returns the network currently connected to, or nil if none.
311
+ def current_network
312
+ lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
313
+ ssid_lines = lines.grep(/ SSID:/)
314
+ ssid_lines.empty? ? nil : ssid_lines.first.split('SSID: ').last.strip
315
+ end
316
+
317
+
318
+ # Disconnects from the currently connected network. Does not turn off wifi.
319
+ def disconnect
320
+ run_os_command("sudo #{AIRPORT_CMD} -z")
321
+ nil
322
+ end
323
+
324
+
325
+ # Returns some useful wifi-related information.
326
+ def wifi_info
327
+
328
+ info = {
329
+ wifi_on: wifi_on?,
330
+ internet_on: connected_to_internet?,
331
+ vpn_on: vpn_running?,
332
+ port: wifi_hardware_port,
333
+ network: current_network,
334
+ ip_address: ip_address,
335
+ timestamp: Time.now,
336
+ }
337
+ more_output = run_os_command(AIRPORT_CMD + " -I")
338
+ more_info = colon_output_to_hash(more_output)
339
+ info.merge!(more_info)
340
+ info.delete('AirPort') # will be here if off, but info is already in wifi_on key
341
+ info
342
+ end
343
+
344
+
345
+ # Waits for the Internet connection to be in the desired state.
346
+ # @param status must be in [:conn, :disc, :off, :on]; waits for that state
347
+ # @param wait_interval_in_secs sleeps this interval between retries
348
+ #
349
+ # NOTE!: This is experimental and sometimes hangs.
350
+ def till(status, wait_interval_in_secs = nil)
351
+
352
+ wait_interval_in_secs ||= 0.5
353
+
354
+ exit_when = case status
355
+ when :conn
356
+ -> { connected_to_internet? }
357
+ when :disc
358
+ -> { ! connected_to_internet? }
359
+ when :on
360
+ -> { wifi_on? }
361
+ when :off
362
+ -> { ! wifi_on? }
363
+ else
364
+ raise ArgumentError.new("Option must be one of [:conn, :disc, :off, :on]. Was: #{status.inspect}")
365
+ end
366
+
367
+ loop do
368
+ return if exit_when.()
369
+ sleep(wait_interval_in_secs)
370
+ end
371
+ end
372
+
373
+
374
+ class OsCommandError < RuntimeError
375
+ attr_reader :exitstatus, :command, :text
376
+
377
+ def initialize(exitstatus, command, text)
378
+ @exitstatus = exitstatus
379
+ @command = command
380
+ @text = text
381
+ end
382
+ end
383
+
384
+
385
+ def run_os_command(command)
386
+ output = `#{command} 2>&1` # join stderr with stdout
387
+ if $?.exitstatus != 0
388
+ raise OsCommandError.new($?.exitstatus, command, output)
389
+ end
390
+ if @verbose_mode
391
+ puts "\n\n#{'-' * 79}\nCommand: #{command}\n\nOutput:\n#{output}#{'-' * 79}\n\n"
392
+ end
393
+ output
394
+ end
395
+ private :run_os_command
396
+
397
+
398
+ # Parses output like the text below into a hash:
399
+ # SSID: Pattara211
400
+ # MCS: 5
401
+ # channel: 7
402
+ def colon_output_to_hash(output)
403
+ lines = output.split("\n")
404
+ lines.each_with_object({}) do |line, new_hash|
405
+ key, value = line.split(': ')
406
+ key = key.strip
407
+ value.strip! if value
408
+ new_hash[key] = value
409
+ end
410
+ end
411
+ private :colon_output_to_hash
412
+ end
413
+
414
+
415
+ class CommandLineInterface
416
+
417
+ attr_reader :model
418
+
419
+ # Help text to be used when requested by 'h' command, in case of unrecognized or nonexistent command, etc.
420
+ HELP_TEXT = "
421
+ mac-wifi version #{VERSION} -- Available commands are:
422
+
423
+ ci - connected to Internet (not just wifi on)?
424
+ co[nnect] network-name - turns wifi on, connects to network-name
425
+ cy[cle] - turns wifi off, then on, preserving network selection
426
+ d[isconnect] - disconnects from current network, does not turn off wifi
427
+ h[elp] - prints this help
428
+ i[nfo] - prints wifi-related information
429
+ lsp[referred] - lists preferred (not necessarily available) networks
430
+ lsa[vailable] - lists available networks
431
+ n[etwork_name] - name (SSID) of currently connected network
432
+ on - turns wifi on
433
+ of[f] - turns wifi off
434
+ pa[ssword] network-name - shows password for preferred network-name
435
+ q[uit] - exits this program (interactive shell mode only)
436
+ r[m] network-name - removes network-name from the preferred networks list
437
+ s[hell] - opens an interactive pry shell (command line only)
438
+ t[ill] - (experimental!) returns when the desired Internet connection state is true. Options:
439
+ 'on'/:on or 'off'/:off
440
+ wait interval, in seconds (optional, defaults to 0.5 seconds)
441
+ w[ifion] - is the wifi on?
442
+ x[it] - exits this program (interactive shell mode only)
443
+
444
+ When in interactive shell mode:
445
+ * use quotes for string parameters such as method names.
446
+ * for pry commands, use prefix `%`.
447
+
448
+ "
449
+
450
+
451
+ def initialize
452
+ @model = Model.new(verbose_mode)
453
+ @interactive_mode = false # will be true if/when user selects shell option on command line
454
+ end
455
+
456
+
457
+ class Command < Struct.new(:regex, :action); end
458
+
459
+
460
+ class BadCommandError < RuntimeError
461
+ def initialize(error_message)
462
+ super
463
+ end
464
+ end
465
+
466
+
467
+ def verbose_mode
468
+ /-v/.match(ENV['MAC_WIFI_OPTS'])
469
+ end
470
+
471
+
472
+ def print_help
473
+ puts HELP_TEXT
474
+ end
475
+
476
+
477
+ # We'd like to use awesome_print if it is available, but not require it.
478
+ # So, we try to require it, but if that fails, we fall back to using pp (pretty print).
479
+ # Returns true if awesome_print is available (after requiring it), else false after requiring 'pp'.
480
+ def awesome_print_available?
481
+ if @awesome_print_available.nil? # first time here
482
+ begin
483
+ require 'awesome_print'
484
+ @awesome_print_available = true
485
+ rescue LoadError
486
+ require 'pp'
487
+ @awesome_print_available = false
488
+ end
489
+ end
490
+
491
+ @awesome_print_available
492
+ end
493
+
494
+
495
+ def fancy_puts(object)
496
+ awesome_print_available? ? ap(object) : pp(object)
497
+ end
498
+
499
+
500
+ # Asserts that a command has been passed on the command line.
501
+ def validate_command_line
502
+ if ARGV.empty?
503
+ puts "Syntax is: #{__FILE__} command [options]"
504
+ print_help
505
+ exit(-1)
506
+ end
507
+ end
508
+
509
+
510
+ # Pry will output the content of the method from which it was called.
511
+ # This small method exists solely to reduce the amount of pry's output
512
+ # that is not needed here.
513
+ def run_pry
514
+ binding.pry
515
+ end
516
+
517
+
518
+ # Runs a pry session in the context of this object.
519
+ # Commands and options specified on the command line can also be specified in the shell.
520
+ def run_shell
521
+ if @interactive_mode
522
+ puts "Already in shell."
523
+ return
524
+ end
525
+
526
+ @interactive_mode = true
527
+
528
+ begin
529
+ require 'pry'
530
+ rescue LoadError
531
+ puts "The 'pry' gem, required for running the shell, was not found. Please `gem install pry`."
532
+ exit(-1)
533
+ end
534
+
535
+ print_help
536
+
537
+ # Enable the line below if you have any problems with pry configuration being loaded
538
+ # that is messing up this runtime use of pry:
539
+ # Pry.config.should_load_rc = false
540
+
541
+ # Strangely, this is the only thing I have found that successfully suppresses the
542
+ # code context output, which is not useful here. Anyway, this will differentiate
543
+ # a pry command from a DSL command, which _is_ useful here.
544
+ Pry.config.command_prefix = '%'
545
+
546
+ run_pry
547
+ end
548
+
549
+
550
+ def output_network_password(network_name, password)
551
+ output = %Q{Preferred network "#{network_name}" }
552
+ output << (password ? %Q{stored password is: "#{password}".} : "has no stored password.")
553
+ puts output
554
+ password
555
+ end
556
+
557
+
558
+ # For use by the shell; when typing a command and options, it is passed to process_command_line
559
+ def method_missing(method_name, *options)
560
+ method_valid = !! find_command_action(method_name.to_s)
561
+ if method_valid
562
+ process_command_line(method_name, options)
563
+ else
564
+ puts(%Q{"#{method_name}" is not a valid command or option. If you intend for this to be a string literal, use quotes.})
565
+ end
566
+ end
567
+
568
+
569
+ # Processes the command (ARGV[0]) and any relevant options (ARGV[1..-1]).
570
+ #
571
+ # CAUTION! In interactive mode, any strings entered (e.g. a network name) MUST
572
+ # be in a form that Ruby will recognize as a string, i.e. single or double quotes,
573
+ # %q, %Q, etc. Otherwise Ruby will assume it's a method name and pass it to
574
+ # method_missing!
575
+ def process_command_line(command, options)
576
+ action = find_command_action(command)
577
+ if action
578
+ action.(*options)
579
+ else
580
+ print_help
581
+ raise BadCommandError.new(
582
+ "Unrecognized command. Command was #{action} and options were #{options.inspect}.")
583
+ end
584
+ end
585
+
586
+
587
+ def quit
588
+ if @interactive_mode
589
+ exit(0)
590
+ else
591
+ puts "This command can only be run in shell mode."
592
+ end
593
+ end
594
+
595
+
596
+ def cmd_ci
597
+ connected = model.connected_to_internet?
598
+ puts "Connected to Internet: #{connected}" unless @interactive_mode
599
+ connected
600
+ end
601
+
602
+
603
+ def cmd_co(network, password = nil)
604
+ model.connect(network, password)
605
+ end
606
+
607
+
608
+ def cmd_cy
609
+ model.cycle_network
610
+ end
611
+
612
+
613
+ def cmd_d
614
+ model.disconnect
615
+ end
616
+
617
+
618
+ def cmd_h
619
+ print_help
620
+ end
621
+
622
+
623
+ def cmd_i
624
+ info = model.wifi_info
625
+ fancy_puts(info) unless @interactive_mode
626
+ info
627
+ end
628
+
629
+
630
+ def cmd_lsa
631
+ info = model.available_network_info
632
+ puts info unless @interactive_mode
633
+ info
634
+ end
635
+
636
+
637
+ def cmd_lsp
638
+ networks = model.preferred_networks
639
+ puts networks unless @interactive_mode
640
+ networks
641
+ end
642
+
643
+
644
+ def cmd_n
645
+ name = model.connected_network_name
646
+ unless @interactive_mode
647
+ puts "Network (SSID) name: #{name ? name : '[none]'}"
648
+ end
649
+ name
650
+ end
651
+
652
+
653
+ def cmd_of
654
+ model.wifi_off
655
+ end
656
+
657
+
658
+ def cmd_on
659
+ model.wifi_on
660
+ end
661
+
662
+
663
+ def cmd_pa(network)
664
+ password = model.preferred_network_password(network)
665
+ output_network_password(network, password) unless @interactive_mode
666
+ password
667
+ end
668
+
669
+
670
+ def cmd_q
671
+ quit
672
+ end
673
+
674
+
675
+ def cmd_r(*options)
676
+ model.remove_preferred_networks(*options)
677
+ end
678
+
679
+
680
+ def cmd_s
681
+ run_shell
682
+ end
683
+
684
+
685
+ def cmd_t(*options)
686
+ target_status = options[0].to_sym
687
+ wait_interval_in_secs = (options[1] ? Float(options[1]) : nil)
688
+ model.till(target_status, wait_interval_in_secs)
689
+ end
690
+
691
+
692
+ def cmd_w
693
+ on = model.wifi_on?
694
+ puts "Wifi on?: #{on}" unless @interactive_mode
695
+ on
696
+ end
697
+
698
+
699
+ def cmd_x
700
+ quit
701
+ end
702
+
703
+
704
+ def commands
705
+ @commands_ ||= [
706
+ Command.new(/^ci/, -> (*_options) { cmd_ci }),
707
+ Command.new(/^co/, -> (*options) { cmd_co(*options) }),
708
+ Command.new(/^cy/, -> (*_options) { cmd_cy }),
709
+ Command.new(/^d/, -> (*_options) { cmd_d }),
710
+ Command.new(/^h/, -> (*_options) { cmd_h }),
711
+ Command.new(/^i/, -> (*_options) { cmd_i }),
712
+ Command.new(/^lsa/, -> (*_options) { cmd_lsa }),
713
+ Command.new(/^lsp/, -> (*_options) { cmd_lsp }),
714
+ Command.new(/^n/, -> (*_options) { cmd_n }),
715
+ Command.new(/^of/, -> (*_options) { cmd_of }),
716
+ Command.new(/^on/, -> (*_options) { cmd_on }),
717
+ Command.new(/^pa/, -> (*options) { cmd_pa(*options) }),
718
+ Command.new(/^q/, -> (*_options) { cmd_q }),
719
+ Command.new(/^r/, -> (*options) { cmd_r(*options) }),
720
+ Command.new(/^s/, -> (*_options) { cmd_s }),
721
+ Command.new(/^t/, -> (*options) { cmd_t(*options) }),
722
+ Command.new(/^w/, -> (*_options) { cmd_w }),
723
+ Command.new(/^x/, -> (*_options) { cmd_x })
724
+ ]
725
+ end
726
+
727
+
728
+ def find_command_action(command_string)
729
+ result = commands.detect { |cmd| cmd.regex.match(command_string) }
730
+ result ? result.action : nil
731
+ end
732
+
733
+
734
+ def call
735
+ validate_command_line
736
+ begin
737
+ process_command_line(ARGV[0], ARGV[1..-1])
738
+ rescue BadCommandError => error
739
+ separator_line = "#{'!' * 79}\n"
740
+ puts separator_line + "Bad command: #{ARGV[0]}\n" + separator_line + "\n"
741
+ exit(-1)
742
+ end
743
+ end
744
+
745
+ end
746
+
747
+ end
748
+
749
+
750
+ # @return true if this file is being run as a script, else false
751
+ #
752
+ # This file could be called as a script in either of these two ways:
753
+ #
754
+ # 1) by loading this file directly, or
755
+ # 2) by running as a gem executable's binstub, in (relatively) '../../../bin'
756
+
757
+ def running_as_script?
758
+ return true if __FILE__ == $0
759
+ return false if File.basename(__FILE__) != File.basename($0)
760
+
761
+ binstub_spec = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'bin', File.basename(__FILE__)))
762
+ $0 == binstub_spec
763
+ end
764
+
765
+
766
+ # If this file is being called as a script, run it.
767
+ # Else, it may be loaded to use the model in a different way.
768
+ if running_as_script?
769
+ MacWifi::CommandLineInterface.new.call
770
+ end
771
+
data/mac-wifi.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ VERSION = '0.0.1'
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "mac-wifi"
6
+ spec.version = VERSION
7
+ spec.authors = ["Keith Bennett"]
8
+ spec.email = ["keithrbennett@gmail.com"]
9
+ spec.description = %q{A command line interface for managing wifi on a Mac.}
10
+ spec.summary = %q{Mac wifi utility}
11
+ spec.homepage = "https://github.com/keithrbennett/mac-wifi"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files`.split($/) - ['teaching_outline.md']
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ['lib']
18
+
19
+ # spec.add_development_dependency "bundler", "~> 1.3"
20
+ # spec.add_development_dependency "rake", '~> 10.1'
21
+ # spec.add_development_dependency "rspec", '~> 3.0'
22
+
23
+ end
24
+
@@ -0,0 +1,114 @@
1
+ # The functionality of this software is very difficult to test,
2
+ # sinece it relies on external conditions that cannot be faked.
3
+ # These tests merely run the commands and assert that no
4
+ # error has occurred; they don't make any attempt to verify the data.
5
+ # Many of them are run once with the wifi on, and once when it's off.
6
+
7
+
8
+ load File.join(File.dirname(__FILE__), '..', 'bin', 'mac-wifi')
9
+
10
+ module MacWifi
11
+
12
+ describe Model do
13
+
14
+
15
+ subject { Model.new }
16
+
17
+ context 'turning wifi on and off' do
18
+ it 'can turn wifi on' do
19
+ subject.wifi_off
20
+ expect(subject.wifi_on?).to eq(false)
21
+ subject.wifi_on
22
+ expect(subject.wifi_on?).to eq(true)
23
+ end
24
+
25
+ it 'can turn wifi off' do
26
+ subject.wifi_on
27
+ expect(subject.wifi_on?).to eq(true)
28
+ subject.wifi_off
29
+ expect(subject.wifi_on?).to eq(false)
30
+ end
31
+
32
+ it 'can cycle network' do
33
+ subject.wifi_on
34
+ subject.cycle_network
35
+ expect(subject.wifi_on?).to eq(true)
36
+ end
37
+
38
+ it 'can list available networks' do
39
+ subject.wifi_on
40
+ subject.available_network_info
41
+ end
42
+ end
43
+
44
+
45
+ shared_examples_for 'testing to see commands complete without error' do |wifi_starts_on|
46
+
47
+ it 'can determine if connected to Internet' do
48
+ wifi_starts_on ? subject.wifi_on : subject.wifi_off
49
+ # We cannot assert that we're connected to the Internet even
50
+ # if the wifi is on, because we're probably not connected to a network.
51
+ subject.connected_to_internet?
52
+ end
53
+
54
+ it 'can get wifi port' do
55
+ wifi_starts_on ? subject.wifi_on : subject.wifi_off
56
+ subject.wifi_hardware_port
57
+ end
58
+
59
+ it 'can list info' do
60
+ wifi_starts_on ? subject.wifi_on : subject.wifi_off
61
+ subject.wifi_info
62
+ end
63
+
64
+ it 'can list preferred networks' do
65
+ wifi_starts_on ? subject.wifi_on : subject.wifi_off
66
+ subject.preferred_networks
67
+ end
68
+
69
+ it 'can see if wifi is on' do
70
+ wifi_starts_on ? subject.wifi_on : subject.wifi_off
71
+ subject.wifi_on?
72
+ end
73
+
74
+ it 'can query the connected network name' do
75
+ wifi_starts_on ? subject.wifi_on : subject.wifi_off
76
+ name = subject.connected_network_name
77
+ unless subject.wifi_on?
78
+ expect(name).to eq(nil)
79
+ end
80
+ end
81
+
82
+ # it 'can attempt to connect to a network' do
83
+ # pending 'cannot reliably expect any given network to be available'
84
+ # end
85
+
86
+ # it 'can determine the IP address on the network' do
87
+ # pending 'How to reliably reproduce connection to a network?'
88
+ # end
89
+
90
+ it 'can determine the current network' do
91
+ wifi_starts_on ? subject.wifi_on : subject.wifi_off
92
+ network = subject.current_network
93
+ unless subject.wifi_on?
94
+ expect(network).to eq(nil)
95
+ end
96
+ end
97
+
98
+ it 'can call disconnect twice consecutively' do
99
+ wifi_starts_on ? subject.wifi_on : subject.wifi_off
100
+ subject.disconnect
101
+ subject.disconnect
102
+ end
103
+ end
104
+
105
+ context 'wifi starts on' do # without a context block the way that rspec expands the examples causes the parameters to overwrite each other
106
+ include_examples 'testing to see commands complete without error', true
107
+ end
108
+
109
+ context 'wifi starts off' do # without a context block the way that rspec expands the examples causes the parameters to overwrite each other
110
+ include_examples 'testing to see commands complete without error', false
111
+ end
112
+ end
113
+
114
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mac-wifi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Keith Bennett
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-09-17 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A command line interface for managing wifi on a Mac.
14
+ email:
15
+ - keithrbennett@gmail.com
16
+ executables:
17
+ - mac-wifi
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".gitignore"
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - bin/mac-wifi
26
+ - mac-wifi.gemspec
27
+ - spec/mac-wifi_spec.rb
28
+ homepage: https://github.com/keithrbennett/mac-wifi
29
+ licenses:
30
+ - MIT
31
+ metadata: {}
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubyforge_project:
48
+ rubygems_version: 2.6.13
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Mac wifi utility
52
+ test_files:
53
+ - spec/mac-wifi_spec.rb