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.
@@ -0,0 +1,221 @@
1
+ require 'tempfile'
2
+ require_relative '../mac-wifi'
3
+
4
+ module MacWifi
5
+
6
+ class BaseModel
7
+
8
+ class OsCommandError < RuntimeError
9
+ attr_reader :exitstatus, :command, :text
10
+
11
+ def initialize(exitstatus, command, text)
12
+ @exitstatus = exitstatus
13
+ @command = command
14
+ @text = text
15
+ end
16
+ end
17
+
18
+
19
+ def initialize(verbose = false)
20
+ @verbose_mode = verbose
21
+ end
22
+
23
+
24
+ def run_os_command(command)
25
+ output = `#{command} 2>&1` # join stderr with stdout
26
+ if $?.exitstatus != 0
27
+ raise OsCommandError.new($?.exitstatus, command, output)
28
+ end
29
+ if @verbose_mode
30
+ puts "\n\n#{'-' * 79}\nCommand: #{command}\n\nOutput:\n#{output}#{'-' * 79}\n\n"
31
+ end
32
+ output
33
+ end
34
+ private :run_os_command
35
+
36
+
37
+ # This method returns whether or not there is a working Internet connection.
38
+ # Because of a Mac issue which causes a request to hang if the network is turned
39
+ # off during its lifetime, we give it only 5 seconds per try,
40
+ # and limit the number of tries to 3.
41
+ #
42
+ # This implementation will probably strike you as overly complex. The following
43
+ # code looks like it is all that should be necessary, but unfortunately
44
+ # this implementation often hangs when wifi is turned off while curl is active
45
+ #
46
+ # def connected_to_internet?
47
+ # script = "curl --silent --head http://www.google.com/ > /dev/null ; echo $?"
48
+ # result = `#{script}`.chomp
49
+ # puts result
50
+ # result == '0'
51
+ # end
52
+
53
+ # TODO Investigate using Curl options: --connect-timeout 1 --max-time 2 --retry 0
54
+ # to greatly simplify this method.
55
+ def connected_to_internet?
56
+
57
+ tempfile = Tempfile.open('mac-wifi-')
58
+
59
+ begin
60
+ start_status_script = -> do
61
+ script = "curl --silent --head http://www.google.com/ > /dev/null ; echo $? > #{tempfile.path} &"
62
+ pid = Process.spawn(script)
63
+ Process.detach(pid)
64
+ pid
65
+ end
66
+
67
+ process_is_running = ->(pid) do
68
+ script = %Q{ps -p #{pid} > /dev/null; echo $?}
69
+ output = `#{script}`.chomp
70
+ output == "0"
71
+ end
72
+
73
+ get_connected_state_from_curl = -> do
74
+ tempfile.close
75
+ File.read(tempfile.path).chomp == '0'
76
+ end
77
+
78
+ # Do one run, iterating during the timeout period to see if the command has completed
79
+ do_one_run = -> do
80
+ end_time = Time.now + 3
81
+ pid = start_status_script.()
82
+ while Time.now < end_time
83
+ if process_is_running.(pid)
84
+ sleep 0.5
85
+ else
86
+ return get_connected_state_from_curl.()
87
+ end
88
+ end
89
+ Process.kill('KILL', pid) if process_is_running.(pid)
90
+ :hung
91
+ end
92
+
93
+ 3.times do
94
+ connected = do_one_run.()
95
+ return connected if connected != :hung
96
+ end
97
+
98
+ raise "Could not determine Internet status."
99
+
100
+ ensure
101
+ tempfile.unlink
102
+ end
103
+
104
+ end
105
+
106
+
107
+ # Turns wifi off and then on, reconnecting to the originally connecting network.
108
+ def cycle_network
109
+ # TODO: Make this network name saving and restoring conditional on it not having a password.
110
+ # If the disabled code below is enabled, an error will be raised if a password is required,
111
+ # even though it is stored.
112
+ # network_name = current_network
113
+ wifi_off
114
+ wifi_on
115
+ # connect(network_name) if network_name
116
+ end
117
+
118
+
119
+ def connected_to?(network_name)
120
+ network_name == connected_network_name
121
+ end
122
+
123
+
124
+ # Connects to the passed network name, optionally with password.
125
+ # Turns wifi on first, in case it was turned off.
126
+ # Relies on subclass implementation of os_level_connect().
127
+ def connect(network_name, password = nil)
128
+ # Allow symbols and anything responding to to_s for user convenience
129
+ network_name = network_name.to_s if network_name
130
+ password = password.to_s if password
131
+
132
+ if network_name.nil? || network_name.empty?
133
+ raise "A network name is required but was not provided."
134
+ end
135
+ wifi_on
136
+ os_level_connect(network_name, password)
137
+
138
+ # Verify that the network is now connected:
139
+ actual_network_name = connected_network_name
140
+ unless actual_network_name == network_name
141
+ message = %Q{Expected to connect to "#{network_name}" but }
142
+ if actual_network_name
143
+ message << %Q{connected to "#{connected_network_name}" instead.}
144
+ else
145
+ message << "unable to connect to any network. Did you "
146
+ end
147
+ message << (password ? "provide the correct password?" : "need to provide a password?")
148
+ raise message
149
+ end
150
+ nil
151
+ end
152
+
153
+
154
+ # Removes the specified network(s) from the preferred network list.
155
+ # @param network_names names of networks to remove; may be empty or contain nonexistent networks
156
+ # @return names of the networks that were removed (excludes non-preexisting networks)
157
+ def remove_preferred_networks(*network_names)
158
+ networks_to_remove = network_names & preferred_networks # exclude any nonexistent networks
159
+ networks_to_remove.each { |name| remove_preferred_network(name) }
160
+ end
161
+
162
+
163
+ def preferred_network_password(preferred_network_name)
164
+ preferred_network_name = preferred_network_name.to_s
165
+ if preferred_networks.include?(preferred_network_name)
166
+ os_level_preferred_network_password(preferred_network_name)
167
+ else
168
+ raise "Network #{preferred_network_name} not in preferred networks list."
169
+ end
170
+ end
171
+
172
+
173
+ # Waits for the Internet connection to be in the desired state.
174
+ # @param target_status must be in [:conn, :disc, :off, :on]; waits for that state
175
+ # @param wait_interval_in_secs sleeps this interval between retries; if nil or absent,
176
+ # a default will be provided
177
+ #
178
+ def till(target_status, wait_interval_in_secs = nil)
179
+
180
+ # One might ask, why not just put the 0.5 up there as the default argument.
181
+ # We could do that, but we'd still need the line below in case nil
182
+ # was explicitly specified. The default argument of nil above emphasizes that
183
+ # the absence of an argument and a specification of nil will behave identically.
184
+ wait_interval_in_secs ||= 0.5
185
+
186
+ finished_predicates = {
187
+ conn: -> { connected_to_internet? },
188
+ disc: -> { ! connected_to_internet? },
189
+ on: -> { wifi_on? },
190
+ off: -> { ! wifi_on? }
191
+ }
192
+
193
+ finished_predicate = finished_predicates[target_status]
194
+
195
+ if finished_predicate.nil?
196
+ raise ArgumentError.new(
197
+ "Option must be one of #{finished_predicates.keys.inspect}. Was: #{target_status.inspect}")
198
+ end
199
+
200
+ loop do
201
+ return if finished_predicate.()
202
+ sleep(wait_interval_in_secs)
203
+ end
204
+ end
205
+
206
+
207
+ # Tries an OS command until the stop condition is true.
208
+ # @command the command to run in the OS
209
+ # @stop_condition a lambda taking the commands stdout as its sole parameter
210
+ # @return the stdout produced by the command
211
+ def try_os_command_until(command, stop_condition, max_tries = 100)
212
+ max_tries.times do
213
+ stdout = run_os_command(command)
214
+ if stop_condition.(stdout)
215
+ return stdout
216
+ end
217
+ end
218
+ nil
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,417 @@
1
+ require_relative 'mac_os_model'
2
+
3
+ module MacWifi
4
+
5
+ class CommandLineInterface
6
+
7
+ attr_reader :model, :interactive_mode, :options
8
+
9
+ class Command < Struct.new(:min_string, :max_string, :action); end
10
+
11
+
12
+ class BadCommandError < RuntimeError
13
+ def initialize(error_message)
14
+ super
15
+ end
16
+ end
17
+
18
+
19
+ # Help text to be used when requested by 'h' command, in case of unrecognized or nonexistent command, etc.
20
+ HELP_TEXT = "
21
+ Command Line Switches: [mac-wifi version #{MacWifi::VERSION}]
22
+
23
+ -o[i,j,p,y] - outputs data in inspect, JSON, puts, or YAML format when not in shell mode
24
+ -s - run in shell mode
25
+ -v - verbose mode (prints OS commands and their outputs)
26
+
27
+ Commands:
28
+
29
+ a[vail_nets] - array of names of the available networks
30
+ ci - connected to Internet (not just wifi on)?
31
+ co[nnect] network-name - turns wifi on, connects to network-name
32
+ cy[cle] - turns wifi off, then on, preserving network selection
33
+ d[isconnect] - disconnects from current network, does not turn off wifi
34
+ h[elp] - prints this help
35
+ i[nfo] - a hash of wifi-related information
36
+ l[s_avail_nets] - details about available networks
37
+ n[etwork_name] - name (SSID) of currently connected network
38
+ on - turns wifi on
39
+ of[f] - turns wifi off
40
+ pa[ssword] network-name - password for preferred network-name
41
+ pr[ef_nets] - preferred (not necessarily available) networks
42
+ q[uit] - exits this program (interactive shell mode only) (see also 'x')
43
+ r[m_pref_nets] network-name - removes network-name from the preferred networks list
44
+ (can provide multiple names separated by spaces)
45
+ s[hell] - opens an interactive pry shell (command line only)
46
+ t[ill] - returns when the desired Internet connection state is true. Options:
47
+ 1) 'on'/:on, 'off'/:off, 'conn'/:conn, or 'disc'/:disc
48
+ 2) wait interval, in seconds (optional, defaults to 0.5 seconds)
49
+ w[ifion] - is the wifi on?
50
+ x[it] - exits this program (interactive shell mode only) (see also 'q')
51
+
52
+ When in interactive shell mode:
53
+ * use quotes for string parameters such as method names.
54
+ * for pry commands, use prefix `%`.
55
+
56
+ "
57
+
58
+
59
+ def initialize(options)
60
+ @options = options
61
+ @model = MacOsModel.new(verbose_mode)
62
+ @interactive_mode = !!(options.interactive_mode)
63
+ run_shell if @interactive_mode
64
+ end
65
+
66
+
67
+ # Until command line option parsing is added, the only way to specify
68
+ # verbose mode is in the environment variable MAC_WIFI_OPTS.
69
+ def verbose_mode
70
+ options.verbose
71
+ end
72
+
73
+
74
+ def print_help
75
+ puts HELP_TEXT
76
+ end
77
+
78
+
79
+ # @return true if awesome_print is available (after requiring it), else false after requiring 'pp'.
80
+ # We'd like to use awesome_print if it is available, but not require it.
81
+ # So, we try to require it, but if that fails, we fall back to using pp (pretty print),
82
+ # which is included in Ruby distributions without the need to install a gem.
83
+ def awesome_print_available?
84
+ if @awesome_print_available.nil? # first time here
85
+ begin
86
+ require 'awesome_print'
87
+ @awesome_print_available = true
88
+ rescue LoadError
89
+ require 'pp'
90
+ @awesome_print_available = false
91
+ end
92
+ end
93
+
94
+ @awesome_print_available
95
+ end
96
+
97
+
98
+ def fancy_string(object)
99
+ awesome_print_available? ? object.ai : object.pretty_inspect
100
+ end
101
+
102
+
103
+ def fancy_puts(object)
104
+ puts fancy_string(object)
105
+ end
106
+ alias_method :fp, :fancy_puts
107
+
108
+
109
+ # Asserts that a command has been passed on the command line.
110
+ def validate_command_line
111
+ if ARGV.empty?
112
+ puts "Syntax is: #{__FILE__} [options] command [command_options]"
113
+ print_help
114
+ exit(-1)
115
+ end
116
+ end
117
+
118
+
119
+ # Pry will output the content of the method from which it was called.
120
+ # This small method exists solely to reduce the amount of pry's output
121
+ # that is not needed here.
122
+ def run_pry
123
+ binding.pry
124
+
125
+ # the seemingly useless line below is needed to avoid pry's exiting
126
+ # (see https://github.com/deivid-rodriguez/pry-byebug/issues/45)
127
+ _a = nil
128
+ end
129
+
130
+
131
+ # Runs a pry session in the context of this object.
132
+ # Commands and options specified on the command line can also be specified in the shell.
133
+ def run_shell
134
+ begin
135
+ require 'pry'
136
+ rescue LoadError
137
+ puts "The 'pry' gem and/or one of its prerequisites, required for running the shell, was not found." +
138
+ " Please `gem install pry`."
139
+ exit(-1)
140
+ end
141
+
142
+ print_help
143
+
144
+ # Enable the line below if you have any problems with pry configuration being loaded
145
+ # that is messing up this runtime use of pry:
146
+ # Pry.config.should_load_rc = false
147
+
148
+ # Strangely, this is the only thing I have found that successfully suppresses the
149
+ # code context output, which is not useful here. Anyway, this will differentiate
150
+ # a pry command from a DSL command, which _is_ useful here.
151
+ Pry.config.command_prefix = '%'
152
+
153
+ run_pry
154
+ end
155
+
156
+
157
+ # For use by the shell; when typing a command and options, it is passed to process_command_line
158
+ def method_missing(method_name, *options)
159
+ method_name = method_name.to_s
160
+ method_exists = !! find_command_action(method_name)
161
+ if method_exists
162
+ process_command_line(method_name, options)
163
+ else
164
+ 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{}.})
165
+ end
166
+ end
167
+
168
+
169
+ # Processes the command (ARGV[0]) and any relevant options (ARGV[1..-1]).
170
+ #
171
+ # CAUTION! In interactive mode, any strings entered (e.g. a network name) MUST
172
+ # be in a form that the Ruby interpreter will recognize as a string,
173
+ # i.e. single or double quotes, %q, %Q, etc.
174
+ # Otherwise it will assume it's a method name and pass it to method_missing!
175
+ def process_command_line(command, options)
176
+ action = find_command_action(command)
177
+ if action
178
+ action.(*options)
179
+ else
180
+ print_help
181
+ raise BadCommandError.new(
182
+ %Q{Unrecognized command. Command was "#{command}" and options were #{options.inspect}.})
183
+ end
184
+ end
185
+
186
+
187
+ def quit
188
+ if interactive_mode
189
+ exit(0)
190
+ else
191
+ puts "This command can only be run in shell mode."
192
+ end
193
+ end
194
+
195
+
196
+ def cmd_a
197
+ info = model.available_network_names
198
+ if interactive_mode
199
+ info
200
+ else
201
+ if post_processor
202
+ puts post_processor.(info)
203
+ else
204
+ message = if model.wifi_on?
205
+ "Available networks are:\n\n#{fancy_string(info)}"
206
+ else
207
+ "Wifi is off, cannot see available networks."
208
+ end
209
+ puts(message)
210
+ end
211
+ end
212
+ end
213
+
214
+
215
+ def cmd_ci
216
+ connected = model.connected_to_internet?
217
+ if interactive_mode
218
+ connected
219
+ else
220
+ puts (post_processor ? post_processor.(connected) : "Connected to Internet: #{connected}")
221
+ end
222
+ end
223
+
224
+
225
+ def cmd_co(network, password = nil)
226
+ model.connect(network, password)
227
+ end
228
+
229
+
230
+ def cmd_cy
231
+ model.cycle_network
232
+ end
233
+
234
+
235
+ def cmd_d
236
+ model.disconnect
237
+ end
238
+
239
+
240
+ def cmd_h
241
+ print_help
242
+ end
243
+
244
+
245
+ def cmd_i
246
+ info = model.wifi_info
247
+ if interactive_mode
248
+ info
249
+ else
250
+ if post_processor
251
+ puts post_processor.(info)
252
+ else
253
+ puts fancy_string(info)
254
+ end
255
+ end
256
+ end
257
+
258
+
259
+ def cmd_lsa
260
+ info = model.available_network_info
261
+ if interactive_mode
262
+ info
263
+ else
264
+ if post_processor
265
+ puts post_processor.(info)
266
+ else
267
+ message = model.wifi_on? ? fancy_string(info) : "Wifi is off, cannot see available networks."
268
+ puts(message)
269
+ end
270
+ end
271
+ end
272
+
273
+
274
+ def cmd_n
275
+ name = model.connected_network_name
276
+ if interactive_mode
277
+ name
278
+ else
279
+ display_name = name ? name : '[none]'
280
+ puts (post_processor ? post_processor.(name) : %Q{Network (SSID) name: "#{display_name}"})
281
+ end
282
+ end
283
+
284
+
285
+ def cmd_of
286
+ model.wifi_off
287
+ end
288
+
289
+
290
+ def cmd_on
291
+ model.wifi_on
292
+ end
293
+
294
+
295
+ def cmd_pa(network)
296
+ password = model.preferred_network_password(network)
297
+
298
+ if interactive_mode
299
+ password
300
+ else
301
+ if post_processor
302
+ puts post_processor.(password)
303
+ else
304
+ output = %Q{Preferred network "#{model.connected_network_name}" }
305
+ output << (password ? %Q{stored password is "#{password}".} : "has no stored password.")
306
+ puts output
307
+ end
308
+ end
309
+ end
310
+
311
+
312
+ def cmd_pr
313
+ networks = model.preferred_networks
314
+ if interactive_mode
315
+ networks
316
+ else
317
+ puts (post_processor ? post_processor.(networks) : fancy_string(networks))
318
+ end
319
+ end
320
+
321
+
322
+ def cmd_q
323
+ quit
324
+ end
325
+
326
+
327
+ def cmd_r(*options)
328
+ removed_networks = model.remove_preferred_networks(*options)
329
+ if interactive_mode
330
+ removed_networks
331
+ else
332
+ puts (post_processor ? post_processor.(removed_networks) : "Removed networks: #{removed_networks.inspect}")
333
+ end
334
+ end
335
+
336
+
337
+ def cmd_t(*options)
338
+ target_status = options[0].to_sym
339
+ wait_interval_in_secs = (options[1] ? Float(options[1]) : nil)
340
+ model.till(target_status, wait_interval_in_secs)
341
+ end
342
+
343
+
344
+ def cmd_w
345
+ on = model.wifi_on?
346
+ if interactive_mode
347
+ on
348
+ else
349
+ puts (post_processor ? post_processor.(on) : "Wifi on: #{on}")
350
+ end
351
+ end
352
+
353
+
354
+ def cmd_x
355
+ quit
356
+ end
357
+
358
+
359
+ def commands
360
+ @commands_ ||= [
361
+ Command.new('a', 'avail_nets', -> (*_options) { cmd_a }),
362
+ Command.new('ci', 'ci', -> (*_options) { cmd_ci }),
363
+ Command.new('co', 'connect', -> (*options) { cmd_co(*options) }),
364
+ Command.new('cy', 'cycle', -> (*_options) { cmd_cy }),
365
+ Command.new('d', 'disconnect', -> (*_options) { cmd_d }),
366
+ Command.new('h', 'help', -> (*_options) { cmd_h }),
367
+ Command.new('i', 'info', -> (*_options) { cmd_i }),
368
+ Command.new('l', 'ls_avail_nets', -> (*_options) { cmd_lsa }),
369
+ Command.new('n', 'network_name', -> (*_options) { cmd_n }),
370
+ Command.new('of', 'off', -> (*_options) { cmd_of }),
371
+ Command.new('on', 'on', -> (*_options) { cmd_on }),
372
+ Command.new('pa', 'password', -> (*options) { cmd_pa(*options) }),
373
+ Command.new('pr', 'pref_nets', -> (*_options) { cmd_pr }),
374
+ Command.new('q', 'quit', -> (*_options) { cmd_q }),
375
+ Command.new('r', 'rm_pref_nets', -> (*options) { cmd_r(*options) }),
376
+ Command.new('t', 'till', -> (*options) { cmd_t(*options) }),
377
+ Command.new('w', 'wifion', -> (*_options) { cmd_w }),
378
+ Command.new('x', 'xit', -> (*_options) { cmd_x })
379
+ ]
380
+ end
381
+
382
+
383
+ def find_command_action(command_string)
384
+ result = commands.detect do |cmd|
385
+ cmd.max_string.start_with?(command_string) \
386
+ && \
387
+ command_string.length >= cmd.min_string.length # e.g. 'c' by itself should not work
388
+ end
389
+
390
+ result ? result.action : nil
391
+ end
392
+
393
+
394
+ # If a post-processor has been configured (e.g. YAML or JSON), use it.
395
+ def post_process(object)
396
+ post_processor ? post_processor.(object) : object
397
+ end
398
+
399
+
400
+
401
+ def post_processor
402
+ options.post_processor
403
+ end
404
+
405
+
406
+ def call
407
+ validate_command_line
408
+ begin
409
+ process_command_line(ARGV[0], ARGV[1..-1])
410
+ rescue BadCommandError => error
411
+ separator_line = "! #{'-' * 75} !\n"
412
+ puts '' << separator_line << error.to_s << "\n" << separator_line
413
+ exit(-1)
414
+ end
415
+ end
416
+ end
417
+ end