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,166 @@
1
+ module RunLoop
2
+ module HTTP
3
+ require "httpclient"
4
+
5
+ # An HTTP client that retries its connection on errors and can time out.
6
+ # @!visibility private
7
+ class RetriableClient
8
+ attr_reader :client
9
+
10
+ # @!visibility private
11
+ RETRY_ON =
12
+ [
13
+ # The connection, request, or response timed out
14
+ #HTTPClient::TimeoutError,
15
+ # The address is not found. Useful for polling.
16
+ SocketError,
17
+ # The proxy could not connect to the server (Android)
18
+ # or the server is not running (iOS)
19
+ HTTPClient::KeepAliveDisconnected,
20
+ # No proxy has been set up (Android)
21
+ Errno::ECONNREFUSED,
22
+ # The server sent a partial response
23
+ #Errno::ECONNRESET,
24
+ # Client sent TCP reset (RST) before server has accepted the
25
+ # connection requested by client.
26
+ Errno::ECONNABORTED,
27
+ # The foreign function call call timed out
28
+ #Errno::ETIMEDOUT
29
+ ]
30
+
31
+ # @!visibility private
32
+ HEADER =
33
+ {
34
+ 'Content-Type' => 'application/json;charset=utf-8'
35
+ }
36
+
37
+ # Creates a new retriable client.
38
+ #
39
+ # This initializer takes multiple options. If the option is not
40
+ # documented, it should be considered _private_. You use undocumented
41
+ # options at your own risk.
42
+ #
43
+ # @param [RunLoop::HTTP::Server] server The server to make the HTTP request
44
+ # on.
45
+ # @param [Hash] options Control the retry, timeout, and interval.
46
+ # @option options [Number] :retries (5) How often to retry.
47
+ # @option options [Number] :timeout (5) How long to wait for a response
48
+ # before timing out.
49
+ # @option options [Number] :interval (0.5) How long to sleep between
50
+ # retries.
51
+ def initialize(server, options = {})
52
+ @client = options[:client] || ::HTTPClient.new
53
+ @server = server
54
+ @retries = options.fetch(:retries, 5)
55
+ @timeout = options.fetch(:timeout, 5)
56
+ @interval = options.fetch(:interval, 0.5)
57
+ @on_error = {}
58
+ end
59
+
60
+ # @!visibility private
61
+ def on_error(type, &block)
62
+ @on_error[type] = block
63
+ end
64
+
65
+ # @!visibility private
66
+ def change_server(new_server)
67
+ @server = new_server
68
+ end
69
+
70
+ # Make an HTTP get request.
71
+ #
72
+ # This method takes multiple options. If the option is not documented,
73
+ # it should be considered _private_. You use undocumented options at
74
+ # your own risk.
75
+ #
76
+ # @param [RunLoop::HTTP::Request] request The request.
77
+ # @param [Hash] options Control the retry, timeout, and interval.
78
+ # @option options [Number] :retries (5) How often to retry.
79
+ # @option options [Number] :timeout (5) How long to wait for a response
80
+ # before timing out.
81
+ # @option options [Number] :interval (0.5) How long to sleep between
82
+ # retries.
83
+ def get(request, options={})
84
+ request(request, :get, options)
85
+ end
86
+
87
+ # Make an HTTP post request.
88
+ #
89
+ # This method takes multiple options. If the option is not documented,
90
+ # it should be considered _private_. You use undocumented options at
91
+ # your own risk.
92
+ #
93
+ # @param [RunLoop::HTTP::Request] request The request.
94
+ # @param [Hash] options Control the retry, timeout, and interval.
95
+ # @option options [Number] :retries (5) How often to retry.
96
+ # @option options [Number] :timeout (5) How long to wait for a response
97
+ # before timing out.
98
+ # @option options [Number] :interval (0.5) How long to sleep between
99
+ # retries.
100
+ def post(request, options={})
101
+ request(request, :post, options)
102
+ end
103
+
104
+ def delete(request, options={})
105
+ request(request, :delete, options)
106
+ end
107
+
108
+ private
109
+
110
+ def request(request, request_method, options={})
111
+ retries = options.fetch(:retries, @retries)
112
+ timeout = options.fetch(:timeout, @timeout)
113
+ interval = options.fetch(:interval, @interval)
114
+ header = options.fetch(:header, HEADER)
115
+
116
+ RunLoop.log_debug("HTTP: #{@server.endpoint + request.route} #{options}")
117
+
118
+ start_time = Time.now
119
+ last_error = nil
120
+
121
+ client = @client.dup
122
+ client.receive_timeout = timeout
123
+
124
+ retries.times do |i|
125
+ first_try = i == 0
126
+
127
+ # Subtract the aggregate time we've spent thus far to make sure we're
128
+ # not exceeding the request timeout across retries.
129
+ time_diff = start_time + timeout - Time.now
130
+
131
+ if time_diff <= 0
132
+ raise HTTP::Error, 'Timeout exceeded'
133
+ end
134
+
135
+ client.receive_timeout = [time_diff, client.receive_timeout].min
136
+
137
+ begin
138
+ return client.send(request_method, @server.endpoint + request.route,
139
+ request.params, header)
140
+ rescue *RETRY_ON => e
141
+ #RunLoop.log_debug("Rescued http error: #{e}")
142
+
143
+ if first_try
144
+ if @on_error[e.class]
145
+ @on_error[e.class].call(@server)
146
+ end
147
+ end
148
+
149
+ last_error = e
150
+ sleep interval
151
+ end
152
+ end
153
+
154
+ # We should raise helpful messages
155
+ if last_error.is_a?(HTTPClient::KeepAliveDisconnected)
156
+ raise HTTP::Error, "#{last_error}: It is likely your server has crashed."
157
+ elsif last_error.is_a?(SocketError)
158
+ raise HTTP::Error, "#{last_error}: Did your server start and is it on the same network?"
159
+ end
160
+
161
+ raise HTTP::Error, last_error
162
+ end
163
+ end
164
+ end
165
+ end
166
+
@@ -0,0 +1,17 @@
1
+ module RunLoop
2
+ module HTTP
3
+
4
+ # A representation of the RunLoop test server.
5
+ # @!visibility private
6
+ class Server
7
+ attr_reader :endpoint
8
+
9
+ # @param [URI] endpoint The endpoint to reach the test server.
10
+ # running on the device. The port should be included in the URI.
11
+ def initialize(endpoint)
12
+ @endpoint = endpoint
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,436 @@
1
+ module RunLoop
2
+
3
+ # A class for interacting with the instruments command-line tool
4
+ #
5
+ # @note All instruments commands are run in the context of `xcrun`.
6
+ class Instruments
7
+
8
+ include RunLoop::Regex
9
+
10
+ # @!visibility private
11
+ #
12
+ # Rotates xrtmp__ directories in /Library/Caches/com.apple.dt.instruments
13
+ # keeping the last 5. On CI systems these can be hundreds of gigabytes.
14
+ def self.rotate_cache_directories
15
+
16
+ # Never run on the XTC
17
+ return :xtc if RunLoop::Environment.xtc?
18
+
19
+ cache = self.library_cache_dir
20
+
21
+ # If the directory does not exist, do nothing.
22
+ return :no_cache if !cache || !File.exist?(cache)
23
+
24
+ start = Time.now
25
+
26
+ glob = "#{cache}/xrtmp__*"
27
+
28
+ RunLoop.log_debug("Searching for instruments caches with glob: #{glob}")
29
+
30
+ directories = Dir.glob(glob).select do |path|
31
+ File.directory?(path)
32
+ end
33
+
34
+ log_progress = false
35
+ if directories.count > 25
36
+ RunLoop.log_info2("Found #{directories.count} instruments caches: ~#{20 * directories.count} Mb")
37
+ RunLoop.log_info2("Deleting them could take a long time.")
38
+ RunLoop.log_info2("This delay will only happen once!")
39
+ RunLoop.log_info2("Please be patient and allow the directories to be deleted")
40
+ log_progress = true
41
+ else
42
+ RunLoop.log_debug("Found #{directories.count} instruments caches")
43
+ end
44
+
45
+ if log_progress
46
+ RunLoop.log_info2("Sorting instruments caches by modification time...")
47
+ end
48
+
49
+ oldest_first = directories.sort_by { |f| File.mtime(f) }
50
+
51
+ oldest_first.pop(5)
52
+
53
+ if log_progress
54
+ RunLoop.log_info2("Will delete #{oldest_first.count} instruments caches...")
55
+ else
56
+ RunLoop.log_debug("Will delete #{oldest_first.count} instruments caches")
57
+ end
58
+
59
+ oldest_first.each do |path|
60
+ FileUtils.rm_rf(path)
61
+ if log_progress
62
+ printf "."
63
+ end
64
+ end
65
+
66
+ elapsed = Time.now - start
67
+
68
+ if log_progress
69
+ puts ""
70
+ RunLoop.log_info2("Deleted #{oldest_first.count} instruments caches in #{elapsed} seconds")
71
+ else
72
+ RunLoop.log_debug("Deleted #{oldest_first.count} instruments caches in #{elapsed} seconds")
73
+ end
74
+ true
75
+ end
76
+
77
+ attr_reader :xcode
78
+
79
+ def pbuddy
80
+ @pbuddy ||= RunLoop::PlistBuddy.new
81
+ end
82
+
83
+ def xcode
84
+ @xcode ||= RunLoop::Xcode.new
85
+ end
86
+
87
+ def xcrun
88
+ RunLoop::Xcrun.new
89
+ end
90
+
91
+ # @!visibility private
92
+ def to_s
93
+ "#<Instruments #{version.to_s}>"
94
+ end
95
+
96
+ # @!visibility private
97
+ def inspect
98
+ to_s
99
+ end
100
+
101
+ # Returns an Array of instruments process ids.
102
+ #
103
+ # @note The `block` parameter is included for legacy API and will be
104
+ # deprecated. Replace your existing calls with with .each or .map. The
105
+ # block argument makes this method hard to mock.
106
+ # @return [Array<Integer>] An array of instruments process ids.
107
+ def instruments_pids(&block)
108
+ pids = pids_from_ps_output
109
+ if block_given?
110
+ pids.each do |pid|
111
+ block.call(pid)
112
+ end
113
+ else
114
+ pids
115
+ end
116
+ end
117
+
118
+ # Are there any instruments processes running?
119
+ # @return [Boolean] True if there is are any instruments processes running.
120
+ def instruments_running?
121
+ instruments_pids.count > 0
122
+ end
123
+
124
+ # Send a kill signal to any running `instruments` processes.
125
+ #
126
+ # Only one instruments process can be running at any one time.
127
+ def kill_instruments(_=nil)
128
+ instruments_pids.each do |pid|
129
+ terminator = RunLoop::ProcessTerminator.new(pid, "QUIT", "instruments")
130
+ unless terminator.kill_process
131
+ terminator = RunLoop::ProcessTerminator.new(pid, "KILL", "instruments")
132
+ terminator.kill_process
133
+ end
134
+ end
135
+ end
136
+
137
+ # Is the Instruments.app running?
138
+ #
139
+ # If the Instruments.app is running, the instruments command line tool
140
+ # cannot take control of applications.
141
+ def instruments_app_running?
142
+ ps_output = `ps x -o pid,comm | grep Instruments.app | grep -v grep`.strip
143
+ if ps_output[/Instruments\.app/, 0]
144
+ true
145
+ else
146
+ false
147
+ end
148
+ end
149
+
150
+ # Spawn a new instruments process in the context of `xcrun` and detach.
151
+ #
152
+ # @param [String] automation_template The template instruments will use when
153
+ # launching the application.
154
+ # @param [Hash] options The launch options.
155
+ # @param [String] log_file The file to log to.
156
+ # @return [Integer] Returns the process id of the instruments process.
157
+ # @todo Do I need to enumerate the launch options in the docs?
158
+ # @todo Should this raise errors?
159
+ # @todo Is this jruby compatible?
160
+ def spawn(automation_template, options, log_file)
161
+ splat_args = spawn_arguments(automation_template, options)
162
+ logger = options[:logger]
163
+ RunLoop::Logging.log_debug(logger, "xcrun #{splat_args.join(' ')} >& #{log_file}")
164
+ pid = Process.spawn('xcrun', *splat_args, {:out => log_file, :err => log_file})
165
+ Process.detach(pid)
166
+ pid.to_i
167
+ end
168
+
169
+ # Returns the instruments version.
170
+ # @return [RunLoop::Version] A version object.
171
+ def version
172
+ @instruments_version ||= lambda do
173
+ version_string = pbuddy.plist_read('CFBundleShortVersionString',
174
+ path_to_instruments_app_plist)
175
+ RunLoop::Version.new(version_string)
176
+ end.call
177
+ end
178
+
179
+ # Returns an array of Instruments.app templates.
180
+ #
181
+ # Depending on the Xcode version Instruments.app templates will either be:
182
+ #
183
+ # * A full path to the template. # Xcode 5 and Xcode > 5 betas
184
+ # * The name of a template. # Xcode >= 6 (non beta)
185
+ #
186
+ # **Maintainers!** The rules above are important and explain why we can't
187
+ # simply filter by `~= /tracetemplate/`.
188
+ #
189
+ # Templates that users have saved will always be full paths - regardless
190
+ # of the Xcode version.
191
+ #
192
+ # @return [Array<String>] Instruments.app templates.
193
+ def templates
194
+ @instruments_templates ||= lambda do
195
+ args = ['instruments', '-s', 'templates']
196
+ hash = xcrun.run_command_in_context(args, log_cmd: true)
197
+ hash[:out].chomp.split("\n").map do |elm|
198
+ stripped = elm.strip.tr('"', '')
199
+ if stripped == '' || stripped == 'Known Templates:'
200
+ nil
201
+ else
202
+ stripped
203
+ end
204
+ end.compact
205
+ end.call
206
+ end
207
+
208
+ # Returns an array of the available physical devices.
209
+ #
210
+ # @return [Array<RunLoop::Device>] All the devices will be physical
211
+ # devices.
212
+ def physical_devices
213
+ @instruments_physical_devices ||= lambda do
214
+ fetch_devices[:out].chomp.split("\n").map do |line|
215
+ udid = line[DEVICE_UDID_REGEX, 0]
216
+ if udid
217
+ version = line[VERSION_REGEX, 0]
218
+ name = line.split('(').first.strip
219
+ RunLoop::Device.new(name, version, udid)
220
+ else
221
+ nil
222
+ end
223
+ end.compact
224
+ end.call
225
+ end
226
+
227
+ # Returns an array of the available simulators.
228
+ #
229
+ # **Xcode 5.1**
230
+ # * iPad Retina - Simulator - iOS 7.1
231
+ #
232
+ # **Xcode 6**
233
+ # * iPad Retina (8.3 Simulator) [EA79555F-ADB4-4D75-930C-A745EAC8FA8B]
234
+ #
235
+ # **Xcode 7**
236
+ # * iPhone 6 (9.0) [3EDC9C6E-3096-48BF-BCEC-7A5CAF8AA706]
237
+ # * iPhone 6 (9.0) + Apple Watch - 38mm (2.0) [EE3C200C-69BA-4816-A087-0457C5FCEDA0]
238
+ #
239
+ # @return [Array<RunLoop::Device>] All the devices will be simulators.
240
+ def simulators
241
+ @instruments_simulators ||= lambda do
242
+ fetch_devices[:out].chomp.split("\n").map do |line|
243
+ stripped = line.strip
244
+ if line_is_simulator?(stripped) &&
245
+ !line_is_simulator_paired_with_watch?(stripped) &&
246
+ !line_is_apple_tv?(stripped)
247
+
248
+ version = stripped[VERSION_REGEX, 0]
249
+
250
+ if line_is_xcode5_simulator?(stripped)
251
+ name = line
252
+ udid = line
253
+ else
254
+ name = stripped.split('(').first.strip
255
+ udid = line[CORE_SIMULATOR_UDID_REGEX, 0]
256
+ end
257
+
258
+ RunLoop::Device.new(name, version, udid)
259
+ else
260
+ nil
261
+ end
262
+ end.compact
263
+ end.call
264
+ end
265
+
266
+ private
267
+
268
+ # @!visibility private
269
+ def fetch_devices
270
+ @device_hash ||= lambda do
271
+ args = ['instruments', '-s', 'devices']
272
+ xcrun.run_command_in_context(args, log_cmd: true)
273
+ end.call
274
+ end
275
+
276
+ # @!visibility private
277
+ #
278
+ # ```
279
+ # $ ps x -o pid,command | grep -v grep | grep instruments
280
+ # 98081 sh -c xcrun instruments -w "43be3f89d9587e9468c24672777ff6241bd91124" < args >
281
+ # 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args >
282
+ # ```
283
+ #
284
+ # When run from run-loop (via rspec), expect this:
285
+ #
286
+ # ```
287
+ # $ ps x -o pid,command | grep -v grep | grep instruments
288
+ # 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args >
289
+ # ```
290
+ INSTRUMENTS_FIND_PIDS_CMD = 'ps x -o pid,command | grep -v grep | grep instruments'
291
+
292
+ # @!visibility private
293
+ # Parses the run-loop options hash into an array of arguments that can be
294
+ # passed to `Process.spawn` to launch instruments.
295
+ def spawn_arguments(automation_template, options)
296
+ array = ['instruments']
297
+ array << '-w'
298
+
299
+ array << options[:udid]
300
+
301
+ trace = options[:results_dir_trace]
302
+ if trace
303
+ array << '-D'
304
+ array << trace
305
+ end
306
+
307
+ array << '-t'
308
+ array << automation_template
309
+
310
+ array << options[:bundle_id]
311
+
312
+ {
313
+ 'UIARESULTSPATH' => options[:results_dir],
314
+ 'UIASCRIPT' => options[:script]
315
+ }.each do |key, value|
316
+ array << '-e'
317
+ array << key
318
+ array << value
319
+ end
320
+ array + options.fetch(:args, [])
321
+ end
322
+
323
+ # @!visibility private
324
+ #
325
+ # Executes `ps_cmd` to find instruments processes and returns the result.
326
+ #
327
+ # @param [String] ps_cmd The Unix ps command to execute to find instruments
328
+ # processes.
329
+ # @return [String] A ps-style list of process details. The details returned
330
+ # are controlled by the `ps_cmd`.
331
+ def ps_for_instruments(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
332
+ `#{ps_cmd}`.strip
333
+ end
334
+
335
+ # @!visibility private
336
+ # Is the process described an instruments process?
337
+ #
338
+ # @param [String] ps_details Details about a process as returned by `ps`
339
+ # @return [Boolean] True if the details describe an instruments process.
340
+ def is_instruments_process?(ps_details)
341
+ return false if ps_details.nil?
342
+ ps_details[/\/usr\/bin\/instruments/, 0] != nil
343
+ end
344
+
345
+ # @!visibility private
346
+ # Extracts an Array of integer process ids from the output of executing
347
+ # the Unix `ps_cmd`.
348
+ #
349
+ # @param [String] ps_cmd The Unix `ps` command used to find instruments
350
+ # processes.
351
+ # @return [Array<Integer>] An array of integer pids for instruments
352
+ # processes. Returns an empty list if no instruments process are found.
353
+ def pids_from_ps_output(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
354
+ ps_output = ps_for_instruments(ps_cmd)
355
+ lines = ps_output.lines("\n").map { |line| line.strip }
356
+ lines.map do |line|
357
+ tokens = line.strip.split(' ').map { |token| token.strip }
358
+ pid = tokens.fetch(0, nil)
359
+ process_description = tokens[1..-1].join(' ')
360
+ if is_instruments_process? process_description
361
+ pid.to_i
362
+ else
363
+ nil
364
+ end
365
+ end.compact.sort
366
+ end
367
+
368
+ # @!visibility private
369
+ #
370
+ # Execute an instruments command.
371
+ # @param [Array] args An array of arguments
372
+ def execute_command(args)
373
+ Open3.popen3('xcrun', 'instruments', *args) do |_, stdout, stderr, process_status|
374
+ yield stdout, stderr, process_status
375
+ end
376
+ end
377
+
378
+ # @!visibility private
379
+ def line_is_simulator?(line)
380
+ line_is_core_simulator?(line) || line_is_xcode5_simulator?(line)
381
+ end
382
+
383
+ # @!visibility private
384
+ def line_is_xcode5_simulator?(line)
385
+ !line[CORE_SIMULATOR_UDID_REGEX, 0] && line[/Simulator/, 0]
386
+ end
387
+
388
+ # @!visibility private
389
+ def line_is_core_simulator?(line)
390
+ return nil if !line_has_a_version?(line)
391
+
392
+ line[CORE_SIMULATOR_UDID_REGEX, 0]
393
+ end
394
+
395
+ # @!visibility private
396
+ def line_has_a_version?(line)
397
+ line[VERSION_REGEX, 0]
398
+ end
399
+
400
+ # @!visibility private
401
+ def line_is_simulator_paired_with_watch?(line)
402
+ line[CORE_SIMULATOR_UDID_REGEX, 0] && line[/Apple Watch/, 0]
403
+ end
404
+
405
+ # @!visibility private
406
+ def line_is_apple_tv?(line)
407
+ line[/Apple TV/, 0]
408
+ end
409
+
410
+ # @!visibility private
411
+ def path_to_instruments_app_plist
412
+ @path_to_instruments_app_plist ||=
413
+ File.expand_path(File.join(xcode.developer_dir,
414
+ '..',
415
+ 'Applications',
416
+ 'Instruments.app',
417
+ 'Contents',
418
+ 'Info.plist'))
419
+ end
420
+
421
+ # @!visibility private
422
+ #
423
+ # Instruments caches files in this directory and it can become quite large
424
+ # over time; particularly on CI system.
425
+ def self.library_cache_dir
426
+ path = "/Library/Caches/com.apple.dt.instruments"
427
+
428
+ if File.exist?(path)
429
+ path
430
+ else
431
+ nil
432
+ end
433
+ end
434
+ end
435
+ end
436
+