run_loop_tcc 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+