mac-wifi 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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