run_loop_tcc 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/bin/run-loop +19 -0
  4. data/lib/run_loop/abstract.rb +18 -0
  5. data/lib/run_loop/app.rb +372 -0
  6. data/lib/run_loop/cache/cache.rb +68 -0
  7. data/lib/run_loop/cli/cli.rb +48 -0
  8. data/lib/run_loop/cli/codesign.rb +24 -0
  9. data/lib/run_loop/cli/errors.rb +11 -0
  10. data/lib/run_loop/cli/instruments.rb +160 -0
  11. data/lib/run_loop/cli/locale.rb +31 -0
  12. data/lib/run_loop/cli/simctl.rb +257 -0
  13. data/lib/run_loop/cli/tcc.rb +139 -0
  14. data/lib/run_loop/codesign.rb +76 -0
  15. data/lib/run_loop/core.rb +902 -0
  16. data/lib/run_loop/core_simulator.rb +960 -0
  17. data/lib/run_loop/detect_aut/detect.rb +185 -0
  18. data/lib/run_loop/detect_aut/errors.rb +126 -0
  19. data/lib/run_loop/detect_aut/xamarin_studio.rb +46 -0
  20. data/lib/run_loop/detect_aut/xcode.rb +157 -0
  21. data/lib/run_loop/device.rb +722 -0
  22. data/lib/run_loop/device_agent/app/CBX-Runner.app.zip +0 -0
  23. data/lib/run_loop/device_agent/bin/xctestctl +0 -0
  24. data/lib/run_loop/device_agent/cbxrunner.rb +156 -0
  25. data/lib/run_loop/device_agent/frameworks/Frameworks.zip +0 -0
  26. data/lib/run_loop/device_agent/frameworks.rb +65 -0
  27. data/lib/run_loop/device_agent/ipa/CBX-Runner.app.zip +0 -0
  28. data/lib/run_loop/device_agent/launcher.rb +51 -0
  29. data/lib/run_loop/device_agent/xcodebuild.rb +91 -0
  30. data/lib/run_loop/device_agent/xctestctl.rb +109 -0
  31. data/lib/run_loop/directory.rb +179 -0
  32. data/lib/run_loop/dnssd.rb +148 -0
  33. data/lib/run_loop/dot_dir.rb +87 -0
  34. data/lib/run_loop/dylib_injector.rb +145 -0
  35. data/lib/run_loop/encoding.rb +56 -0
  36. data/lib/run_loop/environment.rb +361 -0
  37. data/lib/run_loop/fifo.rb +40 -0
  38. data/lib/run_loop/host_cache.rb +128 -0
  39. data/lib/run_loop/http/error.rb +15 -0
  40. data/lib/run_loop/http/request.rb +44 -0
  41. data/lib/run_loop/http/retriable_client.rb +166 -0
  42. data/lib/run_loop/http/server.rb +17 -0
  43. data/lib/run_loop/instruments.rb +436 -0
  44. data/lib/run_loop/ipa.rb +142 -0
  45. data/lib/run_loop/l10n.rb +93 -0
  46. data/lib/run_loop/language.rb +63 -0
  47. data/lib/run_loop/lipo.rb +132 -0
  48. data/lib/run_loop/lldb.rb +52 -0
  49. data/lib/run_loop/locale.rb +101 -0
  50. data/lib/run_loop/logging.rb +111 -0
  51. data/lib/run_loop/otool.rb +76 -0
  52. data/lib/run_loop/patches/awesome_print.rb +17 -0
  53. data/lib/run_loop/physical_device/life_cycle.rb +268 -0
  54. data/lib/run_loop/plist_buddy.rb +189 -0
  55. data/lib/run_loop/process_terminator.rb +128 -0
  56. data/lib/run_loop/process_waiter.rb +117 -0
  57. data/lib/run_loop/regex.rb +19 -0
  58. data/lib/run_loop/shell.rb +103 -0
  59. data/lib/run_loop/sim_control.rb +1264 -0
  60. data/lib/run_loop/simctl.rb +275 -0
  61. data/lib/run_loop/sqlite.rb +61 -0
  62. data/lib/run_loop/strings.rb +88 -0
  63. data/lib/run_loop/tcc/TCC.db +0 -0
  64. data/lib/run_loop/tcc/tcc.rb +240 -0
  65. data/lib/run_loop/template.rb +61 -0
  66. data/lib/run_loop/version.rb +182 -0
  67. data/lib/run_loop/xcode.rb +318 -0
  68. data/lib/run_loop/xcrun.rb +107 -0
  69. data/lib/run_loop/xcuitest.rb +550 -0
  70. data/lib/run_loop.rb +230 -0
  71. data/plists/simctl/com.apple.UIAutomation.plist +0 -0
  72. data/plists/simctl/com.apple.UIAutomationPlugIn.plist +0 -0
  73. data/scripts/calabash_script_uia.js +28184 -0
  74. data/scripts/lib/json2.min.js +26 -0
  75. data/scripts/lib/log.js +26 -0
  76. data/scripts/lib/on_alert.js +224 -0
  77. data/scripts/read-cmd.sh +2 -0
  78. data/scripts/run_dismiss_location.js +89 -0
  79. data/scripts/run_loop_basic.js +34 -0
  80. data/scripts/run_loop_fast_uia.js +188 -0
  81. data/scripts/run_loop_host.js +117 -0
  82. data/scripts/run_loop_shared_element.js +125 -0
  83. data/scripts/timeout3 +23 -0
  84. data/scripts/udidetect +0 -0
  85. data/vendor-licenses/FBSimulatorControl.LICENSE +30 -0
  86. data/vendor-licenses/xctestctl.LICENSE +32 -0
  87. metadata +443 -0
@@ -0,0 +1,268 @@
1
+ module RunLoop
2
+ # @!visibility private
3
+ module PhysicalDevice
4
+
5
+ # Raised when installation fails.
6
+ class InstallError < RuntimeError; end
7
+
8
+ # Raised when uninstall fails.
9
+ class UninstallError < RuntimeError; end
10
+
11
+ # Raised when tool cannot perform task.
12
+ class NotImplementedError < StandardError; end
13
+
14
+ # Controls the behavior of various life cycle commands.
15
+ #
16
+ # You can override these values if they do not work in your environment.
17
+ #
18
+ # For cucumber users, the best place to override would be in your
19
+ # features/support/env.rb.
20
+ #
21
+ # For example:
22
+ #
23
+ # RunLoop::PhysicalDevice::LifeCycle::DEFAULT_OPTIONS[:timeout] = 60
24
+ DEFAULT_OPTIONS = {
25
+ :install_timeout => RunLoop::Environment.ci? ? 120 : 30
26
+ }
27
+
28
+ # @!visibility private
29
+ class LifeCycle
30
+
31
+ require "run_loop/abstract"
32
+ include RunLoop::Abstract
33
+
34
+ require "run_loop/shell"
35
+ include RunLoop::Shell
36
+
37
+ attr_reader :device
38
+
39
+ # Create a new instance.
40
+ #
41
+ # @param [RunLoop::Device] device A physical device.
42
+ # @raise [ArgumentError] If device is a simulator.
43
+ def initialize(device)
44
+ if !device.physical_device?
45
+ raise ArgumentError, %Q[Device:
46
+
47
+ #{device}
48
+
49
+ must be a physical device.]
50
+ end
51
+ @device = device
52
+ end
53
+
54
+ # Is the tool installed?
55
+ def self.tool_is_installed?
56
+ raise RunLoop::Abstract::AbstractMethodError,
57
+ "Subclass must implement '.tool_is_installed?'"
58
+ end
59
+
60
+ # Path to tool.
61
+ def self.executable_path
62
+ raise RunLoop::Abstract::AbstractMethodError,
63
+ "Subclass must implement '.executable_path'"
64
+ end
65
+
66
+ # Is the app installed?
67
+ #
68
+ # @param [String] bundle_id The CFBundleIdentifier of an app.
69
+ # @return [Boolean] true or false
70
+ def app_installed?(bundle_id)
71
+ abstract_method!
72
+ end
73
+
74
+ # Install the app or ipa.
75
+ #
76
+ # If the app is already installed, it will be reinstalled from disk;
77
+ # no version check is performed.
78
+ #
79
+ # App data is never preserved. If you want to preserve the app data,
80
+ # call `ensure_newest_installed`.
81
+ #
82
+ # Possible return values:
83
+ #
84
+ # * :reinstalled => app was installed, but app data was not preserved.
85
+ # * :installed => app was not installed.
86
+ #
87
+ # @param [RunLoop::Ipa, RunLoop::App] app_or_ipa The ipa to install.
88
+ # The caller is responsible for validating the ipa for the device by
89
+ # checking that the codesign and instruction set is correct.
90
+ #
91
+ # @raise [InstallError] If app was not installed.
92
+ #
93
+ # @return [Symbol] A keyword describing the action that was performed.
94
+ def install_app(app_or_ipa)
95
+ abstract_method!
96
+ end
97
+
98
+ # Uninstall the app with bundle_id.
99
+ #
100
+ # App data is never preserved. If you want to install a new version of
101
+ # an app and preserve app data (upgrade testing), call
102
+ # `ensure_newest_installed`.
103
+ #
104
+ # Possible return values:
105
+ #
106
+ # * :nothing => app was not installed
107
+ # * :uninstall => app was uninstalled
108
+ #
109
+ # @param [String] bundle_id The CFBundleIdentifier of an app.
110
+ #
111
+ # @raise [UninstallError] If the app cannot be uninstalled, usually
112
+ # because it is a system app.
113
+ #
114
+ # @return [Symbol] A keyword that describes what action was performed.
115
+ def uninstall_app(bundle_id)
116
+ abstract_method!
117
+ end
118
+
119
+ # Ensures the app is installed and ensures that app is not stale by
120
+ # asking if the version of installed app is different than the version
121
+ # of the app or ipa on disk.
122
+ #
123
+ # The concrete implementation needs to check the CFBundleVersion and
124
+ # the CFBundleShortVersionString. If either are different, then the
125
+ # app should be reinstalled.
126
+ #
127
+ # If possible, the app data should be preserved across reinstallation.
128
+ #
129
+ # Possible return values:
130
+ #
131
+ # * :nothing => app was already installed and versions matched.
132
+ # * :upgraded => app was stale; newer version from disk was installed and
133
+ # app data was preserved.
134
+ # * :reinstalled => app was stale; newer version from disk was installed,
135
+ # but app data was not preserved.
136
+ # * :installed => app was not installed.
137
+ #
138
+ # @param [RunLoop::Ipa, RunLoop::App] app_or_ipa The ipa to install.
139
+ # The caller is responsible for validating the ipa for the device by
140
+ # checking that the codesign and instruction set is correct.
141
+ #
142
+ # @raise [InstallError] If the app could not be installed.
143
+ # @raise [UninstallError] If the app could not be uninstalled.
144
+ #
145
+ # @return [Symbol] A keyword that describes the action that was taken.
146
+ def ensure_newest_installed(app_or_ipa)
147
+ abstract_method!
148
+ end
149
+
150
+ # Is the app on disk the same as the installed app?
151
+ #
152
+ # The concrete implementation needs to check the CFBundleVersion and
153
+ # the CFBundleShortVersionString. If either are different, then this
154
+ # method returns false.
155
+ #
156
+ # @param [RunLoop::Ipa, RunLoop::App] app_or_ipa The ipa to install.
157
+ # The caller is responsible for validating the ipa for the device by
158
+ # checking that the codesign and instruction set is correct.
159
+ #
160
+ # @raise [RuntimeError] If app is not already installed.
161
+ def installed_app_same_as?(app_or_ipa)
162
+ abstract_method!
163
+ end
164
+
165
+ # Returns true if this tool can reset an app's sandbox without
166
+ # uninstalling the app.
167
+ def can_reset_app_sandbox?
168
+ abstract_method!
169
+ end
170
+
171
+ # Clear the app sandbox.
172
+ #
173
+ # This method will never uninstall the app. If the concrete
174
+ # implementation cannot reset the app data, this method should raise
175
+ # a RunLoop::PhysicalDevice::NotImplementedError
176
+ #
177
+ # Does not clear Keychain. Use the Calabash iOS Keychain API.
178
+ #
179
+ # @param [String] bundle_id The CFBundleIdentifier of an app.
180
+ #
181
+ # @raise [RunLoop::PhysicalDevice::NotImplementedError] If this tool
182
+ # cannot reset the app sandbox without unintalling the app.
183
+ def reset_app_sandbox(bundle_id)
184
+ abstract_method!
185
+ end
186
+
187
+ # Return the architecture of the device.
188
+ def architecture
189
+ abstract_method!
190
+ end
191
+
192
+ # Is the app or ipa compatible with the architecture of the device?
193
+ def app_has_compatible_architecture?(app_or_ipa)
194
+ abstract_method!
195
+ end
196
+
197
+ # Return true if the device is an iPhone.
198
+ def iphone?
199
+ abstract_method!
200
+ end
201
+
202
+ # Return false if the device is an iPad.
203
+ def ipad?
204
+ abstract_method!
205
+ end
206
+
207
+ # Return the model of the device.
208
+ def model
209
+ abstract_method!
210
+ end
211
+
212
+ # Sideload data into the app's sandbox.
213
+ #
214
+ # These directories exist in the application sandbox.
215
+ #
216
+ # * sandbox/Documents
217
+ # * sandbox/Library
218
+ # * sandbox/Library/Preferences
219
+ # * sandbox/tmp
220
+ #
221
+ # Behavior TBD.
222
+ def sideload(data)
223
+ raise NotImplementedError,
224
+ "The behavior of the sideload method has not been determined"
225
+ end
226
+
227
+ # Removes a file or directory from the app sandbox.
228
+ #
229
+ # Behavior TBD.
230
+ def remove_from_sandbox(path)
231
+ raise NotImplementedError,
232
+ "The behavior of the remove_from_sandbox method has not been determined"
233
+ end
234
+
235
+ # @!visibility private
236
+ def expect_app_or_ipa(app_or_ipa)
237
+ if ![is_app?(app_or_ipa), is_ipa?(app_or_ipa)].any?
238
+ if app_or_ipa.nil?
239
+ object = "nil"
240
+ elsif app_or_ipa == ""
241
+ object = "<empty string>"
242
+ else
243
+ object = app_or_ipa
244
+ end
245
+
246
+ raise ArgumentError, %Q[Expected:
247
+
248
+ #{object}
249
+
250
+ to be a RunLoop::App or a RunLoop::Ipa.]
251
+ end
252
+
253
+ true
254
+ end
255
+
256
+ # @!visibility private
257
+ def is_app?(app_or_ipa)
258
+ app_or_ipa.is_a?(RunLoop::App)
259
+ end
260
+
261
+ # @!visibility private
262
+ def is_ipa?(app_or_ipa)
263
+ app_or_ipa.is_a?(RunLoop::Ipa)
264
+ end
265
+ end
266
+ end
267
+ end
268
+
@@ -0,0 +1,189 @@
1
+ module RunLoop
2
+ # A class for reading and writing property list values.
3
+ #
4
+ # Why not use CFPropertyList? Because it is super wonky. Among its other
5
+ # faults, it matches Boolean to a string type with 'true/false' values which
6
+ # is problematic for our purposes.
7
+ class PlistBuddy
8
+
9
+ # Reads key from file and returns the result.
10
+ # @param [String] key the key to inspect (may not be nil or empty)
11
+ # @param [String] file the plist to read
12
+ # @param [Hash] opts options for controlling execution
13
+ # @option opts [Boolean] :verbose (false) controls log level
14
+ # @return [String] the value of the key
15
+ # @raise [ArgumentError] if nil or empty key
16
+ def plist_read(key, file, opts={})
17
+ if key.nil? or key.length == 0
18
+ raise(ArgumentError, "key '#{key}' must not be nil or empty")
19
+ end
20
+ cmd = build_plist_cmd(:print, {:key => key}, file)
21
+ res = execute_plist_cmd(cmd, opts)
22
+ if res == "Print: Entry, \":#{key}\", Does Not Exist"
23
+ nil
24
+ else
25
+ res
26
+ end
27
+ end
28
+
29
+ # Checks if the key exists in plist.
30
+ # @param [String] key the key to inspect (may not be nil or empty)
31
+ # @param [String] file the plist to read
32
+ # @param [Hash] opts options for controlling execution
33
+ # @option opts [Boolean] :verbose (false) controls log level
34
+ # @return [Boolean] true if the key exists in plist file
35
+ def plist_key_exists?(key, file, opts={})
36
+ plist_read(key, file, opts) != nil
37
+ end
38
+
39
+ # Replaces or creates the value of key in the file.
40
+ #
41
+ # @param [String] key the key to set (may not be nil or empty)
42
+ # @param [String] type the plist type (used only when adding a value)
43
+ # @param [String] value the new value
44
+ # @param [String] file the plist to read
45
+ # @param [Hash] opts options for controlling execution
46
+ # @option opts [Boolean] :verbose (false) controls log level
47
+ # @return [Boolean] true if the operation was successful
48
+ # @raise [ArgumentError] if nil or empty key
49
+ def plist_set(key, type, value, file, opts={})
50
+ default_opts = {:verbose => false}
51
+ merged = default_opts.merge(opts)
52
+
53
+ if key.nil? or key.length == 0
54
+ raise(ArgumentError, "key '#{key}' must not be nil or empty")
55
+ end
56
+
57
+ cmd_args = {:key => key,
58
+ :type => type,
59
+ :value => value}
60
+
61
+ if plist_key_exists?(key, file, merged)
62
+ cmd = build_plist_cmd(:set, cmd_args, file)
63
+ else
64
+ cmd = build_plist_cmd(:add, cmd_args, file)
65
+ end
66
+
67
+ res = execute_plist_cmd(cmd, merged)
68
+ res == ''
69
+ end
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
+
88
+ private
89
+
90
+ # returns the path to the PlistBuddy executable
91
+ # @return [String] path to PlistBuddy
92
+ def plist_buddy
93
+ '/usr/libexec/PlistBuddy'
94
+ end
95
+
96
+ # Executes cmd as a shell command and returns the result.
97
+ #
98
+ # @param [String] cmd shell command to execute
99
+ # @param [Hash] opts options for controlling execution
100
+ # @option opts [Boolean] :verbose (false) controls log level
101
+ # @return [Boolean,String] `true` if command was successful. If :print'ing
102
+ # the result, the value of the key. If there is an error, the output of
103
+ # stderr.
104
+ def execute_plist_cmd(cmd, opts={})
105
+ default_opts = {:verbose => false}
106
+ merged = default_opts.merge(opts)
107
+
108
+ puts cmd if merged[:verbose]
109
+
110
+ res = nil
111
+ # noinspection RubyUnusedLocalVariable
112
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
113
+ err = stderr.read
114
+ std = stdout.read
115
+ if not err.nil? and err != ''
116
+ res = err.chomp
117
+ else
118
+ res = std.chomp
119
+ end
120
+ end
121
+ res
122
+ end
123
+
124
+ # Composes a PlistBuddy command that can be executed as a shell command.
125
+ #
126
+ # @param [Symbol] type should be one of [:print, :set, :add]
127
+ #
128
+ # @param [Hash] args_hash arguments used to construct plist command
129
+ # @option args_hash [String] :key (required) the plist key
130
+ # @option args_hash [String] :value (required for :set and :add) the new value
131
+ # @option args_hash [String] :type (required for :add) the new type of the value
132
+ #
133
+ # @param [String] file the plist file to interact with (must exist)
134
+ #
135
+ # @raise [RuntimeError] if file does not exist
136
+ # @raise [ArgumentError] when invalid type is passed
137
+ # @raise [ArgumentError] when args_hash does not include required key/value pairs
138
+ #
139
+ # @return [String] a shell-ready PlistBuddy command
140
+ def build_plist_cmd(type, args_hash, file)
141
+
142
+ unless File.exist?(File.expand_path(file))
143
+ raise(RuntimeError, "plist '#{file}' does not exist - could not read")
144
+ end
145
+
146
+ case type
147
+ when :add
148
+ value_type = args_hash[:type]
149
+ unless value_type
150
+ raise(ArgumentError, ':value_type is a required key for :add command')
151
+ end
152
+ allowed_value_types = ['string', 'bool', 'real', 'integer']
153
+ unless allowed_value_types.include?(value_type)
154
+ raise(ArgumentError, "expected '#{value_type}' to be one of '#{allowed_value_types}'")
155
+ end
156
+ value = args_hash[:value]
157
+ unless value
158
+ raise(ArgumentError, ':value is a required key for :add command')
159
+ end
160
+ key = args_hash[:key]
161
+ unless key
162
+ raise(ArgumentError, ':key is a required key for :add command')
163
+ end
164
+ cmd_part = "\"Add :#{key} #{value_type} #{value}\""
165
+ when :print
166
+ key = args_hash[:key]
167
+ unless key
168
+ raise(ArgumentError, ':key is a required key for :print command')
169
+ end
170
+ cmd_part = "\"Print :#{key}\""
171
+ when :set
172
+ value = args_hash[:value]
173
+ unless value
174
+ raise(ArgumentError, ':value is a required key for :set command')
175
+ end
176
+ key = args_hash[:key]
177
+ unless key
178
+ raise(ArgumentError, ':key is a required key for :set command')
179
+ end
180
+ cmd_part = "\"Set :#{key} #{value}\""
181
+ else
182
+ cmds = [:add, :print, :set]
183
+ raise(ArgumentError, "expected '#{type}' to be one of '#{cmds}'")
184
+ end
185
+
186
+ "#{plist_buddy} -c #{cmd_part} \"#{file}\""
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,128 @@
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
+ begin
59
+ RunLoop.log_debug("Sending '#{kill_signal}' to #{display_name} process '#{pid}'")
60
+ Process.kill(kill_signal, pid.to_i)
61
+ # Don't wait.
62
+ # We might not own this process and a WNOHANG would be a nop.
63
+ # Process.wait(pid, Process::WNOHANG)
64
+ rescue Errno::ESRCH
65
+ RunLoop.log_debug("Process with pid '#{pid}' does not exist; nothing to do.")
66
+ # Return early; there is no need to wait if the process does not exist.
67
+ return true
68
+ rescue Errno::EPERM
69
+ RunLoop.log_debug("Cannot kill process '#{pid}' with '#{kill_signal}'; not a child of this process")
70
+ rescue SignalException => e
71
+ raise e.message
72
+ end
73
+ RunLoop.log_debug("Waiting for #{display_name} with pid '#{pid}' to terminate")
74
+ wait_for_process_to_terminate
75
+ end
76
+
77
+ # Is the process `pid` alive?
78
+ # @return [Boolean] Returns true if the process is still alive.
79
+ def process_alive?
80
+ begin
81
+ Process.kill(0, pid.to_i)
82
+ true
83
+ rescue Errno::ESRCH
84
+ false
85
+ rescue Errno::EPERM
86
+ true
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # @!visibility private
93
+ # The default options for waiting on a process to terminate.
94
+ DEFAULT_OPTIONS =
95
+ {
96
+ :timeout => 2.0,
97
+ :interval => 0.1,
98
+ :raise_on_no_terminate => false
99
+ }
100
+
101
+ # @!visibility private
102
+ # The details of the process reported by `ps`.
103
+ def ps_details
104
+ `xcrun ps -p #{pid} -o pid,comm | grep #{pid}`.strip
105
+ end
106
+
107
+ # @!visibility private
108
+ # Wait for the process to terminate by polling.
109
+ def wait_for_process_to_terminate
110
+ now = Time.now
111
+ poll_until = now + options[:timeout]
112
+ delay = options[:interval]
113
+ has_terminated = false
114
+ while Time.now < poll_until
115
+ has_terminated = !process_alive?
116
+ break if has_terminated
117
+ sleep delay
118
+ end
119
+
120
+ RunLoop.log_debug("Waited for #{Time.now - now} seconds for #{display_name} with pid '#{pid}' to terminate")
121
+
122
+ if @options[:raise_on_no_terminate] and !has_terminated
123
+ raise "Waited #{options[:timeout]} seconds for #{display_name} (#{ps_details}) to terminate"
124
+ end
125
+ has_terminated
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,117 @@
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 a number of process to start.
27
+ # @param [Integer] n The number of processes to wait for.
28
+ # @raise [ArgumentError] If n < 0
29
+ # @raise [ArgumentError] If n is not an Integer
30
+ def wait_for_n(n)
31
+ unless n.is_a?(Integer)
32
+ raise ArgumentError, "Expected #{n.class} to be #{1.class}"
33
+ end
34
+
35
+ unless n > 0
36
+ raise ArgumentError, "Expected #{n} to be > 0"
37
+ end
38
+
39
+ return true if pids.count == n
40
+
41
+ now = Time.now
42
+ poll_until = now + @options[:timeout]
43
+ delay = @options[:interval]
44
+ there_are_n = false
45
+ while Time.now < poll_until
46
+ there_are_n = pids.count == n
47
+ break if there_are_n
48
+ sleep delay
49
+ end
50
+
51
+ plural = n > 1 ? "es" : ''
52
+ RunLoop.log_debug("Waited for #{Time.now - now} seconds for #{n} '#{process_name}' process#{plural} to start.")
53
+
54
+ if @options[:raise_on_timeout] and !there_are_n
55
+ plural = n > 1 ? "es" : ''
56
+ raise "Waited #{@options[:timeout]} seconds for #{n} '#{process_name}' process#{plural} to start."
57
+ end
58
+ there_are_n
59
+ end
60
+
61
+
62
+ # Wait for `process_name` to start.
63
+ def wait_for_any
64
+ return true if running_process?
65
+
66
+ now = Time.now
67
+ poll_until = now + @options[:timeout]
68
+ delay = @options[:interval]
69
+ is_alive = false
70
+ while Time.now < poll_until
71
+ is_alive = running_process?
72
+ break if is_alive
73
+ sleep delay
74
+ end
75
+
76
+ RunLoop.log_debug("Waited for #{Time.now - now} seconds for '#{process_name}' to start.")
77
+
78
+ if @options[:raise_on_timeout] and !is_alive
79
+ raise "Waited #{@options[:timeout]} seconds for '#{process_name}' to start."
80
+ end
81
+ is_alive
82
+ end
83
+
84
+ # Wait for all `process_name` to finish.
85
+ def wait_for_none
86
+ return true if !running_process?
87
+
88
+ now = Time.now
89
+ poll_until = now + @options[:timeout]
90
+ delay = @options[:interval]
91
+ has_terminated = false
92
+ while Time.now < poll_until
93
+ has_terminated = !self.running_process?
94
+ break if has_terminated
95
+ sleep delay
96
+ end
97
+
98
+ RunLoop.log_debug("Waited for #{Time.now - now} seconds for '#{process_name}' to die.")
99
+
100
+ if @options[:raise_on_timeout] and !has_terminated
101
+ raise "Waited #{@options[:timeout]} seconds for '#{process_name}' to die."
102
+ end
103
+ has_terminated
104
+ end
105
+
106
+ private
107
+
108
+ # @!visibility private
109
+ DEFAULT_OPTIONS =
110
+ {
111
+ :timeout => 10.0,
112
+ :interval => 0.1,
113
+ :raise_on_timeout => false
114
+ }
115
+ end
116
+ end
117
+