wifi-wand 2.15.0 → 2.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51d32000847efd2e81288f34905ef2622d875d460b3f4bd3f3a823a7bfc53ff5
4
- data.tar.gz: 9a9bd05cac4003f148387c877a2e47cf37cea9fdd793cc848e3e5b01a6bc0887
3
+ metadata.gz: 4729288451e02d7b968dac5ef6bfd703383e3e7a759386ae6b74d82527e2dea6
4
+ data.tar.gz: '085c16eba0b518c4be31609cfd8847788f8d0e0985ae9b167c41585c66a18182'
5
5
  SHA512:
6
- metadata.gz: 6c82d035c66e289ff335f774313fde6a9bfec1bf403dc4f71f375d2ea825ebb12b0c265e27ee61361455466606ccfb898c9ab7588efde854a909f9bc1a023224
7
- data.tar.gz: f75163a1ec497cf8010ddd28306cd4d1c621042c4af6e6835d55aeaad20f2751d8ad6c9f479c748fe26c65259d3c19015200612aa297349db3b59d4df0dcfb16
6
+ metadata.gz: 83b47a2b64ba1a6b436f38c7da2f546156076891cb73fae03a6b90f57b72119c4d6affd25bff7bac42523fd0e4abfcbf45eb9fb7363739cc9641a7992881fdad
7
+ data.tar.gz: 365bd3f24ed79803b100ab0785c16a4257dadb18c25f9a8a6c0617eda4e4bba8786104000c559d24932bffc6cda95e428bf617b1c39b50db675b87a8170eabed
data/LICENSE.txt CHANGED
@@ -1,22 +1,201 @@
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.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2019 Bennett Business Solutions, Inc.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/README.md CHANGED
@@ -69,6 +69,15 @@ When in interactive shell mode:
69
69
  Internally, it uses several Mac command line utilities to interact with the
70
70
  underlying operating system.
71
71
 
72
+ > [!WARNING]
73
+ > Starting in Mac OS version 14.4, the `airport` utility on which some of this project's
74
+ > functionality relies has been disabled and will presumably eventually be removed.
75
+ >
76
+ > The following commands will result in a runtime error if `airport` is no longer available:
77
+ >
78
+ > * listing names of available wifi networks
79
+ > * listing detailed information about available wifi networks
80
+ > * disconnecting from a wifi network
72
81
 
73
82
  ### Pretty Output
74
83
 
@@ -359,6 +368,11 @@ wifi-wand connect my-usual-network its-password
359
368
 
360
369
  MIT License (see LICENSE.txt)
361
370
 
371
+ ### Logo
372
+
373
+ Logo designed and generously contributed by Anhar Ismail (Github: [@anharismail](https://github.com/anharismail), Twitter: [@aizenanhar](https://twitter.com/aizenanhar)).
374
+
375
+
362
376
  ### Shameless Ad
363
377
 
364
378
  I am available for consulting, development, tutoring, training, troubleshooting, etc.
data/RELEASE_NOTES.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## v2.16.0 (2024-04)
2
+
3
+ * Handle deprecation of the `airport` command starting at Mac OS 14.4.
4
+ * Add hotspot_login_required functionality.
5
+ * Change 'port' to 'interface' in some names.
6
+ * Add to external resources: captive.apple.com, librespeed.org
7
+ * Change license from MIT to Apache 2.
8
+
9
+
10
+ ## v2.15.2
11
+
12
+ * Improve support for 'hotspot login required'.
13
+ * Add 'hotspot_login_required' field to info hash, & on connect, opens captive.aple.com page if needed.
14
+ * Change license from MIT to Apache 2.
15
+
16
+
17
+ ## v2.15.1
18
+
19
+ * Fix bug; when calling connect with an SSID with leading spaces, a warning was erroneously issued about the SSID.
20
+
21
+
1
22
  ## v2.15.0
2
23
 
3
24
  * Allow using symbols in the 'nameservers' subcommands.
@@ -54,9 +54,9 @@ describe MacOsModel do
54
54
  subject.connected_to_internet?
55
55
  end
56
56
 
57
- it 'can get wifi port' do
57
+ it 'can get wifi interface' do
58
58
  wifi_starts_on ? subject.wifi_on : subject.wifi_off
59
- subject.wifi_port
59
+ subject.wifi_interface
60
60
  end
61
61
 
62
62
  it 'can list info' do
@@ -1,3 +1,4 @@
1
+ require 'awesome_print'
1
2
  require_relative 'operating_systems'
2
3
  require 'ostruct'
3
4
  require_relative 'error'
@@ -43,8 +44,10 @@ class CommandLineInterface
43
44
  end
44
45
 
45
46
  OPEN_RESOURCES = OpenResources.new([
47
+ OpenResource.new('cap', 'https://captive.apple.com/', 'Portal Logins'),
46
48
  OpenResource.new('ipl', 'https://www.iplocation.net/', 'IP Location'),
47
49
  OpenResource.new('ipw', 'https://www.whatismyip.com', 'What is My IP'),
50
+ OpenResource.new('libre','https://www.librespeed.org', 'LibreSpeed'),
48
51
  OpenResource.new('spe', 'http://speedtest.net/', 'Speed Test'),
49
52
  OpenResource.new('this', 'https://github.com/keithrbennett/wifiwand', 'wifi-wand home page'),
50
53
  ])
@@ -55,7 +58,7 @@ class CommandLineInterface
55
58
  Command Line Switches: [wifi-wand version #{WifiWand::VERSION} at https://github.com/keithrbennett/wifiwand]
56
59
 
57
60
  -o {i,j,k,p,y} - outputs data in inspect, JSON, pretty JSON, puts, or YAML format when not in shell mode
58
- -p wifi_port_name - override automatic detection of port name with this name
61
+ -p wifi_interface_name - override automatic detection of interface name with this name
59
62
  -s - run in shell mode
60
63
  -v - verbose mode (prints OS commands and their outputs)
61
64
 
@@ -93,18 +96,19 @@ When in interactive shell mode:
93
96
  "
94
97
 
95
98
  def initialize(options)
96
- @options = options
97
99
  current_os = OperatingSystems.new.current_os
98
- raise Error.new("Could not determine operating system") if current_os.nil?
100
+ if current_os.nil?
101
+ puts "This application currently runs only on Mac OS."
102
+ exit(1)
103
+ end
104
+
105
+ @options = options
106
+
99
107
  model_options = OpenStruct.new({
100
- verbose: options.verbose,
101
- wifi_port: options.wifi_port
108
+ verbose: options.verbose,
109
+ wifi_interface: options.wifi_interface
102
110
  })
103
111
 
104
- unless awesome_print_available?
105
- HELP_TEXT << "For nicer output, `gem install awesome_print`.\n\n"
106
- end
107
-
108
112
  @model = current_os.create_model(model_options)
109
113
  @interactive_mode = !!(options.interactive_mode)
110
114
  run_shell if @interactive_mode
@@ -121,27 +125,8 @@ When in interactive shell mode:
121
125
  end
122
126
 
123
127
 
124
- # @return true if awesome_print is available (after requiring it), else false after requiring 'pp'.
125
- # We'd like to use awesome_print if it is available, but not require it.
126
- # So, we try to require it, but if that fails, we fall back to using pp (pretty print),
127
- # which is included in Ruby distributions without the need to install a gem.
128
- def awesome_print_available?
129
- if @awesome_print_available.nil? # first time here
130
- begin
131
- require 'awesome_print'
132
- @awesome_print_available = true
133
- rescue LoadError
134
- require 'pp'
135
- @awesome_print_available = false
136
- end
137
- end
138
-
139
- @awesome_print_available
140
- end
141
-
142
-
143
128
  def fancy_string(object)
144
- awesome_print_available? ? object.ai : object.pretty_inspect
129
+ object.awesome_inspect
145
130
  end
146
131
 
147
132
 
@@ -161,30 +146,11 @@ When in interactive shell mode:
161
146
  end
162
147
 
163
148
 
164
- # Pry will output the content of the method from which it was called.
165
- # This small method exists solely to reduce the amount of pry's output
166
- # that is not needed here.
167
- def run_pry
168
- binding.pry
169
-
170
- # the seemingly useless line below is needed to avoid pry's exiting
171
- # (see https://github.com/deivid-rodriguez/pry-byebug/issues/45)
172
- _a = nil
173
- end
174
-
175
-
176
149
  # Runs a pry session in the context of this object.
177
150
  # Commands and options specified on the command line can also be specified in the shell.
178
151
  def run_shell
179
- begin
180
- require 'pry'
181
- rescue LoadError
182
- message = "The 'pry' gem and/or one of its prerequisites, required for running the shell, was not found." +
183
- " Please `gem install pry` or, if necessary, `sudo gem install pry`."
184
- raise Error.new(message)
185
- end
186
-
187
152
  print_help
153
+ require 'pry'
188
154
 
189
155
  # Enable the line below if you have any problems with pry configuration being loaded
190
156
  # that is messing up this runtime use of pry:
@@ -195,7 +161,7 @@ When in interactive shell mode:
195
161
  # a pry command from a DSL command, which _is_ useful here.
196
162
  Pry.config.command_prefix = '%'
197
163
 
198
- run_pry
164
+ binding.pry
199
165
  end
200
166
 
201
167
 
@@ -385,7 +351,13 @@ When in interactive shell mode:
385
351
 
386
352
  # Use Mac OS 'open' command line utility
387
353
  def cmd_ro(*resource_codes)
354
+ if resource_codes.empty?
355
+ puts "Please specify a resource to open:\n #{OPEN_RESOURCES.help_string.gsub(',', "\n")}"
356
+ return
357
+ end
358
+
388
359
  resource_codes.each do |code|
360
+ code = code.to_s # accommodate conversion of parameter from other types, esp. symbols
389
361
  resource = OPEN_RESOURCES.find_by_code(code)
390
362
  if resource
391
363
  if code == 'spe' && Dir.exist?('/Applications/Speedtest.app/')
@@ -524,4 +496,4 @@ When in interactive shell mode:
524
496
  end
525
497
  end
526
498
  end
527
- end
499
+ end
@@ -46,8 +46,8 @@ class Main
46
46
  options.post_processor = formatters[choice]
47
47
  end
48
48
 
49
- parser.on("-p", "--wifi-port PORT", "WiFi port name") do |v|
50
- options.wifi_port = v
49
+ parser.on("-p", "--wifi-interface interface", "WiFi interface name") do |v|
50
+ options.wifi_interface = v
51
51
  end
52
52
 
53
53
  parser.on("-h", "--help", "Show help") do |_help_requested|
@@ -64,9 +64,8 @@ class Main
64
64
  begin
65
65
  WifiWand::CommandLineInterface.new(options).call
66
66
  rescue => e
67
- # require 'pry'; binding.pry
68
67
  puts "Error: #{e.backtrace.join("\n")}\n\n#{e.message}"
69
68
  end
70
69
  end
71
70
  end
72
- end
71
+ end
@@ -4,13 +4,12 @@ require 'tempfile'
4
4
  require 'uri'
5
5
  require_relative 'helpers/command_output_formatter'
6
6
  require_relative '../error'
7
- require_relative '../../wifi-wand'
8
7
 
9
8
  module WifiWand
10
9
 
11
10
  class BaseModel
12
11
 
13
- attr_accessor :wifi_port, :verbose_mode
12
+ attr_accessor :wifi_interface, :verbose_mode
14
13
 
15
14
  class OsCommandError < RuntimeError
16
15
  attr_reader :exitstatus, :command, :text
@@ -34,10 +33,10 @@ class BaseModel
34
33
  def initialize(options)
35
34
  @verbose_mode = options.verbose
36
35
 
37
- if options.wifi_port && (! is_wifi_port?(options.wifi_port))
38
- raise Error.new("#{options.wifi_port} is not a Wi-Fi interface.")
36
+ if options.wifi_interface && (! is_wifi_interface?(options.wifi_interface))
37
+ raise Error.new("#{options.wifi_interface} is not a Wi-Fi interface.")
39
38
  end
40
- @wifi_port = options.wifi_port
39
+ @wifi_interface = options.wifi_interface
41
40
  end
42
41
 
43
42
 
@@ -102,16 +101,16 @@ class BaseModel
102
101
  Thread.new do
103
102
  url = URI.parse(site)
104
103
  success = true
104
+ start = Time.now
105
105
 
106
106
  begin
107
107
  Net::HTTP.start(url.host) do |http|
108
- start = Time.now
109
108
  http.read_timeout = 3 # seconds
110
109
  http.get('.')
111
- duration = Time.now - start
112
- puts "Finished HTTP get #{url.host} in #{duration} seconds" if verbose_mode
110
+ puts "Finished HTTP get #{url.host} in #{Time.now - start} seconds" if verbose_mode
113
111
  end
114
- rescue
112
+ rescue => e
113
+ puts "Got error for host #{url.host} in #{Time.now - start} seconds:\n#{e.inspect}" if verbose_mode
115
114
  success = false
116
115
  end
117
116
 
@@ -131,6 +130,13 @@ class BaseModel
131
130
  end
132
131
 
133
132
 
133
+ def hotspot_login_required
134
+ response_text = run_os_command('curl --max-time 3 http://captive.apple.com', false)
135
+ required = ['Error', '302', 'Hotspot login required'].all? { |s| response_text.include?(s) }
136
+ required
137
+ end
138
+
139
+
134
140
  # Turns wifi off and then on, reconnecting to the originally connecting network.
135
141
  def cycle_network
136
142
  # TODO: Make this network name saving and restoring conditional on it not having a password.
@@ -164,7 +170,13 @@ class BaseModel
164
170
 
165
171
  # Verify that the network is now connected:
166
172
  actual_network_name = connected_network_name
167
- unless actual_network_name == network_name
173
+
174
+ if actual_network_name == network_name
175
+ if hotspot_login_required
176
+ puts "Hotspot login required. Opening browser to captive.apple.com."
177
+ `open https://captive.apple.com`
178
+ end
179
+ else
168
180
  message = %Q{Expected to connect to "#{network_name}" but }
169
181
  if actual_network_name.nil? || actual_network_name.empty?
170
182
  message << "unable to connect to any network."
@@ -271,8 +283,8 @@ class BaseModel
271
283
  end
272
284
 
273
285
 
274
- def wifi_port
275
- @wifi_port ||= detect_wifi_port
286
+ def wifi_interface
287
+ @wifi_interface ||= detect_wifi_interface
276
288
  end
277
289
  end
278
- end
290
+ end
@@ -6,36 +6,76 @@ require 'shellwords'
6
6
  require_relative 'base_model'
7
7
  require_relative '../error'
8
8
 
9
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10
+ # 2024-04-18:
11
+ #
12
+ # Apple has deprecated the 'airport' utility and has already disabled its
13
+ # functionality. This utility is used for the following wifi-wand commands:
14
+
15
+ # 1) cmd: info, fn: wifi_info - adds information to the info output
16
+ # 2) cmd: avail_nets, fn: available_network_names - available wifi network names
17
+ # 3) cmd: ls_avail_nets, fn: available_network_info - available wifi networks details
18
+ # 4) cmd: wifi_on, fn: wifi_on?
19
+ # 5) cmd: network_name, fn: connected_network_name
20
+ # 6) cmd: disconnect, fn: disconnect
21
+
22
+ # Functions 4 and 5 have been fixed to use `networksetup` instead of `airport`.
23
+ # The others are not yet fixed.
24
+
25
+ # An AskDifferent (Mac StackExchange site) question has been posted to
26
+ # https://apple.stackexchange.com/questions/471886/how-to-replace-functionality-of-deprecated-airport-command-line-application.
27
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
28
+
9
29
  module WifiWand
10
30
 
11
31
  class MacOsModel < BaseModel
12
32
 
13
- AIRPORT_CMD = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
33
+ DEFAULT_AIRPORT_FILESPEC = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
34
+
35
+ attr_reader :airport_deprecated, :mac_os_version_major, :mac_os_version_minor, :mac_os_version_string
14
36
 
15
- # Takes an OpenStruct containing options such as verbose mode and port name.
37
+ # Takes an OpenStruct containing options such as verbose mode and interface name.
16
38
  def initialize(options = OpenStruct.new)
17
39
  super
40
+ populate_mac_os_version
41
+ @airport_deprecated = @mac_os_version_major > 14 || (@mac_os_version_major == 14 && @mac_os_version_minor >= 4)
42
+ end
43
+
44
+ # Provides Mac OS major and minor version numbers
45
+ def populate_mac_os_version
46
+ @mac_os_version_string = `sw_vers --productVersion`.chomp
47
+ @mac_os_version_major, @mac_os_version_minor = mac_os_version_string.split('.').map(&:to_i)
48
+ [@mac_os_version_major, @mac_os_version_minor]
18
49
  end
19
50
 
51
+ def airport_deprecated_message
52
+ <<~MESSAGE
53
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
54
+ This method requires the airport utility which is no longer functional in Mac OS >= 14.4.
55
+ You are running Mac OS version #{mac_os_version_string}.
56
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
57
+ MESSAGE
58
+ end
20
59
 
21
60
  # Although at this time the airport command utility is predictable,
22
61
  # allow putting it elsewhere in the path for overriding and easier fix
23
62
  # if that location should change.
24
63
  def airport_command
25
64
  airport_in_path = `which airport`.chomp
26
- if ! airport_in_path.empty?
27
- airport_in_path
28
- elsif File.exist?(AIRPORT_CMD)
29
- AIRPORT_CMD
30
- else
31
- raise Error.new("Airport command not found.")
32
- end
65
+
66
+ return airport_in_path unless airport_in_path.empty?
67
+
68
+ return DEFAULT_AIRPORT_FILESPEC if File.exist?(DEFAULT_AIRPORT_FILESPEC)
69
+
70
+ raise Error.new("Airport command not found.") unless airport_deprecated
71
+
72
+ nil # no error, no data
33
73
  end
34
74
 
35
75
 
36
- # Identifies the (first) wireless network hardware port in the system, e.g. en0 or en1
76
+ # Identifies the (first) wireless network hardware interface in the system, e.g. en0 or en1
37
77
  # This may not detect wifi ports with nonstandard names, such as USB wifi devices.
38
- def detect_wifi_port
78
+ def detect_wifi_interface
39
79
 
40
80
  lines = run_os_command("networksetup -listallhardwareports").split("\n")
41
81
  # Produces something like this:
@@ -47,14 +87,14 @@ class MacOsModel < BaseModel
47
87
  # Device: en3
48
88
  # Ethernet Address: ac:bc:32:b9:a9:9e
49
89
 
50
- wifi_port_line_num = (0...lines.size).detect do |index|
90
+ wifi_interface_line_num = (0...lines.size).detect do |index|
51
91
  /: Wi-Fi$/.match(lines[index])
52
92
  end
53
93
 
54
- if wifi_port_line_num.nil?
55
- raise Error.new(%Q{Wifi port (e.g. "en0") not found in output of: networksetup -listallhardwareports})
94
+ if wifi_interface_line_num.nil?
95
+ raise Error.new(%Q{Wifi interface (e.g. "en0") not found in output of: networksetup -listallhardwareports})
56
96
  else
57
- lines[wifi_port_line_num + 1].split(': ').last
97
+ lines[wifi_interface_line_num + 1].split(': ').last
58
98
  end
59
99
  end
60
100
 
@@ -72,6 +112,9 @@ class MacOsModel < BaseModel
72
112
  # "Chancery 2a:a4:3c:03:33:99 -59 60,+1 Y -- NONE",
73
113
  # "DIRECT-sq-BRAVIA 02:71:cc:87:4a:8c -76 6 Y -- WPA2(PSK/AES/AES) ", #
74
114
  def available_network_info
115
+
116
+ raise RuntimeError, airport_deprecated_message if airport_deprecated
117
+
75
118
  return nil unless wifi_on? # no need to try
76
119
  command = "#{airport_command} -s | iconv -f macroman -t utf-8"
77
120
  max_attempts = 50
@@ -147,6 +190,8 @@ class MacOsModel < BaseModel
147
190
  #
148
191
  # REXML is used here to avoid the need for the user to install Nokogiri.
149
192
  def available_network_names
193
+ raise RuntimeError, airport_deprecated_message if airport_deprecated
194
+
150
195
  return nil unless wifi_on? # no need to try
151
196
 
152
197
  # For some reason, the airport command very often returns nothing, so we need to try until
@@ -166,7 +211,7 @@ class MacOsModel < BaseModel
166
211
 
167
212
  # Returns data pertaining to "preferred" networks, many/most of which will probably not be available.
168
213
  def preferred_networks
169
- lines = run_os_command("networksetup -listpreferredwirelessnetworks #{wifi_port}").split("\n")
214
+ lines = run_os_command("networksetup -listpreferredwirelessnetworks #{wifi_interface}").split("\n")
170
215
  # Produces something like this, unsorted, and with leading tabs:
171
216
  # Preferred networks on en0:
172
217
  # LibraryWiFi
@@ -179,9 +224,9 @@ class MacOsModel < BaseModel
179
224
  end
180
225
 
181
226
 
182
- # Returns whether or not the specified interface is a WiFi interfae.
183
- def is_wifi_port?(port)
184
- run_os_command("networksetup -listpreferredwirelessnetworks #{port} 2>/dev/null")
227
+ # Returns whether or not the specified interface is a WiFi interface.
228
+ def is_wifi_interface?(interface)
229
+ run_os_command("networksetup -listpreferredwirelessnetworks #{interface} 2>/dev/null")
185
230
  exit_status = $?.exitstatus
186
231
  exit_status != 10
187
232
  end
@@ -189,15 +234,16 @@ class MacOsModel < BaseModel
189
234
 
190
235
  # Returns true if wifi is on, else false.
191
236
  def wifi_on?
192
- lines = run_os_command("#{airport_command} -I").split("\n")
193
- lines.grep("AirPort: Off").none?
237
+ output = run_os_command("networksetup -getairportpower #{wifi_interface}")
238
+ output.chomp.match?(/\): On$/)
194
239
  end
195
240
 
196
241
 
197
242
  # Turns wifi on.
198
243
  def wifi_on
199
244
  return if wifi_on?
200
- run_os_command("networksetup -setairportpower #{wifi_port} on")
245
+
246
+ run_os_command("networksetup -setairportpower #{wifi_interface} on")
201
247
  wifi_on? ? nil : Error.new(raise("Wifi could not be enabled."))
202
248
  end
203
249
 
@@ -205,14 +251,16 @@ class MacOsModel < BaseModel
205
251
  # Turns wifi off.
206
252
  def wifi_off
207
253
  return unless wifi_on?
208
- run_os_command("networksetup -setairportpower #{wifi_port} off")
254
+
255
+ run_os_command("networksetup -setairportpower #{wifi_interface} off")
256
+
209
257
  wifi_on? ? Error.new(raise("Wifi could not be disabled.")) : nil
210
258
  end
211
259
 
212
260
 
213
261
  # This method is called by BaseModel#connect to do the OS-specific connection logic.
214
262
  def os_level_connect(network_name, password = nil)
215
- command = "networksetup -setairportnetwork #{wifi_port} " + "#{Shellwords.shellescape(network_name)}"
263
+ command = "networksetup -setairportnetwork #{wifi_interface} #{Shellwords.shellescape(network_name)}"
216
264
  if password
217
265
  command << ' ' << Shellwords.shellescape(password)
218
266
  end
@@ -240,11 +288,11 @@ class MacOsModel < BaseModel
240
288
  end
241
289
 
242
290
 
243
- # Returns the IP address assigned to the wifi port, or nil if none.
291
+ # Returns the IP address assigned to the wifi interface, or nil if none.
244
292
  def ip_address
245
293
  return nil unless wifi_on? # no need to try
246
294
  begin
247
- run_os_command("ipconfig getifaddr #{wifi_port}").chomp
295
+ run_os_command("ipconfig getifaddr #{wifi_interface}").chomp
248
296
  rescue OsCommandError => error
249
297
  if error.exitstatus == 1
250
298
  nil
@@ -258,21 +306,25 @@ class MacOsModel < BaseModel
258
306
  def remove_preferred_network(network_name)
259
307
  network_name = network_name.to_s
260
308
  run_os_command("sudo networksetup -removepreferredwirelessnetwork " +
261
- "#{wifi_port} #{Shellwords.shellescape(network_name)}")
309
+ "#{wifi_interface} #{Shellwords.shellescape(network_name)}")
262
310
  end
263
311
 
264
312
 
265
313
  # Returns the network currently connected to, or nil if none.
266
314
  def connected_network_name
267
315
  return nil unless wifi_on? # no need to try
268
- lines = run_os_command("#{airport_command} -I").split("\n")
269
- ssid_lines = lines.grep(/ SSID:/)
270
- ssid_lines.empty? ? nil : ssid_lines.first.split('SSID: ').last.lstrip
316
+
317
+ command_output = run_os_command("networksetup -getairportnetwork #{wifi_interface}")
318
+ connected_prefix = 'Current Wi-Fi Network: '
319
+ connected = Regexp.new(connected_prefix).match?(command_output)
320
+ connected ? command_output.split(connected_prefix).last.chomp : nil
271
321
  end
272
322
 
273
323
 
274
324
  # Disconnects from the currently connected network. Does not turn off wifi.
275
325
  def disconnect
326
+ raise RuntimeError, airport_deprecated_message if airport_deprecated
327
+
276
328
  return nil unless wifi_on? # no need to try
277
329
  run_os_command("sudo #{airport_command} -z")
278
330
  nil
@@ -286,7 +338,7 @@ class MacOsModel < BaseModel
286
338
  # then this method returns the current address if none is provided,
287
339
  # but sets to the specified address if it is.
288
340
  def mac_address
289
- run_os_command("ifconfig #{wifi_port} | awk '/ether/{print $2}'").chomp
341
+ run_os_command("ifconfig #{wifi_interface} | awk '/ether/{print $2}'").chomp
290
342
  end
291
343
 
292
344
 
@@ -299,22 +351,28 @@ class MacOsModel < BaseModel
299
351
  false
300
352
  end
301
353
 
354
+ need_hotspot_login = hotspot_login_required
355
+
302
356
  info = {
303
357
  'wifi_on' => wifi_on?,
304
358
  'internet_on' => connected,
305
- 'port' => wifi_port,
359
+ 'hotspot_login_required' => need_hotspot_login,
360
+ 'interface' => wifi_interface,
306
361
  'network' => connected_network_name,
307
362
  'ip_address' => ip_address,
308
363
  'mac_address' => mac_address,
309
364
  'nameservers' => nameservers_using_scutil,
310
365
  'timestamp' => Time.now,
311
366
  }
312
- more_output = run_os_command(airport_command + " -I")
313
- more_info = colon_output_to_hash(more_output)
314
- info.merge!(more_info)
315
- info.delete('AirPort') # will be here if off, but info is already in wifi_on key
316
367
 
317
- if info['internet_on']
368
+ unless airport_deprecated
369
+ more_output = run_os_command(airport_command + " -I")
370
+ more_info = colon_output_to_hash(more_output)
371
+ info.merge!(more_info)
372
+ info.delete('AirPort') # will be here if off, but info is already in wifi_on key
373
+ end
374
+
375
+ if info['internet_on'] && (! need_hotspot_login)
318
376
  begin
319
377
  info['public_ip'] = public_ip_address_info
320
378
  rescue => e
@@ -406,4 +464,4 @@ class MacOsModel < BaseModel
406
464
  output.split("\n")
407
465
  end
408
466
  end
409
- end
467
+ end
@@ -16,8 +16,8 @@ module WifiWand
16
16
  :try_os_command_until,
17
17
  :verbose_mode,
18
18
  :verbose_mode=,
19
- :wifi_port,
20
- :wifi_port=
19
+ :wifi_interface,
20
+ :wifi_interface=
21
21
  ]
22
22
 
23
23
 
@@ -30,10 +30,10 @@ module WifiWand
30
30
  :available_network_info,
31
31
  :available_network_names,
32
32
  :connected_network_name,
33
- :detect_wifi_port,
33
+ :detect_wifi_interface,
34
34
  :disconnect,
35
35
  :ip_address,
36
- :is_wifi_port?,
36
+ :is_wifi_interface?,
37
37
  :mac_address,
38
38
  :nameservers_using_networksetup,
39
39
  :nameservers_using_resolv_conf,
@@ -57,4 +57,4 @@ module WifiWand
57
57
 
58
58
 
59
59
  end
60
- end
60
+ end
@@ -1,5 +1,3 @@
1
1
  module WifiWand
2
-
3
- VERSION = '2.15.0'
4
-
2
+ VERSION = '2.16.0' unless defined?(VERSION)
5
3
  end
data/lib/wifi-wand.rb CHANGED
@@ -5,4 +5,4 @@ require_relative 'wifi-wand/main' # recursively requires the other files
5
5
  # When additional operating systems are added, we will need to modify this
6
6
  # to load only the model appropriate for the environment:
7
7
 
8
- require_relative 'wifi-wand/models/mac_os_model'
8
+ require_relative 'wifi-wand/models/mac_os_model'
@@ -0,0 +1,18 @@
1
+ require_relative '../../../lib/wifi-wand/models/base_model'
2
+
3
+ module WifiWand
4
+
5
+ describe BaseModel::OsCommandError do
6
+ subject { BaseModel::OsCommandError.new(1, 'the command', 'failed to produce an x') }
7
+
8
+ specify 'to_h produces a correct hash' do
9
+ expect(subject.to_h).to eq(exitstatus: 1, command: 'the command', text: 'failed to produce an x')
10
+ end
11
+
12
+ specify 'raising the error produces the correct string' do
13
+ expect(subject.to_s).to eq(
14
+ "WifiWand::BaseModel::OsCommandError: Error code 1, command = the command, text = failed to produce an x"
15
+ )
16
+ end
17
+ end
18
+ end
data/wifi-wand.gemspec CHANGED
@@ -19,10 +19,16 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  # spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.add_dependency('awesome_print', '>= 1.9.2', '< 2')
22
23
 
23
- spec.add_development_dependency "bundler", "~> 1.16"
24
- spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "rspec", "~> 3.0"
24
+ # still on version 0, no need to exclude future versions, but need bug fix for pry not pry'ing
25
+ # on last line of method:
26
+ spec.add_dependency('pry', '>= 0.14.2')
26
27
 
27
- end
28
+ spec.add_dependency('rexml', '>= 3.2.6', '< 4')
29
+
30
+ spec.add_development_dependency "bundler", ">= 2.5.9"
31
+ spec.add_development_dependency "rake", ">= 13.2.1"
32
+ spec.add_development_dependency "rspec", ">= 3.13.0", "< 4"
28
33
 
34
+ end
metadata CHANGED
@@ -1,57 +1,117 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wifi-wand
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Bennett
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-02-21 00:00:00.000000000 Z
11
+ date: 2024-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: awesome_print
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.9.2
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.9.2
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: pry
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.14.2
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.14.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: rexml
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 3.2.6
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '4'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.2.6
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '4'
13
67
  - !ruby/object:Gem::Dependency
14
68
  name: bundler
15
69
  requirement: !ruby/object:Gem::Requirement
16
70
  requirements:
17
- - - "~>"
71
+ - - ">="
18
72
  - !ruby/object:Gem::Version
19
- version: '1.16'
73
+ version: 2.5.9
20
74
  type: :development
21
75
  prerelease: false
22
76
  version_requirements: !ruby/object:Gem::Requirement
23
77
  requirements:
24
- - - "~>"
78
+ - - ">="
25
79
  - !ruby/object:Gem::Version
26
- version: '1.16'
80
+ version: 2.5.9
27
81
  - !ruby/object:Gem::Dependency
28
82
  name: rake
29
83
  requirement: !ruby/object:Gem::Requirement
30
84
  requirements:
31
- - - "~>"
85
+ - - ">="
32
86
  - !ruby/object:Gem::Version
33
- version: '10.0'
87
+ version: 13.2.1
34
88
  type: :development
35
89
  prerelease: false
36
90
  version_requirements: !ruby/object:Gem::Requirement
37
91
  requirements:
38
- - - "~>"
92
+ - - ">="
39
93
  - !ruby/object:Gem::Version
40
- version: '10.0'
94
+ version: 13.2.1
41
95
  - !ruby/object:Gem::Dependency
42
96
  name: rspec
43
97
  requirement: !ruby/object:Gem::Requirement
44
98
  requirements:
45
- - - "~>"
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 3.13.0
102
+ - - "<"
46
103
  - !ruby/object:Gem::Version
47
- version: '3.0'
104
+ version: '4'
48
105
  type: :development
49
106
  prerelease: false
50
107
  version_requirements: !ruby/object:Gem::Requirement
51
108
  requirements:
52
- - - "~>"
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.13.0
112
+ - - "<"
53
113
  - !ruby/object:Gem::Version
54
- version: '3.0'
114
+ version: '4'
55
115
  description: A command line interface for managing WiFi on a Mac.
56
116
  email:
57
117
  - keithrbennett@gmail.com
@@ -66,6 +126,7 @@ files:
66
126
  - README.md
67
127
  - RELEASE_NOTES.md
68
128
  - exe/wifi-wand
129
+ - integration-tests/wifi-wand/models/mac_os_model_spec.rb
69
130
  - lib/wifi-wand.rb
70
131
  - lib/wifi-wand/command_line_interface.rb
71
132
  - lib/wifi-wand/error.rb
@@ -93,14 +154,14 @@ files:
93
154
  - sample-avail-network-data.xml
94
155
  - sample-available-networks.json
95
156
  - sample-available-networks.yaml
96
- - spec/wifi-wand/models/mac_os_model_spec.rb
157
+ - spec/wifi-wand/models/base_model_spec.rb
97
158
  - test-data/invalid-byte-sequence-network-names.txt
98
159
  - wifi-wand.gemspec
99
160
  homepage: https://github.com/keithrbennett/wifiwand
100
161
  licenses:
101
162
  - MIT
102
163
  metadata: {}
103
- post_install_message:
164
+ post_install_message:
104
165
  rdoc_options: []
105
166
  require_paths:
106
167
  - lib
@@ -115,8 +176,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
176
  - !ruby/object:Gem::Version
116
177
  version: '0'
117
178
  requirements: []
118
- rubygems_version: 3.0.2
119
- signing_key:
179
+ rubygems_version: 3.5.7
180
+ signing_key:
120
181
  specification_version: 4
121
182
  summary: Mac WiFi utility
122
183
  test_files: []