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.
- 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
|
+
|