run_loop 1.2.6 → 1.2.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ module RunLoop
2
+ class Environment
3
+
4
+ # Returns the user's Unix uid.
5
+ # @return [Integer] The user's Unix uid as an integer.
6
+ def self.uid
7
+ `id -u`.strip.to_i
8
+ end
9
+
10
+ # Returns true if debugging is enabled.
11
+ def self.debug?
12
+ ENV['DEBUG'] == '1'
13
+ end
14
+ end
15
+ end
@@ -23,7 +23,8 @@ module RunLoop
23
23
  # The directory where the cache is stored.
24
24
  # @return [String] Expanded path to the default cache directory.
25
25
  def self.default_directory
26
- File.expand_path('/tmp/run-loop-host-cache')
26
+ uid = RunLoop::Environment.uid
27
+ File.expand_path("/tmp/com.xamarin.calabash.run-loop/host-cache/#{uid}")
27
28
  end
28
29
 
29
30
  # The default cache.
@@ -3,8 +3,6 @@ module RunLoop
3
3
  # A class for interacting with the instruments command-line tool
4
4
  #
5
5
  # @note All instruments commands are run in the context of `xcrun`.
6
- #
7
- # @todo Detect Instruments.app is running and pop an alert.
8
6
  class Instruments
9
7
 
10
8
  # Returns an Array of instruments process ids.
@@ -38,31 +36,11 @@ module RunLoop
38
36
  # what version of Xcode is active.
39
37
  def kill_instruments(xcode_tools = RunLoop::XCTools.new)
40
38
  kill_signal = kill_signal xcode_tools
41
- # It is difficult to test using a block.
42
39
  instruments_pids.each do |pid|
43
- begin
44
- if ENV['DEBUG'] == '1' or ENV['DEBUG_UNIX_CALLS'] == '1'
45
- puts "Sending '#{kill_signal}' to instruments process '#{pid}'"
46
- end
47
- Process.kill(kill_signal, pid.to_i)
48
- Process.wait(pid, Process::WNOHANG)
49
- rescue Exception => e
50
- if ENV['DEBUG'] == '1' or ENV['DEBUG_UNIX'] == '1'
51
- puts "Could not kill and wait for process '#{pid.to_i}' - ignoring exception '#{e}'"
52
- end
53
- end
54
-
55
- # Process.wait or `wait` here is pointless. The pid may or may not be
56
- # a child of this Process.
57
- begin
58
- if ENV['DEBUG'] == '1' or ENV['DEBUG_UNIX_CALLS'] == '1'
59
- puts "Waiting for instruments '#{pid}' to terminate"
60
- end
61
- wait_for_process_to_terminate(pid, {:timeout => 2.0})
62
- rescue Exception => e
63
- if ENV['DEBUG'] == '1' or ENV['DEBUG_UNIX_CALLS'] == '1'
64
- puts "Ignoring #{e.message}"
65
- end
40
+ terminator = RunLoop::ProcessTerminator.new(pid, kill_signal, 'instruments')
41
+ unless terminator.kill_process
42
+ terminator = RunLoop::ProcessTerminator.new(pid, 'KILL', 'instruments')
43
+ terminator.kill_process
66
44
  end
67
45
  end
68
46
  end
@@ -80,8 +58,59 @@ module RunLoop
80
58
  end
81
59
  end
82
60
 
61
+ # Spawn a new instruments process in the context of `xcrun` and detach.
62
+ #
63
+ # @param [String] automation_template The template instruments will use when
64
+ # launching the application.
65
+ # @param [Hash] options The launch options.
66
+ # @param [String] log_file The file to log to.
67
+ # @return [Integer] Returns the process id of the instruments process.
68
+ # @todo Do I need to enumerate the launch options in the docs?
69
+ # @todo Should this raise errors?
70
+ # @todo Is this jruby compatible?
71
+ def spawn(automation_template, options, log_file)
72
+ splat_args = spawn_arguments(automation_template, options)
73
+ if ENV['DEBUG'] == '1'
74
+ puts "#{Time.now} xcrun #{splat_args.join(' ')} >& #{log_file}"
75
+ $stdout.flush
76
+ end
77
+ pid = Process.spawn('xcrun', *splat_args, {:out => log_file, :err => log_file})
78
+ Process.detach(pid)
79
+ pid.to_i
80
+ end
81
+
83
82
  private
84
83
 
84
+ # @!visibility private
85
+ # Parses the run-loop options hash into an array of arguments that can be
86
+ # passed to `Process.spawn` to launch instruments.
87
+ def spawn_arguments(automation_template, options)
88
+ array = ['instruments']
89
+ array << '-w'
90
+ array << options[:udid]
91
+
92
+ trace = options[:results_dir_trace]
93
+ if trace
94
+ array << '-D'
95
+ array << trace
96
+ end
97
+
98
+ array << '-t'
99
+ array << automation_template
100
+
101
+ array << options[:bundle_dir_or_bundle_id]
102
+
103
+ {
104
+ 'UIARESULTSPATH' => options[:results_dir],
105
+ 'UIASCRIPT' => options[:script]
106
+ }.each do |key, value|
107
+ array << '-e'
108
+ array << key
109
+ array << value
110
+ end
111
+ array + options.fetch(:args, [])
112
+ end
113
+
85
114
  # @!visibility private
86
115
  #
87
116
  # ```
@@ -96,7 +125,7 @@ module RunLoop
96
125
  # $ ps x -o pid,command | grep -v grep | grep instruments
97
126
  # 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args >
98
127
  # ```
99
- FIND_PIDS_CMD = 'ps x -o pid,command | grep -v grep | grep instruments'
128
+ INSTRUMENTS_FIND_PIDS_CMD = 'ps x -o pid,command | grep -v grep | grep instruments'
100
129
 
101
130
  # @!visibility private
102
131
  #
@@ -106,7 +135,7 @@ module RunLoop
106
135
  # processes.
107
136
  # @return [String] A ps-style list of process details. The details returned
108
137
  # are controlled by the `ps_cmd`.
109
- def ps_for_instruments(ps_cmd=FIND_PIDS_CMD)
138
+ def ps_for_instruments(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
110
139
  `#{ps_cmd}`.strip
111
140
  end
112
141
 
@@ -117,8 +146,7 @@ module RunLoop
117
146
  # @return [Boolean] True if the details describe an instruments process.
118
147
  def is_instruments_process?(ps_details)
119
148
  return false if ps_details.nil?
120
- (ps_details[/\/usr\/bin\/instruments/, 0] or
121
- ps_details[/sh -c xcrun instruments/, 0]) != nil
149
+ ps_details[/\/usr\/bin\/instruments/, 0] != nil
122
150
  end
123
151
 
124
152
  # @!visibility private
@@ -129,7 +157,7 @@ module RunLoop
129
157
  # processes.
130
158
  # @return [Array<Integer>] An array of integer pids for instruments
131
159
  # processes. Returns an empty list if no instruments process are found.
132
- def pids_from_ps_output(ps_cmd=FIND_PIDS_CMD)
160
+ def pids_from_ps_output(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
133
161
  ps_output = ps_for_instruments(ps_cmd)
134
162
  lines = ps_output.lines("\n").map { |line| line.strip }
135
163
  lines.map do |line|
@@ -166,39 +194,5 @@ module RunLoop
166
194
  def kill_signal(xcode_tools = RunLoop::XCTools.new)
167
195
  xcode_tools.xcode_version_gte_6? ? 'QUIT' : 'TERM'
168
196
  end
169
-
170
- # @!visibility private
171
- # Wait for Unix process with id `pid` to terminate.
172
- #
173
- # @param [Integer] pid The id of the process we are waiting on.
174
- # @param [Hash] options Values to control the behavior of this method.
175
- # @option options [Float] :timeout (2.0) How long to wait for the process to
176
- # terminate.
177
- # @option options [Float] :interval (0.1) The polling interval.
178
- # @option options [Boolean] :raise_on_no_terminate (false) Should an error
179
- # be raised if process does not terminate.
180
- # @raise [RuntimeError] If process does not terminate and
181
- # options[:raise_on_no_terminate] is truthy.
182
- def wait_for_process_to_terminate(pid, options={})
183
- default_opts = {:timeout => 2.0,
184
- :interval => 0.1,
185
- :raise_on_no_terminate => false}
186
- merged_opts = default_opts.merge(options)
187
-
188
- cmd = "ps #{pid} -o pid | grep #{pid}"
189
- poll_until = Time.now + merged_opts[:timeout]
190
- delay = merged_opts[:interval]
191
- has_terminated = false
192
- while Time.now < poll_until
193
- has_terminated = `#{cmd}`.strip == ''
194
- break if has_terminated
195
- sleep delay
196
- end
197
-
198
- if merged_opts[:raise_on_no_terminate] and not has_terminated
199
- details = `ps -p #{pid} -o pid,comm | grep #{pid}`.strip
200
- raise RuntimeError, "Waited #{merged_opts[:timeout]} s for process '#{details}' to terminate"
201
- end
202
- end
203
197
  end
204
198
  end
@@ -0,0 +1,55 @@
1
+ module RunLoop
2
+
3
+ # A class for interacting with the lldb command-line tool
4
+ class LLDB
5
+
6
+ # Returns a list of lldb pids.
7
+ # @return [Array<Integer>] An array of integer pids.
8
+ def self.lldb_pids
9
+ ps_output = `#{LLDB_FIND_PIDS_CMD}`.strip
10
+ lines = ps_output.lines("\n").map { |line| line.strip }
11
+ lldb_processes = lines.select { |line| self.is_lldb_process?(line) }
12
+ lldb_processes.map do |ps_description|
13
+ tokens = ps_description.strip.split(' ').map { |token| token.strip }
14
+ pid = tokens.fetch(0, nil)
15
+ if pid.nil?
16
+ nil
17
+ else
18
+ pid.to_i
19
+ end
20
+ end.compact.sort
21
+ end
22
+
23
+ # @!visibility private
24
+ # Is the process described an lldb process?
25
+ #
26
+ # @param [String] ps_details Details about a process as returned by `ps`
27
+ # @return [Boolean] True if the details describe an lldb process.
28
+ def self.is_lldb_process?(ps_details)
29
+ return false if ps_details.nil?
30
+ ps_details[/Contents\/Developer\/usr\/bin\/lldb/, 0] != nil
31
+ end
32
+
33
+ # Attempts to gracefully kill all running lldb processes.
34
+ def self.kill_lldb_processes
35
+ self.lldb_pids.each do |pid|
36
+ unless self.kill_with_signal(pid, 'TERM')
37
+ unless self.kill_with_signal(pid, 'QUIT')
38
+ self.kill_with_signal(pid, 'KILL')
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # @!visibility private
47
+ LLDB_FIND_PIDS_CMD = 'ps x -o pid,command | grep -v grep | grep lldb'
48
+
49
+ # @!visibility private
50
+ def self.kill_with_signal(pid, signal)
51
+ RunLoop::ProcessTerminator.new(pid, signal, 'lldb').kill_process
52
+ end
53
+
54
+ end
55
+ end
@@ -68,6 +68,23 @@ module RunLoop
68
68
  res == ''
69
69
  end
70
70
 
71
+ # Creates an new empty plist at `path`.
72
+ #
73
+ # Is not responsible for creating directories or ensuring write permissions.
74
+ #
75
+ # @param [String] path Where to create the new plist.
76
+ def create_plist(path)
77
+ File.open(path, 'w') do |file|
78
+ file.puts "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
79
+ file.puts "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
80
+ file.puts "<plist version=\"1.0\">"
81
+ file.puts '<dict>'
82
+ file.puts '</dict>'
83
+ file.puts '</plist>'
84
+ end
85
+ path
86
+ end
87
+
71
88
  private
72
89
 
73
90
  # returns the path to the PlistBuddy executable
@@ -168,6 +185,5 @@ module RunLoop
168
185
 
169
186
  "#{plist_buddy} -c #{cmd_part} \"#{file}\""
170
187
  end
171
-
172
188
  end
173
- end
189
+ end
@@ -0,0 +1,140 @@
1
+ module RunLoop
2
+
3
+ # A class for terminating processes and waiting for them to die.
4
+ class ProcessTerminator
5
+
6
+ # @!attribute [r] pid
7
+ # The process id of the process.
8
+ # @return [Integer] The pid.
9
+ attr_reader :pid
10
+
11
+ # @!attribute [r] kill_signal
12
+ # The kill signal to send to the process. Can be a Unix signal name or an
13
+ # Integer.
14
+ # @return [Integer, String] The kill signal.
15
+ attr_reader :kill_signal
16
+
17
+ # @!attribute [r] display_name
18
+ # The process name to use log messages and exceptions. Not used to find
19
+ # or otherwise interact with the process.
20
+ # @return [String] The display name.
21
+ attr_reader :display_name
22
+
23
+ # @!attribute [r] options
24
+ # Options to control the behavior of `kill_process`.
25
+ # @return [Hash] A hash of options.
26
+ attr_reader :options
27
+
28
+ # Create a new process terminator.
29
+ #
30
+ # @param[String,Integer] pid The process pid.
31
+ # @param[String, Integer] kill_signal The kill signal to send to the process.
32
+ # @param[String] display_name The name of the process to kill. Used only
33
+ # in log messages and exceptions.
34
+ # @option options [Float] :timeout (2.0) How long to wait for the process to
35
+ # terminate.
36
+ # @option options [Float] :interval (0.1) The polling interval.
37
+ # @option options [Boolean] :raise_on_no_terminate (false) Should an error
38
+ # be raised if process does not terminate.
39
+ def initialize(pid, kill_signal, display_name, options={})
40
+ @options = DEFAULT_OPTIONS.merge(options)
41
+ @pid = pid.to_i
42
+ @kill_signal = kill_signal
43
+ @display_name = display_name
44
+ end
45
+
46
+ # Try to kill the process identified by `pid`.
47
+ #
48
+ # After sending `kill_signal` to `pid`, wait for the process to terminate.
49
+ #
50
+ # @return [Boolean] Returns true if the process was terminated or is no
51
+ # longer alive.
52
+ # @raise [SignalException] Raised on an unhandled `Process.kill` exception.
53
+ # Errno:ESRCH and Errno:EPERM are _handled_ exceptions; all others will
54
+ # be raised.
55
+ def kill_process
56
+ return true unless process_alive?
57
+
58
+ debug_logging = RunLoop::Environment.debug?
59
+ begin
60
+ if debug_logging
61
+ puts "Sending '#{kill_signal}' to #{display_name} process '#{pid}'"
62
+ end
63
+ Process.kill(kill_signal, pid.to_i)
64
+ # Don't wait.
65
+ # We might not own this process and a WNOHANG would be a nop.
66
+ # Process.wait(pid, Process::WNOHANG)
67
+ rescue Errno::ESRCH
68
+ if debug_logging
69
+ puts "Process with pid '#{pid}' does not exist; nothing to do."
70
+ end
71
+ # Return early; there is no need to wait if the process does not exist.
72
+ return true
73
+ rescue Errno::EPERM
74
+ if debug_logging
75
+ puts "Cannot kill process '#{pid}' with '#{kill_signal}'; not a child of this process"
76
+ end
77
+ rescue SignalException => e
78
+ raise e.message
79
+ end
80
+
81
+ if debug_logging
82
+ puts "Waiting for #{display_name} '#{pid}' to terminate"
83
+ end
84
+ wait_for_process_to_terminate
85
+ end
86
+
87
+ # Is the process `pid` alive?
88
+ # @return [Boolean] Returns true if the process is still alive.
89
+ def process_alive?
90
+ begin
91
+ Process.kill(0, pid.to_i)
92
+ true
93
+ rescue Errno::ESRCH
94
+ false
95
+ rescue Errno::EPERM
96
+ true
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ # @!visibility private
103
+ # The default options for waiting on a process to terminate.
104
+ DEFAULT_OPTIONS =
105
+ {
106
+ :timeout => 2.0,
107
+ :interval => 0.1,
108
+ :raise_on_no_terminate => false
109
+ }
110
+
111
+ # @!visibility private
112
+ # The details of the process reported by `ps`.
113
+ def ps_details
114
+ `xcrun ps -p #{pid} -o pid,comm | grep #{pid}`.strip
115
+ end
116
+
117
+ # @!visibility private
118
+ # Wait for the process to terminate by polling.
119
+ def wait_for_process_to_terminate
120
+ now = Time.now
121
+ poll_until = now + options[:timeout]
122
+ delay = options[:interval]
123
+ has_terminated = false
124
+ while Time.now < poll_until
125
+ has_terminated = !process_alive?
126
+ break if has_terminated
127
+ sleep delay
128
+ end
129
+
130
+ if RunLoop::Environment.debug?
131
+ puts "Waited for #{Time.now - now} seconds for #{display_name} with '#{pid}' to terminate"
132
+ end
133
+
134
+ if @options[:raise_on_no_terminate] and !has_terminated
135
+ raise "Waited #{options[:timeout]} seconds for #{display_name} (#{ps_details}) to terminate"
136
+ end
137
+ has_terminated
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,85 @@
1
+ module RunLoop
2
+
3
+ # A class for waiting on processes.
4
+ class ProcessWaiter
5
+
6
+ attr_reader :process_name
7
+
8
+ def initialize(process_name, options={})
9
+ @options = DEFAULT_OPTIONS.merge(options)
10
+ @process_name = process_name
11
+ end
12
+
13
+ # Collect a list of Integer pids.
14
+ # @return [Array<Integer>] An array of integer pids for the `process_name`
15
+ def pids
16
+ process_info = `ps x -o pid,comm | grep -v grep | grep #{process_name}`
17
+ process_array = process_info.split("\n")
18
+ process_array.map { |process| process.split(' ').first.strip.to_i }
19
+ end
20
+
21
+ # Is the `process_name` a running?
22
+ def running_process?
23
+ !pids.empty?
24
+ end
25
+
26
+ # Wait for `process_name` to start.
27
+ def wait_for_any
28
+ return true if running_process?
29
+
30
+ now = Time.now
31
+ poll_until = now + @options[:timeout]
32
+ delay = @options[:interval]
33
+ is_alive = false
34
+ while Time.now < poll_until
35
+ is_alive = running_process?
36
+ break if is_alive
37
+ sleep delay
38
+ end
39
+
40
+ if RunLoop::Environment.debug?
41
+ puts "Waited for #{Time.now - now} seconds for '#{process_name}' to start."
42
+ end
43
+
44
+ if @options[:raise_on_timeout] and !is_alive
45
+ raise "Waited #{@options[:timeout]} seconds for '#{process_name}' to start."
46
+ end
47
+ is_alive
48
+ end
49
+
50
+ # Wait for all `process_name` to finish.
51
+ def wait_for_none
52
+ return true if !running_process?
53
+
54
+ now = Time.now
55
+ poll_until = now + @options[:timeout]
56
+ delay = @options[:interval]
57
+ has_terminated = false
58
+ while Time.now < poll_until
59
+ has_terminated = !self.running_process?
60
+ break if has_terminated
61
+ sleep delay
62
+ end
63
+
64
+ if RunLoop::Environment.debug?
65
+ puts "Waited for #{Time.now - now} seconds for '#{process_name}' to die."
66
+ end
67
+
68
+ if @options[:raise_on_timeout] and !has_terminated
69
+ raise "Waited #{@options[:timeout]} seconds for '#{process_name}' to die."
70
+ end
71
+ has_terminated
72
+ end
73
+
74
+ private
75
+
76
+ # @!visibility private
77
+ DEFAULT_OPTIONS =
78
+ {
79
+ :timeout => 10.0,
80
+ :interval => 0.1,
81
+ :raise_on_timeout => false
82
+ }
83
+ end
84
+ end
85
+