run_loop_tcc 2.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/bin/run-loop +19 -0
- data/lib/run_loop/abstract.rb +18 -0
- data/lib/run_loop/app.rb +372 -0
- data/lib/run_loop/cache/cache.rb +68 -0
- data/lib/run_loop/cli/cli.rb +48 -0
- data/lib/run_loop/cli/codesign.rb +24 -0
- data/lib/run_loop/cli/errors.rb +11 -0
- data/lib/run_loop/cli/instruments.rb +160 -0
- data/lib/run_loop/cli/locale.rb +31 -0
- data/lib/run_loop/cli/simctl.rb +257 -0
- data/lib/run_loop/cli/tcc.rb +139 -0
- data/lib/run_loop/codesign.rb +76 -0
- data/lib/run_loop/core.rb +902 -0
- data/lib/run_loop/core_simulator.rb +960 -0
- data/lib/run_loop/detect_aut/detect.rb +185 -0
- data/lib/run_loop/detect_aut/errors.rb +126 -0
- data/lib/run_loop/detect_aut/xamarin_studio.rb +46 -0
- data/lib/run_loop/detect_aut/xcode.rb +157 -0
- data/lib/run_loop/device.rb +722 -0
- data/lib/run_loop/device_agent/app/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/bin/xctestctl +0 -0
- data/lib/run_loop/device_agent/cbxrunner.rb +156 -0
- data/lib/run_loop/device_agent/frameworks/Frameworks.zip +0 -0
- data/lib/run_loop/device_agent/frameworks.rb +65 -0
- data/lib/run_loop/device_agent/ipa/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/launcher.rb +51 -0
- data/lib/run_loop/device_agent/xcodebuild.rb +91 -0
- data/lib/run_loop/device_agent/xctestctl.rb +109 -0
- data/lib/run_loop/directory.rb +179 -0
- data/lib/run_loop/dnssd.rb +148 -0
- data/lib/run_loop/dot_dir.rb +87 -0
- data/lib/run_loop/dylib_injector.rb +145 -0
- data/lib/run_loop/encoding.rb +56 -0
- data/lib/run_loop/environment.rb +361 -0
- data/lib/run_loop/fifo.rb +40 -0
- data/lib/run_loop/host_cache.rb +128 -0
- data/lib/run_loop/http/error.rb +15 -0
- data/lib/run_loop/http/request.rb +44 -0
- data/lib/run_loop/http/retriable_client.rb +166 -0
- data/lib/run_loop/http/server.rb +17 -0
- data/lib/run_loop/instruments.rb +436 -0
- data/lib/run_loop/ipa.rb +142 -0
- data/lib/run_loop/l10n.rb +93 -0
- data/lib/run_loop/language.rb +63 -0
- data/lib/run_loop/lipo.rb +132 -0
- data/lib/run_loop/lldb.rb +52 -0
- data/lib/run_loop/locale.rb +101 -0
- data/lib/run_loop/logging.rb +111 -0
- data/lib/run_loop/otool.rb +76 -0
- data/lib/run_loop/patches/awesome_print.rb +17 -0
- data/lib/run_loop/physical_device/life_cycle.rb +268 -0
- data/lib/run_loop/plist_buddy.rb +189 -0
- data/lib/run_loop/process_terminator.rb +128 -0
- data/lib/run_loop/process_waiter.rb +117 -0
- data/lib/run_loop/regex.rb +19 -0
- data/lib/run_loop/shell.rb +103 -0
- data/lib/run_loop/sim_control.rb +1264 -0
- data/lib/run_loop/simctl.rb +275 -0
- data/lib/run_loop/sqlite.rb +61 -0
- data/lib/run_loop/strings.rb +88 -0
- data/lib/run_loop/tcc/TCC.db +0 -0
- data/lib/run_loop/tcc/tcc.rb +240 -0
- data/lib/run_loop/template.rb +61 -0
- data/lib/run_loop/version.rb +182 -0
- data/lib/run_loop/xcode.rb +318 -0
- data/lib/run_loop/xcrun.rb +107 -0
- data/lib/run_loop/xcuitest.rb +550 -0
- data/lib/run_loop.rb +230 -0
- data/plists/simctl/com.apple.UIAutomation.plist +0 -0
- data/plists/simctl/com.apple.UIAutomationPlugIn.plist +0 -0
- data/scripts/calabash_script_uia.js +28184 -0
- data/scripts/lib/json2.min.js +26 -0
- data/scripts/lib/log.js +26 -0
- data/scripts/lib/on_alert.js +224 -0
- data/scripts/read-cmd.sh +2 -0
- data/scripts/run_dismiss_location.js +89 -0
- data/scripts/run_loop_basic.js +34 -0
- data/scripts/run_loop_fast_uia.js +188 -0
- data/scripts/run_loop_host.js +117 -0
- data/scripts/run_loop_shared_element.js +125 -0
- data/scripts/timeout3 +23 -0
- data/scripts/udidetect +0 -0
- data/vendor-licenses/FBSimulatorControl.LICENSE +30 -0
- data/vendor-licenses/xctestctl.LICENSE +32 -0
- 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
|
+
|