mac-wifi 0.0.1

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 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