mac-wifi 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/exe/mac-wifi CHANGED
@@ -19,1057 +19,6 @@
19
19
  # License: MIT License
20
20
 
21
21
 
22
- require 'json'
23
- require 'yaml'
24
-
25
- require 'shellwords'
26
- require 'tempfile'
27
-
28
- module MacWifi
29
-
30
- # This version must be kept in sync with the version in the gemspec file.
31
- VERSION = '2.0.0'
32
-
33
-
34
- class BaseModel
35
-
36
- class OsCommandError < RuntimeError
37
- attr_reader :exitstatus, :command, :text
38
-
39
- def initialize(exitstatus, command, text)
40
- @exitstatus = exitstatus
41
- @command = command
42
- @text = text
43
- end
44
- end
45
-
46
-
47
- def initialize(verbose = false)
48
- @verbose_mode = verbose
49
- end
50
-
51
-
52
- def run_os_command(command)
53
- output = `#{command} 2>&1` # join stderr with stdout
54
- if $?.exitstatus != 0
55
- raise OsCommandError.new($?.exitstatus, command, output)
56
- end
57
- if @verbose_mode
58
- puts "\n\n#{'-' * 79}\nCommand: #{command}\n\nOutput:\n#{output}#{'-' * 79}\n\n"
59
- end
60
- output
61
- end
62
- private :run_os_command
63
-
64
-
65
- # This method returns whether or not there is a working Internet connection.
66
- # Because of a Mac issue which causes a request to hang if the network is turned
67
- # off during its lifetime, we give it only 5 seconds per try,
68
- # and limit the number of tries to 3.
69
- #
70
- # This implementation will probably strike you as overly complex. The following
71
- # code looks like it is all that should be necessary, but unfortunately
72
- # this implementation often hangs when wifi is turned off while curl is active
73
- #
74
- # def connected_to_internet?
75
- # script = "curl --silent --head http://www.google.com/ > /dev/null ; echo $?"
76
- # result = `#{script}`.chomp
77
- # puts result
78
- # result == '0'
79
- # end
80
-
81
- # TODO Investigate using Curl options: --connect-timeout 1 --max-time 2 --retry 0
82
- # to greatly simplify this method.
83
- def connected_to_internet?
84
-
85
- tempfile = Tempfile.open('mac-wifi-')
86
-
87
- begin
88
- start_status_script = -> do
89
- script = "curl --silent --head http://www.google.com/ > /dev/null ; echo $? > #{tempfile.path} &"
90
- pid = Process.spawn(script)
91
- Process.detach(pid)
92
- pid
93
- end
94
-
95
- process_is_running = ->(pid) do
96
- script = %Q{ps -p #{pid} > /dev/null; echo $?}
97
- output = `#{script}`.chomp
98
- output == "0"
99
- end
100
-
101
- get_connected_state_from_curl = -> do
102
- tempfile.close
103
- File.read(tempfile.path).chomp == '0'
104
- end
105
-
106
- # Do one run, iterating during the timeout period to see if the command has completed
107
- do_one_run = -> do
108
- end_time = Time.now + 3
109
- pid = start_status_script.()
110
- while Time.now < end_time
111
- if process_is_running.(pid)
112
- sleep 0.5
113
- else
114
- return get_connected_state_from_curl.()
115
- end
116
- end
117
- Process.kill('KILL', pid) if process_is_running.(pid)
118
- :hung
119
- end
120
-
121
- 3.times do
122
- connected = do_one_run.()
123
- return connected if connected != :hung
124
- end
125
-
126
- raise "Could not determine Internet status."
127
-
128
- ensure
129
- tempfile.unlink
130
- end
131
-
132
- end
133
-
134
-
135
- # Turns wifi off and then on, reconnecting to the originally connecting network.
136
- def cycle_network
137
- # TODO: Make this network name saving and restoring conditional on it not having a password.
138
- # If the disabled code below is enabled, an error will be raised if a password is required,
139
- # even though it is stored.
140
- # network_name = current_network
141
- wifi_off
142
- wifi_on
143
- # connect(network_name) if network_name
144
- end
145
-
146
-
147
- def connected_to?(network_name)
148
- network_name == connected_network_name
149
- end
150
-
151
-
152
- # Connects to the passed network name, optionally with password.
153
- # Turns wifi on first, in case it was turned off.
154
- # Relies on subclass implementation of os_level_connect().
155
- def connect(network_name, password = nil)
156
- # Allow symbols and anything responding to to_s for user convenience
157
- network_name = network_name.to_s if network_name
158
- password = password.to_s if password
159
-
160
- if network_name.nil? || network_name.empty?
161
- raise "A network name is required but was not provided."
162
- end
163
- wifi_on
164
- os_level_connect(network_name, password)
165
-
166
- # Verify that the network is now connected:
167
- actual_network_name = connected_network_name
168
- unless actual_network_name == network_name
169
- message = %Q{Expected to connect to "#{network_name}" but }
170
- if actual_network_name
171
- message << %Q{connected to "#{connected_network_name}" instead.}
172
- else
173
- message << "unable to connect to any network. Did you "
174
- end
175
- message << (password ? "provide the correct password?" : "need to provide a password?")
176
- raise message
177
- end
178
- nil
179
- end
180
-
181
-
182
- # Removes the specified network(s) from the preferred network list.
183
- # @param network_names names of networks to remove; may be empty or contain nonexistent networks
184
- # @return names of the networks that were removed (excludes non-preexisting networks)
185
- def remove_preferred_networks(*network_names)
186
- networks_to_remove = network_names & preferred_networks # exclude any nonexistent networks
187
- networks_to_remove.each { |name| remove_preferred_network(name) }
188
- end
189
-
190
-
191
- def preferred_network_password(preferred_network_name)
192
- preferred_network_name = preferred_network_name.to_s
193
- if preferred_networks.include?(preferred_network_name)
194
- os_level_preferred_network_password(preferred_network_name)
195
- else
196
- raise "Network #{preferred_network_name} not in preferred networks list."
197
- end
198
- end
199
-
200
-
201
- # Waits for the Internet connection to be in the desired state.
202
- # @param target_status must be in [:conn, :disc, :off, :on]; waits for that state
203
- # @param wait_interval_in_secs sleeps this interval between retries; if nil or absent,
204
- # a default will be provided
205
- #
206
- def till(target_status, wait_interval_in_secs = nil)
207
-
208
- # One might ask, why not just put the 0.5 up there as the default argument.
209
- # We could do that, but we'd still need the line below in case nil
210
- # was explicitly specified. The default argument of nil above emphasizes that
211
- # the absence of an argument and a specification of nil will behave identically.
212
- wait_interval_in_secs ||= 0.5
213
-
214
- finished_predicates = {
215
- conn: -> { connected_to_internet? },
216
- disc: -> { ! connected_to_internet? },
217
- on: -> { wifi_on? },
218
- off: -> { ! wifi_on? }
219
- }
220
-
221
- finished_predicate = finished_predicates[target_status]
222
-
223
- if finished_predicate.nil?
224
- raise ArgumentError.new(
225
- "Option must be one of #{finished_predicates.keys.inspect}. Was: #{target_status.inspect}")
226
- end
227
-
228
- loop do
229
- return if finished_predicate.()
230
- sleep(wait_interval_in_secs)
231
- end
232
- end
233
-
234
-
235
- # Tries an OS command until the stop condition is true.
236
- # @command the command to run in the OS
237
- # @stop_condition a lambda taking the commands stdout as its sole parameter
238
- # @return the stdout produced by the command
239
- def try_os_command_until(command, stop_condition, max_tries = 100)
240
- max_tries.times do
241
- stdout = run_os_command(command)
242
- if stop_condition.(stdout)
243
- return stdout
244
- end
245
- end
246
- nil
247
- end
248
- end
249
-
250
-
251
- class MacOsModel < BaseModel
252
-
253
- AIRPORT_CMD = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
254
-
255
- def initialize(verbose = false)
256
- super
257
- end
258
-
259
-
260
- # This is determined by whether or not a line like the following appears in the output of `netstat -nr`:
261
- # 0/1 10.137.0.41 UGSc 15 0 utun1
262
- def vpn_running?
263
- run_os_command('netstat -nr').split("\n").grep(/^0\/1.*utun1/).any?
264
- end
265
-
266
-
267
- # Identifies the (first) wireless network hardware port in the system, e.g. en0 or en1
268
- def wifi_hardware_port
269
- @wifi_hardware_port ||= begin
270
- lines = run_os_command("networksetup -listallhardwareports").split("\n")
271
- # Produces something like this:
272
- # Hardware Port: Wi-Fi
273
- # Device: en0
274
- # Ethernet Address: ac:bc:32:b9:a9:9d
275
- #
276
- # Hardware Port: Bluetooth PAN
277
- # Device: en3
278
- # Ethernet Address: ac:bc:32:b9:a9:9e
279
- wifi_port_line_num = (0...lines.size).detect do |index|
280
- /: Wi-Fi$/.match(lines[index])
281
- end
282
- if wifi_port_line_num.nil?
283
- raise %Q{Wifi port (e.g. "en0") not found in output of: networksetup -listallhardwareports}
284
- else
285
- lines[wifi_port_line_num + 1].split(': ').last
286
- end
287
- end
288
- end
289
-
290
-
291
- # Returns data pertaining to available wireless networks.
292
- # For some reason, this often returns no results, so I've put the operation in a loop.
293
- # I was unable to detect a sort strategy in the airport utility's output, so I sort
294
- # the lines alphabetically, to show duplicates and for easier lookup.
295
- def available_network_info
296
- return nil unless wifi_on? # no need to try
297
- command = "#{AIRPORT_CMD} -s"
298
- max_attempts = 50
299
-
300
-
301
- reformat_line = ->(line) do
302
- ssid = line[0..31].strip
303
- "%-32.32s%s" % [ssid, line[32..-1]]
304
- end
305
-
306
-
307
- process_tabular_data = ->(output) do
308
- lines = output.split("\n")
309
- header_line = lines[0]
310
- data_lines = lines[1..-1]
311
- data_lines.map! do |line|
312
- # Reformat the line so that the name is left instead of right justified
313
- reformat_line.(line)
314
- end
315
- data_lines.sort!
316
- [reformat_line.(header_line)] + data_lines
317
- end
318
-
319
-
320
- output = try_os_command_until(command, ->(output) do
321
- ! ([nil, ''].include?(output))
322
- end)
323
-
324
- if output
325
- process_tabular_data.(output)
326
- else
327
- raise "Unable to get available network information after #{max_attempts} attempts."
328
- end
329
- end
330
-
331
-
332
- def parse_network_names(info)
333
- if info.nil?
334
- nil
335
- else
336
- info[1..-1] \
337
- .map { |line| line[0..32].rstrip } \
338
- .uniq \
339
- .sort { |s1, s2| s1.casecmp(s2) }
340
- end
341
- end
342
-
343
- # @return an array of unique available network names only, sorted alphabetically
344
- # Kludge alert: the tabular data does not differentiate between strings with and without leading whitespace
345
- # Therefore, we get the data once in tabular format, and another time in XML format.
346
- # The XML element will include any leading whitespace. However, it includes all <string> elements,
347
- # many of which are not network names.
348
- # As an improved approximation of the correct result, for each network name found in tabular mode,
349
- # we look to see if there is a corresponding string element with leading whitespace, and, if so,
350
- # replace it.
351
- #
352
- # This will not behave correctly if a given name has occurrences with different amounts of whitespace,
353
- # e.g. ' x' and ' x'.
354
- #
355
- # The reason we don't use an XML parser to get the exactly correct result is that we don't want
356
- # users to need to install any external dependencies in order to run this script.
357
- def available_network_names
358
-
359
- # Parses the XML text (using grep, not XML parsing) to find
360
- # <string> elements, and extracts the network name candidates
361
- # containing leading spaces from it.
362
- get_leading_space_names = ->(text) do
363
- text.split("\n") \
364
- .grep(%r{<string>}) \
365
- .sort \
366
- .uniq \
367
- .map { |line| line.gsub("<string>", '').gsub('</string>', '').gsub("\t", '') } \
368
- .select { |s| s[0] == ' ' }
369
- end
370
-
371
-
372
- output_is_valid = ->(output) { ! ([nil, ''].include?(output)) }
373
- tabular_data = try_os_command_until("#{AIRPORT_CMD} -s", output_is_valid)
374
- xml_data = try_os_command_until("#{AIRPORT_CMD} -s -x", output_is_valid)
375
-
376
- if tabular_data.nil? || xml_data.nil?
377
- raise "Unable to get available network information; please try again."
378
- end
379
-
380
- tabular_data_lines = tabular_data[1..-1] # omit header line
381
- names_no_spaces = parse_network_names(tabular_data_lines.split("\n")).map(&:strip)
382
- names_maybe_spaces = get_leading_space_names.(xml_data)
383
-
384
- names = names_no_spaces.map do |name_no_spaces|
385
- match = names_maybe_spaces.detect do |name_maybe_spaces|
386
- %r{[ \t]?#{name_no_spaces}$}.match(name_maybe_spaces)
387
- end
388
-
389
- match ? match : name_no_spaces
390
- end
391
-
392
- names.sort { |s1, s2| s1.casecmp(s2) } # sort alphabetically, case insensitively
393
- end
394
-
395
-
396
- # Returns data pertaining to "preferred" networks, many/most of which will probably not be available.
397
- def preferred_networks
398
- lines = run_os_command("networksetup -listpreferredwirelessnetworks #{wifi_hardware_port}").split("\n")
399
- # Produces something like this, unsorted, and with leading tabs:
400
- # Preferred networks on en0:
401
- # LibraryWiFi
402
- # @thePAD/Magma
403
-
404
- lines.delete_at(0) # remove title line
405
- lines.map! { |line| line.gsub("\t", '') } # remove leading tabs
406
- lines.sort! { |s1, s2| s1.casecmp(s2) } # sort alphabetically, case insensitively
407
- lines
408
- end
409
-
410
-
411
- # Returns true if wifi is on, else false.
412
- def wifi_on?
413
- lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
414
- lines.grep("AirPort: Off").none?
415
- end
416
-
417
-
418
- # Turns wifi on.
419
- def wifi_on
420
- return if wifi_on?
421
- run_os_command("networksetup -setairportpower #{wifi_hardware_port} on")
422
- wifi_on? ? nil : raise("Wifi could not be enabled.")
423
- end
424
-
425
-
426
- # Turns wifi off.
427
- def wifi_off
428
- return unless wifi_on?
429
- run_os_command("networksetup -setairportpower #{wifi_hardware_port} off")
430
- wifi_on? ? raise("Wifi could not be disabled.") : nil
431
- end
432
-
433
-
434
- def connected_network_name
435
- wifi_info['SSID']
436
- end
437
-
438
-
439
- # This method is called by BaseModel#connect to do the OS-specific connection logic.
440
- def os_level_connect(network_name, password = nil)
441
- command = "networksetup -setairportnetwork #{wifi_hardware_port} " + "#{Shellwords.shellescape(network_name)}"
442
- if password
443
- command << ' ' << Shellwords.shellescape(password)
444
- end
445
- run_os_command(command)
446
- end
447
-
448
-
449
- # @return:
450
- # If the network is in the preferred networks list
451
- # If a password is associated w/this network, return the password
452
- # If not, return nil
453
- # else
454
- # raise an error
455
- def os_level_preferred_network_password(preferred_network_name)
456
- command = %Q{security find-generic-password -D "AirPort network password" -a "#{preferred_network_name}" -w 2>&1}
457
- begin
458
- return run_os_command(command).chomp
459
- rescue OsCommandError => error
460
- if error.exitstatus == 44 # network has no password stored
461
- nil
462
- else
463
- raise
464
- end
465
- end
466
- end
467
-
468
-
469
- # Returns the IP address assigned to the wifi port, or nil if none.
470
- def ip_address
471
- begin
472
- run_os_command("ipconfig getifaddr #{wifi_hardware_port}").chomp
473
- rescue OsCommandError => error
474
- if error.exitstatus == 1
475
- nil
476
- else
477
- raise
478
- end
479
- end
480
- end
481
-
482
-
483
- def remove_preferred_network(network_name)
484
- network_name = network_name.to_s
485
- run_os_command("sudo networksetup -removepreferredwirelessnetwork " +
486
- "#{wifi_hardware_port} #{Shellwords.shellescape(network_name)}")
487
- end
488
-
489
-
490
- # Returns the network currently connected to, or nil if none.
491
- def current_network
492
- lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
493
- ssid_lines = lines.grep(/ SSID:/)
494
- ssid_lines.empty? ? nil : ssid_lines.first.split('SSID: ').last.strip
495
- end
496
-
497
-
498
- # Disconnects from the currently connected network. Does not turn off wifi.
499
- def disconnect
500
- run_os_command("sudo #{AIRPORT_CMD} -z")
501
- nil
502
- end
503
-
504
-
505
- # Returns some useful wifi-related information.
506
- def wifi_info
507
-
508
- info = {
509
- wifi_on: wifi_on?,
510
- internet_on: connected_to_internet?,
511
- vpn_on: vpn_running?,
512
- port: wifi_hardware_port,
513
- network: current_network,
514
- ip_address: ip_address,
515
- timestamp: Time.now,
516
- }
517
- more_output = run_os_command(AIRPORT_CMD + " -I")
518
- more_info = colon_output_to_hash(more_output)
519
- info.merge!(more_info)
520
- info.delete('AirPort') # will be here if off, but info is already in wifi_on key
521
- info
522
- end
523
-
524
-
525
- # Parses output like the text below into a hash:
526
- # SSID: Pattara211
527
- # MCS: 5
528
- # channel: 7
529
- def colon_output_to_hash(output)
530
- lines = output.split("\n")
531
- lines.each_with_object({}) do |line, new_hash|
532
- key, value = line.split(': ')
533
- key = key.strip
534
- value.strip! if value
535
- new_hash[key] = value
536
- end
537
- end
538
- private :colon_output_to_hash
539
- end
540
-
541
-
542
- class CommandLineInterface
543
-
544
- attr_reader :model, :interactive_mode, :options
545
-
546
- class Command < Struct.new(:min_string, :max_string, :action); end
547
-
548
-
549
- class BadCommandError < RuntimeError
550
- def initialize(error_message)
551
- super
552
- end
553
- end
554
-
555
-
556
- # Help text to be used when requested by 'h' command, in case of unrecognized or nonexistent command, etc.
557
- HELP_TEXT = "
558
- Command Line Switches: [mac-wifi version #{VERSION}]
559
-
560
- -o[i,j,p,y] - outputs data in inspect, JSON, puts, or YAML format when not in shell mode
561
- -s - run in shell mode
562
- -v - verbose mode (prints OS commands and their outputs)
563
-
564
- Commands:
565
-
566
- a[vail_nets] - array of names of the available networks
567
- ci - connected to Internet (not just wifi on)?
568
- co[nnect] network-name - turns wifi on, connects to network-name
569
- cy[cle] - turns wifi off, then on, preserving network selection
570
- d[isconnect] - disconnects from current network, does not turn off wifi
571
- h[elp] - prints this help
572
- i[nfo] - a hash of wifi-related information
573
- l[s_avail_nets] - details about available networks
574
- n[etwork_name] - name (SSID) of currently connected network
575
- on - turns wifi on
576
- of[f] - turns wifi off
577
- pa[ssword] network-name - password for preferred network-name
578
- pr[ef_nets] - preferred (not necessarily available) networks
579
- q[uit] - exits this program (interactive shell mode only) (see also 'x')
580
- r[m_pref_nets] network-name - removes network-name from the preferred networks list
581
- (can provide multiple names separated by spaces)
582
- s[hell] - opens an interactive pry shell (command line only)
583
- t[ill] - returns when the desired Internet connection state is true. Options:
584
- 1) 'on'/:on, 'off'/:off, 'conn'/:conn, or 'disc'/:disc
585
- 2) wait interval, in seconds (optional, defaults to 0.5 seconds)
586
- w[ifion] - is the wifi on?
587
- x[it] - exits this program (interactive shell mode only) (see also 'q')
588
-
589
- When in interactive shell mode:
590
- * use quotes for string parameters such as method names.
591
- * for pry commands, use prefix `%`.
592
-
593
- "
594
-
595
-
596
- def initialize(options)
597
- @options = options
598
- @model = MacOsModel.new(verbose_mode)
599
- @interactive_mode = !!(options.interactive_mode)
600
- run_shell if @interactive_mode
601
- end
602
-
603
-
604
- # Until command line option parsing is added, the only way to specify
605
- # verbose mode is in the environment variable MAC_WIFI_OPTS.
606
- def verbose_mode
607
- options.verbose
608
- end
609
-
610
-
611
- def print_help
612
- puts HELP_TEXT
613
- end
614
-
615
-
616
- # @return true if awesome_print is available (after requiring it), else false after requiring 'pp'.
617
- # We'd like to use awesome_print if it is available, but not require it.
618
- # So, we try to require it, but if that fails, we fall back to using pp (pretty print),
619
- # which is included in Ruby distributions without the need to install a gem.
620
- def awesome_print_available?
621
- if @awesome_print_available.nil? # first time here
622
- begin
623
- require 'awesome_print'
624
- @awesome_print_available = true
625
- rescue LoadError
626
- require 'pp'
627
- @awesome_print_available = false
628
- end
629
- end
630
-
631
- @awesome_print_available
632
- end
633
-
634
-
635
- def fancy_string(object)
636
- awesome_print_available? ? object.ai : object.pretty_inspect
637
- end
638
-
639
-
640
- def fancy_puts(object)
641
- puts fancy_string(object)
642
- end
643
- alias_method :fp, :fancy_puts
644
-
645
-
646
- def puts_unless_interactive(string)
647
- puts(string) unless interactive_mode
648
- end
649
-
650
-
651
- # Asserts that a command has been passed on the command line.
652
- def validate_command_line
653
- if ARGV.empty?
654
- puts "Syntax is: #{__FILE__} [options] command [command_options]"
655
- print_help
656
- exit(-1)
657
- end
658
- end
659
-
660
-
661
- # Pry will output the content of the method from which it was called.
662
- # This small method exists solely to reduce the amount of pry's output
663
- # that is not needed here.
664
- def run_pry
665
- binding.pry
666
-
667
- # the seemingly useless line below is needed to avoid pry's exiting
668
- # (see https://github.com/deivid-rodriguez/pry-byebug/issues/45)
669
- _a = nil
670
- end
671
-
672
-
673
- # Runs a pry session in the context of this object.
674
- # Commands and options specified on the command line can also be specified in the shell.
675
- def run_shell
676
- begin
677
- require 'pry'
678
- rescue LoadError
679
- puts "The 'pry' gem and/or one of its prerequisites, required for running the shell, was not found." +
680
- " Please `gem install pry`."
681
- exit(-1)
682
- end
683
-
684
- print_help
685
-
686
- # Enable the line below if you have any problems with pry configuration being loaded
687
- # that is messing up this runtime use of pry:
688
- # Pry.config.should_load_rc = false
689
-
690
- # Strangely, this is the only thing I have found that successfully suppresses the
691
- # code context output, which is not useful here. Anyway, this will differentiate
692
- # a pry command from a DSL command, which _is_ useful here.
693
- Pry.config.command_prefix = '%'
694
-
695
- run_pry
696
- end
697
-
698
-
699
- # For use by the shell; when typing a command and options, it is passed to process_command_line
700
- def method_missing(method_name, *options)
701
- method_name = method_name.to_s
702
- method_exists = !! find_command_action(method_name)
703
- if method_exists
704
- process_command_line(method_name, options)
705
- else
706
- puts(%Q{"#{method_name}" is not a valid command or option. If you intend for this to be a string literal, use quotes or %q/Q{}.})
707
- end
708
- end
709
-
710
-
711
- # Processes the command (ARGV[0]) and any relevant options (ARGV[1..-1]).
712
- #
713
- # CAUTION! In interactive mode, any strings entered (e.g. a network name) MUST
714
- # be in a form that the Ruby interpreter will recognize as a string,
715
- # i.e. single or double quotes, %q, %Q, etc.
716
- # Otherwise it will assume it's a method name and pass it to method_missing!
717
- def process_command_line(command, options)
718
- action = find_command_action(command)
719
- if action
720
- action.(*options)
721
- else
722
- print_help
723
- raise BadCommandError.new(
724
- %Q{Unrecognized command. Command was "#{command}" and options were #{options.inspect}.})
725
- end
726
- end
727
-
728
-
729
- def quit
730
- if interactive_mode
731
- exit(0)
732
- else
733
- puts "This command can only be run in shell mode."
734
- end
735
- end
736
-
737
-
738
- def cmd_a
739
- info = model.available_network_names
740
- if interactive_mode
741
- info
742
- else
743
- if post_processor
744
- puts post_processor.(info)
745
- else
746
- message = if model.wifi_on?
747
- "Available networks are:\n\n#{fancy_string(info)}"
748
- else
749
- "Wifi is off, cannot see available networks."
750
- end
751
- puts(message)
752
- end
753
- end
754
- end
755
-
756
-
757
- def cmd_ci
758
- connected = model.connected_to_internet?
759
- if interactive_mode
760
- connected
761
- else
762
- puts (post_processor ? post_processor.(connected) : "Connected to Internet: #{connected}")
763
- end
764
- end
765
-
766
-
767
- def cmd_co(network, password = nil)
768
- model.connect(network, password)
769
- end
770
-
771
-
772
- def cmd_cy
773
- model.cycle_network
774
- end
775
-
776
-
777
- def cmd_d
778
- model.disconnect
779
- end
780
-
781
-
782
- def cmd_h
783
- print_help
784
- end
785
-
786
-
787
- def cmd_i
788
- info = model.wifi_info
789
- if interactive_mode
790
- info
791
- else
792
- if post_processor
793
- puts post_processor.(info)
794
- else
795
- puts fancy_string(info)
796
- end
797
- end
798
- end
799
-
800
-
801
- def cmd_lsa
802
- info = model.available_network_info
803
- if interactive_mode
804
- info
805
- else
806
- if post_processor
807
- puts post_processor.(info)
808
- else
809
- message = model.wifi_on? ? fancy_string(info) : "Wifi is off, cannot see available networks."
810
- puts(message)
811
- end
812
- end
813
- end
814
-
815
-
816
- def cmd_n
817
- name = model.connected_network_name
818
- if interactive_mode
819
- name
820
- else
821
- display_name = name ? name : '[none]'
822
- puts (post_processor ? post_processor.(name) : %Q{Network (SSID) name: "#{display_name}"})
823
- end
824
- end
825
-
826
-
827
- def cmd_of
828
- model.wifi_off
829
- end
830
-
831
-
832
- def cmd_on
833
- model.wifi_on
834
- end
835
-
836
-
837
- def cmd_pa(network)
838
- password = model.preferred_network_password(network)
839
-
840
- if interactive_mode
841
- password
842
- else
843
- if post_processor
844
- puts post_processor.(password)
845
- else
846
- output = %Q{Preferred network "#{model.connected_network_name}" }
847
- output << (password ? %Q{stored password is "#{password}".} : "has no stored password.")
848
- puts output
849
- end
850
- end
851
- end
852
-
853
-
854
- def cmd_pr
855
- networks = model.preferred_networks
856
- if interactive_mode
857
- networks
858
- else
859
- puts (post_processor ? post_processor.(networks) : fancy_string(networks))
860
- end
861
- end
862
-
863
-
864
- def cmd_q
865
- quit
866
- end
867
-
868
-
869
- def cmd_r(*options)
870
- removed_networks = model.remove_preferred_networks(*options)
871
- if interactive_mode
872
- removed_networks
873
- else
874
- puts (post_processor ? post_processor.(removed_networks) : "Removed networks: #{removed_networks.inspect}")
875
- end
876
- end
877
-
878
-
879
- def cmd_t(*options)
880
- target_status = options[0].to_sym
881
- wait_interval_in_secs = (options[1] ? Float(options[1]) : nil)
882
- model.till(target_status, wait_interval_in_secs)
883
- end
884
-
885
-
886
- def cmd_w
887
- on = model.wifi_on?
888
- if interactive_mode
889
- on
890
- else
891
- puts (post_processor ? post_processor.(on) : "Wifi on: #{on}")
892
- end
893
- end
894
-
895
-
896
- def cmd_x
897
- quit
898
- end
899
-
900
-
901
- def commands
902
- @commands_ ||= [
903
- Command.new('a', 'avail_nets', -> (*_options) { cmd_a }),
904
- Command.new('ci', 'ci', -> (*_options) { cmd_ci }),
905
- Command.new('co', 'connect', -> (*options) { cmd_co(*options) }),
906
- Command.new('cy', 'cycle', -> (*_options) { cmd_cy }),
907
- Command.new('d', 'disconnect', -> (*_options) { cmd_d }),
908
- Command.new('h', 'help', -> (*_options) { cmd_h }),
909
- Command.new('i', 'info', -> (*_options) { cmd_i }),
910
- Command.new('l', 'ls_avail_nets', -> (*_options) { cmd_lsa }),
911
- Command.new('n', 'network_name', -> (*_options) { cmd_n }),
912
- Command.new('of', 'off', -> (*_options) { cmd_of }),
913
- Command.new('on', 'on', -> (*_options) { cmd_on }),
914
- Command.new('pa', 'password', -> (*options) { cmd_pa(*options) }),
915
- Command.new('pr', 'pref_nets', -> (*_options) { cmd_pr }),
916
- Command.new('q', 'quit', -> (*_options) { cmd_q }),
917
- Command.new('r', 'rm_pref_nets', -> (*options) { cmd_r(*options) }),
918
- Command.new('t', 'till', -> (*options) { cmd_t(*options) }),
919
- Command.new('w', 'wifion', -> (*_options) { cmd_w }),
920
- Command.new('x', 'xit', -> (*_options) { cmd_x })
921
- ]
922
- end
923
-
924
-
925
- def find_command_action(command_string)
926
- result = commands.detect do |cmd|
927
- cmd.max_string.start_with?(command_string) \
928
- && \
929
- command_string.length >= cmd.min_string.length # e.g. 'c' by itself should not work
930
- end
931
-
932
- result ? result.action : nil
933
- end
934
-
935
-
936
- # If a post-processor has been configured (e.g. YAML or JSON), use it.
937
- def post_process(object)
938
- post_processor ? post_processor.(object) : object
939
- end
940
-
941
-
942
-
943
- def post_processor
944
- options.post_processor
945
- end
946
-
947
-
948
- def call
949
- validate_command_line
950
- begin
951
- process_command_line(ARGV[0], ARGV[1..-1])
952
- rescue BadCommandError => error
953
- separator_line = "! #{'-' * 75} !\n"
954
- puts '' << separator_line << error.to_s << "\n" << separator_line
955
- exit(-1)
956
- end
957
- end
958
- end
959
-
960
-
961
- require 'optparse'
962
- require 'ostruct'
963
-
964
- class Main
965
-
966
- # @return true if this file is being run as a script, else false
967
- #
968
- # This file could be called as a script in either of these two ways:
969
- #
970
- # 1) by loading this file directly, or
971
- # 2) by running as a gem executable's binstub, in (relatively) '../../../exe'
972
- #
973
- # For example, I get this when running this file as a gem and running:
974
- #
975
- # puts "$0: #{$0}"
976
- # puts "__FILE__: #{__FILE__}"
977
- # puts "GEM_PATH: #{ENV['GEM_PATH']}"
978
- #
979
- # $0: /Users/kbennett/.rvm/gems/ruby-2.4.1/bin/mac-wifi
980
- # __FILE__: /Users/kbennett/.rvm/gems/ruby-2.4.1/gems/mac-wifi-1.2.0/exe/mac-wifi
981
- # GEM_PATH: /Users/kbennett/.rvm/gems/ruby-2.4.1:/Users/kbennett/.rvm/gems/ruby-2.4.1@global
982
-
983
-
984
- def running_as_script?
985
-
986
- this_source_file = File.expand_path(__FILE__)
987
- called_file = File.expand_path($0)
988
- return true if called_file == this_source_file
989
-
990
- this_source_file_basename = File.basename(this_source_file)
991
- called_file_basename = File.basename(called_file)
992
- return false if called_file_basename != this_source_file_basename
993
-
994
- # If here, then filespecs are different but have the same basename.
995
- gem_roots = ENV['GEM_PATH'].split(File::PATH_SEPARATOR)
996
- as_script = gem_roots.any? do |gem_root|
997
- binstub_candidate_name = File.join(gem_root, 'bin', called_file_basename)
998
- up_4_from_this_source_file = File.expand_path(File.join(this_source_file, '..', '..', '..', '..'))
999
- found = ($0 == binstub_candidate_name) && (gem_root == up_4_from_this_source_file)
1000
- found
1001
- end
1002
-
1003
- as_script
1004
- end
1005
-
1006
-
1007
- def assert_os_is_mac_os
1008
- host_os = RbConfig::CONFIG["host_os"]
1009
- unless /darwin/.match(host_os) # e.g. "darwin16.4.0"
1010
- raise "This program currently works only on Mac OS. Platform is '#{host_os}'."
1011
- end
1012
- end
1013
-
1014
-
1015
- # Parses the command line with Ruby's internal 'optparse'.
1016
- # Looks for "-v" flag to set verbosity to true.
1017
- # optparse removes what it processes from ARGV, which simplifies our command parsing.
1018
- def parse_command_line
1019
- options = OpenStruct.new
1020
- OptionParser.new do |parser|
1021
- parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
1022
- options.verbose = v
1023
- end
1024
-
1025
- parser.on("-s", "--shell", "Start interactive shell") do |v|
1026
- options.interactive_mode = true
1027
- end
1028
-
1029
- parser.on("-o", "--output_format FORMAT", "Format output data") do |v|
1030
-
1031
- transformers = {
1032
- 'i' => ->(object) { object.inspect },
1033
- 'j' => ->(object) { JSON.pretty_generate(object) },
1034
- 'p' => ->(object) { sio = StringIO.new; sio.puts(object); sio.string },
1035
- 'y' => ->(object) { object.to_yaml }
1036
- }
1037
-
1038
- choice = v[0].downcase
1039
-
1040
- unless transformers.keys.include?(choice)
1041
- raise %Q{Output format "#{choice}" not in list of available formats} +
1042
- " (#{transformers.keys.inspect})."
1043
- end
1044
-
1045
- options.post_processor = transformers[choice]
1046
- end
1047
-
1048
- parser.on("-h", "--help", "Show help") do |_help_requested|
1049
- ARGV << 'h' # pass on the request to the command processor
1050
- end
1051
- end.parse!
1052
- options
1053
- end
1054
-
1055
-
1056
- def call
1057
- assert_os_is_mac_os
1058
-
1059
- options = parse_command_line
1060
-
1061
- # If this file is being called as a script, run it.
1062
- # Else, it may be loaded to use the model in a different way.
1063
- if running_as_script?
1064
- begin
1065
- MacWifi::CommandLineInterface.new(options).call
1066
- end
1067
- end
1068
- end
1069
- end
1070
-
1071
-
1072
- end # module MacWifi
1073
-
22
+ require_relative '../lib/mac-wifi/main'
1074
23
 
1075
24
  MacWifi::Main.new.call