fastlane 2.148.0 → 2.150.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -86
  3. data/deliver/lib/deliver.rb +0 -1
  4. data/deliver/lib/deliver/app_screenshot.rb +28 -27
  5. data/deliver/lib/deliver/options.rb +6 -11
  6. data/deliver/lib/deliver/runner.rb +7 -21
  7. data/deliver/lib/deliver/setup.rb +5 -30
  8. data/deliver/lib/deliver/submit_for_review.rb +155 -90
  9. data/deliver/lib/deliver/upload_metadata.rb +355 -143
  10. data/deliver/lib/deliver/upload_price_tier.rb +22 -8
  11. data/deliver/lib/deliver/upload_screenshots.rb +112 -37
  12. data/fastlane/lib/assets/s3_html_template.erb +1 -1
  13. data/fastlane/lib/fastlane/actions/docs/capture_android_screenshots.md +42 -2
  14. data/fastlane/lib/fastlane/actions/docs/capture_ios_screenshots.md +1 -1
  15. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +77 -96
  16. data/fastlane/lib/fastlane/actions/docs/upload_to_play_store.md +3 -2
  17. data/fastlane/lib/fastlane/actions/download_dsyms.rb +7 -1
  18. data/fastlane/lib/fastlane/actions/google_play_track_release_names.rb +74 -0
  19. data/fastlane/lib/fastlane/actions/hipchat.rb +1 -1
  20. data/fastlane/lib/fastlane/actions/set_changelog.rb +23 -20
  21. data/fastlane/lib/fastlane/actions/slack.rb +2 -2
  22. data/fastlane/lib/fastlane/actions/slather.rb +8 -1
  23. data/fastlane/lib/fastlane/actions/spm.rb +7 -0
  24. data/fastlane/lib/fastlane/actions/upload_symbols_to_crashlytics.rb +2 -33
  25. data/fastlane/lib/fastlane/documentation/actions_list.rb +1 -1
  26. data/fastlane/lib/fastlane/lane.rb +3 -3
  27. data/fastlane/lib/fastlane/plugins/plugin_manager.rb +1 -1
  28. data/fastlane/lib/fastlane/plugins/template/.github/workflows/test.yml +29 -0
  29. data/fastlane/lib/fastlane/swift_fastlane_function.rb +22 -5
  30. data/fastlane/lib/fastlane/version.rb +1 -1
  31. data/fastlane/swift/ControlCommand.swift +1 -0
  32. data/fastlane/swift/Deliverfile.swift +1 -1
  33. data/fastlane/swift/Fastlane.swift +79 -22
  34. data/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/xcshareddata/xcschemes/FastlaneRunner.xcscheme +3 -9
  35. data/fastlane/swift/Gymfile.swift +1 -1
  36. data/fastlane/swift/GymfileProtocol.swift +1 -1
  37. data/fastlane/swift/LaneFileProtocol.swift +2 -5
  38. data/fastlane/swift/Matchfile.swift +1 -1
  39. data/fastlane/swift/MatchfileProtocol.swift +1 -1
  40. data/fastlane/swift/Precheckfile.swift +1 -1
  41. data/fastlane/swift/RubyCommand.swift +29 -6
  42. data/fastlane/swift/RubyCommandable.swift +1 -0
  43. data/fastlane/swift/Runner.swift +85 -13
  44. data/fastlane/swift/Scanfile.swift +1 -1
  45. data/fastlane/swift/ScanfileProtocol.swift +2 -2
  46. data/fastlane/swift/Screengrabfile.swift +1 -1
  47. data/fastlane/swift/Snapshotfile.swift +1 -1
  48. data/fastlane/swift/SnapshotfileProtocol.swift +9 -1
  49. data/fastlane/swift/SocketClient.swift +76 -45
  50. data/fastlane/swift/SocketClientDelegateProtocol.swift +1 -1
  51. data/fastlane/swift/SocketResponse.swift +1 -0
  52. data/fastlane_core/lib/fastlane_core/configuration/config_item.rb +1 -3
  53. data/fastlane_core/lib/fastlane_core/pkg_file_analyser.rb +7 -0
  54. data/frameit/lib/frameit/device_types.rb +100 -100
  55. data/gym/lib/gym/options.rb +1 -1
  56. data/match/lib/match/nuke.rb +21 -16
  57. data/match/lib/match/storage/git_storage.rb +4 -0
  58. data/match/lib/match/storage/google_cloud_storage.rb +4 -0
  59. data/match/lib/match/storage/interface.rb +4 -0
  60. data/match/lib/match/storage/s3_storage.rb +4 -0
  61. data/produce/lib/produce/itunes_connect.rb +32 -21
  62. data/produce/lib/produce/options.rb +3 -3
  63. data/scan/lib/scan/options.rb +1 -1
  64. data/scan/lib/scan/test_result_parser.rb +9 -2
  65. data/sigh/lib/assets/resign.sh +7 -7
  66. data/snapshot/lib/assets/SnapshotHelper.swift +5 -5
  67. data/snapshot/lib/assets/SnapshotHelperXcode8.swift +3 -3
  68. data/snapshot/lib/snapshot/options.rb +11 -1
  69. data/snapshot/lib/snapshot/reports_generator.rb +8 -1
  70. data/snapshot/lib/snapshot/test_command_generator.rb +8 -3
  71. data/spaceship/lib/spaceship/.DS_Store +0 -0
  72. data/spaceship/lib/spaceship/client.rb +13 -4
  73. data/spaceship/lib/spaceship/connect_api.rb +25 -2
  74. data/spaceship/lib/spaceship/connect_api/client.rb +97 -31
  75. data/spaceship/lib/spaceship/connect_api/file_uploader.rb +66 -0
  76. data/spaceship/lib/spaceship/connect_api/model.rb +1 -1
  77. data/spaceship/lib/spaceship/connect_api/models/age_rating_declaration.rb +113 -0
  78. data/spaceship/lib/spaceship/connect_api/models/app.rb +117 -3
  79. data/spaceship/lib/spaceship/connect_api/models/app_category.rb +94 -0
  80. data/spaceship/lib/spaceship/connect_api/models/app_info.rb +66 -0
  81. data/spaceship/lib/spaceship/connect_api/models/app_info_localization.rb +38 -0
  82. data/spaceship/lib/spaceship/connect_api/models/app_preview.rb +77 -0
  83. data/spaceship/lib/spaceship/connect_api/models/app_preview_set.rb +71 -0
  84. data/spaceship/lib/spaceship/connect_api/models/app_price.rb +22 -0
  85. data/spaceship/lib/spaceship/connect_api/models/app_price_tier.rb +12 -0
  86. data/spaceship/lib/spaceship/connect_api/models/app_review_attachment.rb +81 -0
  87. data/spaceship/lib/spaceship/connect_api/models/app_screenshot.rb +97 -0
  88. data/spaceship/lib/spaceship/connect_api/models/app_screenshot_set.rb +101 -0
  89. data/spaceship/lib/spaceship/connect_api/models/app_store_review_detail.rb +51 -0
  90. data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +182 -0
  91. data/spaceship/lib/spaceship/connect_api/models/app_store_version_localization.rb +86 -0
  92. data/spaceship/lib/spaceship/connect_api/models/app_store_version_phased_release.rb +36 -0
  93. data/spaceship/lib/spaceship/connect_api/models/app_store_version_submission.rb +26 -0
  94. data/spaceship/lib/spaceship/connect_api/models/build.rb +4 -0
  95. data/spaceship/lib/spaceship/connect_api/models/idfa_declaration.rb +40 -0
  96. data/spaceship/lib/spaceship/connect_api/models/reset_ratings_request.rb +26 -0
  97. data/spaceship/lib/spaceship/connect_api/models/territory.rb +27 -0
  98. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +10 -3
  99. data/spaceship/lib/spaceship/connect_api/tunes/client.rb +33 -0
  100. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +832 -0
  101. data/spaceship/lib/spaceship/errors.rb +3 -0
  102. data/spaceship/lib/spaceship/spaceauth_runner.rb +2 -2
  103. data/supply/lib/supply/client.rb +19 -0
  104. data/supply/lib/supply/reader.rb +16 -0
  105. metadata +50 -42
  106. data/deliver/lib/deliver/upload_assets.rb +0 -27
  107. data/snapshot/lib/snapshot/.options.rb.swp +0 -0
  108. data/snapshot/lib/snapshot/.test_command_generator_base.rb.swp +0 -0
@@ -18,4 +18,4 @@ class Matchfile: MatchfileProtocol {
18
18
 
19
19
 
20
20
 
21
- // Generated with fastlane 2.148.0
21
+ // Generated with fastlane 2.149.1
@@ -154,4 +154,4 @@ extension MatchfileProtocol {
154
154
 
155
155
  // Please don't remove the lines below
156
156
  // They are used to detect outdated files
157
- // FastlaneRunnerAPIVersion [0.9.17]
157
+ // FastlaneRunnerAPIVersion [0.9.12]
@@ -18,4 +18,4 @@ class Precheckfile: PrecheckfileProtocol {
18
18
 
19
19
 
20
20
 
21
- // Generated with fastlane 2.148.0
21
+ // Generated with fastlane 2.149.1
@@ -82,27 +82,50 @@ struct RubyCommand: RubyCommandable {
82
82
  let methodName: String
83
83
  let className: String?
84
84
  let args: [Argument]
85
+ let id: String = UUID().uuidString
86
+
87
+ var closure: ((String) -> Void)? {
88
+ let callbacks = self.args.filter { ($0.type != nil) && $0.type == .stringClosure }
89
+ guard let callback = callbacks.first else {
90
+ return nil
91
+ }
92
+
93
+ guard let callbackArgValue = callback.value else {
94
+ return nil
95
+ }
85
96
 
86
- func performCallback(callbackArg: String) {
97
+ guard let callbackClosure = callbackArgValue as? ((String) -> Void) else {
98
+ return nil
99
+ }
100
+ return callbackClosure
101
+ }
102
+
103
+
104
+ func callbackClosure(_ callbackArg: String) -> ((String) -> Void)? {
87
105
  // WARNING: This will perform the first callback it receives
88
106
  let callbacks = self.args.filter { ($0.type != nil) && $0.type == .stringClosure }
89
107
  guard let callback = callbacks.first else {
90
108
  verbose(message: "received call to performCallback with \(callbackArg), but no callback available to perform")
91
- return
109
+ return nil
92
110
  }
93
111
 
94
112
  guard let callbackArgValue = callback.value else {
95
113
  verbose(message: "received call to performCallback with \(callbackArg), but callback is nil")
96
- return
114
+ return nil
97
115
  }
98
116
 
99
117
  guard let callbackClosure = callbackArgValue as? ((String) -> Void) else {
100
118
  verbose(message: "received call to performCallback with \(callbackArg), but callback type is unknown \(callbackArgValue.self)")
101
- return
119
+ return nil
102
120
  }
121
+ return callbackClosure
122
+ }
103
123
 
104
- print("Performing callback with: \(callbackArg)")
105
- callbackClosure(callbackArg)
124
+ func performCallback(callbackArg: String, socket: SocketClient, completion: @escaping () -> Void) {
125
+ verbose(message: "Performing callback with: \(callbackArg)")
126
+ socket.leave()
127
+ self.callbackClosure(callbackArg)?(callbackArg)
128
+ completion()
106
129
  }
107
130
 
108
131
  var commandJson: String {
@@ -31,6 +31,7 @@ enum CommandType {
31
31
  protocol RubyCommandable {
32
32
  var type: CommandType { get }
33
33
  var commandJson: String { get }
34
+ var id: String { get }
34
35
  }
35
36
 
36
37
  extension RubyCommandable {
@@ -33,28 +33,69 @@ class Runner {
33
33
  fileprivate var returnValue: String? // lol, so safe
34
34
  fileprivate var currentlyExecutingCommand: RubyCommandable? = nil
35
35
  fileprivate var shouldLeaveDispatchGroupDuringDisconnect = false
36
+ fileprivate var executeNext: [String: Bool] = [:]
36
37
 
37
38
  func executeCommand(_ command: RubyCommandable) -> String {
38
39
  self.dispatchGroup.enter()
39
- currentlyExecutingCommand = command
40
- socketClient.send(rubyCommand: command)
40
+ self.currentlyExecutingCommand = command
41
+ self.socketClient.send(rubyCommand: command)
41
42
 
42
43
  let secondsToWait = DispatchTimeInterval.seconds(SocketClient.defaultCommandTimeoutSeconds)
43
- let connectTimeout = DispatchTime.now() + secondsToWait
44
- let timeoutResult = self.dispatchGroup.wait(timeout: connectTimeout)
44
+ let timeoutResult = waitWithPolling(self.executeNext[command.id], toEventually: { $0 == true }, timeout: SocketClient.defaultCommandTimeoutSeconds)
45
+ executeNext.removeValue(forKey: command.id)
45
46
  let failureMessage = "command didn't execute in: \(SocketClient.defaultCommandTimeoutSeconds) seconds"
46
- let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait)
47
+ let success = self.testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait)
47
48
  guard success else {
48
49
  log(message: "command timeout")
49
50
  fatalError()
50
51
  }
51
52
 
52
- if let returnValue = self.returnValue {
53
- return returnValue
53
+ if let _returnValue = self.returnValue {
54
+ return _returnValue
54
55
  } else {
55
56
  return ""
56
57
  }
57
58
  }
59
+
60
+ private func waitWithPolling<T>(_ expression: @autoclosure @escaping () throws -> T, toEventually predicate: @escaping (T) -> Bool, timeout: Int, pollingInterval: DispatchTimeInterval = .milliseconds(4)) -> DispatchTimeoutResult {
61
+ func memoizedClosure<T>(_ closure: @escaping () throws -> T) -> (Bool) throws -> T {
62
+ var cache: T?
63
+ return { withoutCaching in
64
+ if withoutCaching || cache == nil {
65
+ cache = try closure()
66
+ }
67
+ guard let cache = cache else {
68
+ preconditionFailure()
69
+ }
70
+
71
+ return cache
72
+ }
73
+ }
74
+
75
+ let runLoop = RunLoop.current
76
+ let timeoutDate = Date(timeInterval: TimeInterval(timeout), since: Date())
77
+ var fulfilled: Bool = false
78
+ let _expression = memoizedClosure(expression)
79
+ repeat {
80
+ do {
81
+ let exp = try _expression(true)
82
+ fulfilled = predicate(exp)
83
+ } catch {
84
+ fatalError("Error raised \(error.localizedDescription)")
85
+ }
86
+ if !fulfilled {
87
+ runLoop.run(until: Date(timeIntervalSinceNow: pollingInterval.timeInterval))
88
+ } else {
89
+ break
90
+ }
91
+ } while Date().compare(timeoutDate) == .orderedAscending
92
+
93
+ if fulfilled {
94
+ return .success
95
+ } else {
96
+ return .timedOut
97
+ }
98
+ }
58
99
  }
59
100
 
60
101
  // Handle threading stuff
@@ -110,25 +151,36 @@ extension Runner {
110
151
  }
111
152
 
112
153
  extension Runner : SocketClientDelegateProtocol {
113
- func commandExecuted(serverResponse: SocketClientResponse) {
154
+ func commandExecuted(serverResponse: SocketClientResponse, completion: (SocketClient) -> Void) {
114
155
  switch serverResponse {
115
156
  case .success(let returnedObject, let closureArgumentValue):
116
157
  verbose(message: "command executed")
117
158
  self.returnValue = returnedObject
118
159
  if let command = self.currentlyExecutingCommand as? RubyCommand {
119
- if let closureArgumentValue = closureArgumentValue {
120
- command.performCallback(callbackArg: closureArgumentValue)
160
+ if let closureArgumentValue = closureArgumentValue, !closureArgumentValue.isEmpty {
161
+ command.performCallback(callbackArg: closureArgumentValue, socket: socketClient) {
162
+ self.executeNext[command.id] = true
163
+ }
164
+ } else {
165
+ self.executeNext[command.id] = true
121
166
  }
122
167
  }
123
- self.dispatchGroup.leave()
168
+ dispatchGroup.leave()
169
+ completion(socketClient)
124
170
  case .clientInitiatedCancelAcknowledged:
125
171
  verbose(message: "server acknowledged a cancel request")
126
172
  self.dispatchGroup.leave()
127
-
173
+ if let command = self.currentlyExecutingCommand as? RubyCommand {
174
+ self.executeNext[command.id] = true
175
+ }
176
+ completion(socketClient)
128
177
  case .alreadyClosedSockets, .connectionFailure, .malformedRequest, .malformedResponse, .serverError:
129
178
  log(message: "error encountered while executing command:\n\(serverResponse)")
130
179
  self.dispatchGroup.leave()
131
-
180
+ if let command = self.currentlyExecutingCommand as? RubyCommand {
181
+ self.executeNext[command.id] = true
182
+ }
183
+ completion(socketClient)
132
184
  case .commandTimeout(let timeout):
133
185
  log(message: "Runner timed out after \(timeout) second(s)")
134
186
  }
@@ -144,6 +196,7 @@ extension Runner : SocketClientDelegateProtocol {
144
196
  DispatchQueue.main.async {
145
197
  self.thread?.cancel()
146
198
  self.thread = nil
199
+ self.socketClient.closeSession()
147
200
  self.socketClient = nil
148
201
  verbose(message: "connection closed!")
149
202
  if self.shouldLeaveDispatchGroupDuringDisconnect {
@@ -194,6 +247,25 @@ func verbose(message: String) {
194
247
  logger.verbose(message: message)
195
248
  }
196
249
 
250
+ private extension DispatchTimeInterval {
251
+ var timeInterval: TimeInterval {
252
+ var result: TimeInterval = 0
253
+ switch self {
254
+ case .seconds(let value):
255
+ result = TimeInterval(value)
256
+ case .milliseconds(let value):
257
+ result = TimeInterval(value)*0.001
258
+ case .microseconds(let value):
259
+ result = TimeInterval(value)*0.000001
260
+ case .nanoseconds(let value):
261
+ result = TimeInterval(value)*0.000000001
262
+ case .never:
263
+ fatalError()
264
+ }
265
+ return result
266
+ }
267
+ }
268
+
197
269
  // Please don't remove the lines below
198
270
  // They are used to detect outdated files
199
271
  // FastlaneRunnerAPIVersion [0.9.2]
@@ -18,4 +18,4 @@ class Scanfile: ScanfileProtocol {
18
18
 
19
19
 
20
20
 
21
- // Generated with fastlane 2.148.0
21
+ // Generated with fastlane 2.149.1
@@ -246,7 +246,7 @@ extension ScanfileProtocol {
246
246
  var slackMessage: String? { return nil }
247
247
  var slackUseWebhookConfiguredUsernameAndIcon: Bool { return false }
248
248
  var slackUsername: String { return "fastlane" }
249
- var slackIconUrl: String { return "https://s3-eu-west-1.amazonaws.com/fastlane.tools/fastlane.png" }
249
+ var slackIconUrl: String { return "https://fastlane.tools/assets/img/fastlane_icon.png" }
250
250
  var skipSlack: Bool { return false }
251
251
  var slackOnlyOnFailure: Bool { return false }
252
252
  var destination: String? { return nil }
@@ -258,4 +258,4 @@ extension ScanfileProtocol {
258
258
 
259
259
  // Please don't remove the lines below
260
260
  // They are used to detect outdated files
261
- // FastlaneRunnerAPIVersion [0.9.29]
261
+ // FastlaneRunnerAPIVersion [0.9.23]
@@ -18,4 +18,4 @@ class Screengrabfile: ScreengrabfileProtocol {
18
18
 
19
19
 
20
20
 
21
- // Generated with fastlane 2.148.0
21
+ // Generated with fastlane 2.149.1
@@ -18,4 +18,4 @@ class Snapshotfile: SnapshotfileProtocol {
18
18
 
19
19
 
20
20
 
21
- // Generated with fastlane 2.148.0
21
+ // Generated with fastlane 2.149.1
@@ -63,6 +63,9 @@ protocol SnapshotfileProtocol: class {
63
63
  /// A list of videos that should be added to the simulator before running the application
64
64
  var addVideos: [String]? { get }
65
65
 
66
+ /// A path to screenshots.html template
67
+ var htmlTemplate: String? { get }
68
+
66
69
  /// The directory where to store the build log
67
70
  var buildlogPath: String { get }
68
71
 
@@ -119,6 +122,9 @@ protocol SnapshotfileProtocol: class {
119
122
 
120
123
  /// Array of strings matching Test Bundle/Test Suite/Test Cases to skip
121
124
  var skipTesting: String? { get }
125
+
126
+ /// Disable xcpretty formatting of build
127
+ var disableXcpretty: Bool? { get }
122
128
  }
123
129
 
124
130
  extension SnapshotfileProtocol {
@@ -143,6 +149,7 @@ extension SnapshotfileProtocol {
143
149
  var appIdentifier: String? { return nil }
144
150
  var addPhotos: [String]? { return nil }
145
151
  var addVideos: [String]? { return nil }
152
+ var htmlTemplate: String? { return nil }
146
153
  var buildlogPath: String { return "~/Library/Logs/snapshot" }
147
154
  var clean: Bool { return false }
148
155
  var testWithoutBuilding: Bool? { return nil }
@@ -162,8 +169,9 @@ extension SnapshotfileProtocol {
162
169
  var testplan: String? { return nil }
163
170
  var onlyTesting: String? { return nil }
164
171
  var skipTesting: String? { return nil }
172
+ var disableXcpretty: Bool? { return nil }
165
173
  }
166
174
 
167
175
  // Please don't remove the lines below
168
176
  // They are used to detect outdated files
169
- // FastlaneRunnerAPIVersion [0.9.9]
177
+ // FastlaneRunnerAPIVersion [0.9.7]
@@ -13,6 +13,7 @@
13
13
  //
14
14
 
15
15
  import Foundation
16
+ import Dispatch
16
17
 
17
18
  public enum SocketClientResponse: Error {
18
19
  case alreadyClosedSockets
@@ -41,8 +42,12 @@ class SocketClient: NSObject {
41
42
  fileprivate var outputStream: OutputStream!
42
43
  fileprivate var cleaningUpAfterDone = false
43
44
  fileprivate let dispatchGroup: DispatchGroup = DispatchGroup()
45
+ fileprivate let readSemaphore = DispatchSemaphore(value: 1)
46
+ fileprivate let writeSemaphore = DispatchSemaphore(value: 1)
44
47
  fileprivate let commandTimeoutSeconds: Int
45
48
 
49
+ private let writeQueue: DispatchQueue
50
+ private let readQueue: DispatchQueue
46
51
  private let streamQueue: DispatchQueue
47
52
  private let host: String
48
53
  private let port: UInt32
@@ -58,7 +63,9 @@ class SocketClient: NSObject {
58
63
  self.host = host
59
64
  self.port = port
60
65
  self.commandTimeoutSeconds = commandTimeoutSeconds
61
- self.streamQueue = DispatchQueue(label: "streamQueue")
66
+ self.readQueue = DispatchQueue(label: "readQueue", qos: .background, attributes: .concurrent)
67
+ self.writeQueue = DispatchQueue(label: "writeQueue", qos: .background, attributes: .concurrent)
68
+ self.streamQueue = DispatchQueue.global(qos: .background)
62
69
  self.socketStatus = .closed
63
70
  self.socketDelegate = socketDelegate
64
71
  super.init()
@@ -68,7 +75,7 @@ class SocketClient: NSObject {
68
75
  var readStream: Unmanaged<CFReadStream>?
69
76
  var writeStream: Unmanaged<CFWriteStream>?
70
77
 
71
- self.streamQueue.async {
78
+ self.streamQueue.sync {
72
79
  CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, self.host as CFString, self.port, &readStream, &writeStream)
73
80
 
74
81
  self.inputStream = readStream!.takeRetainedValue()
@@ -82,12 +89,12 @@ class SocketClient: NSObject {
82
89
  }
83
90
 
84
91
  self.dispatchGroup.enter()
85
- self.streamQueue.async {
92
+ self.readQueue.sync {
86
93
  self.inputStream.open()
87
94
  }
88
95
 
89
96
  self.dispatchGroup.enter()
90
- self.streamQueue.async {
97
+ self.writeQueue.sync {
91
98
  self.outputStream.open()
92
99
  }
93
100
 
@@ -100,7 +107,7 @@ class SocketClient: NSObject {
100
107
  let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait)
101
108
 
102
109
  guard success else {
103
- self.socketDelegate?.commandExecuted(serverResponse: .connectionFailure)
110
+ self.socketDelegate?.commandExecuted(serverResponse: .connectionFailure) { _ in }
104
111
  return
105
112
  }
106
113
 
@@ -125,7 +132,7 @@ class SocketClient: NSObject {
125
132
  log(message: "Timeout: \(failureMessage)")
126
133
 
127
134
  if case .seconds(let seconds) = timeToWait {
128
- socketDelegate?.commandExecuted(serverResponse: .commandTimeout(seconds: seconds))
135
+ socketDelegate?.commandExecuted(serverResponse: .commandTimeout(seconds: seconds)) { _ in }
129
136
  }
130
137
  return false
131
138
  }
@@ -140,29 +147,30 @@ class SocketClient: NSObject {
140
147
  }
141
148
 
142
149
  private func sendThroughQueue(string: String) {
143
- streamQueue.async {
144
- let data = string.data(using: .utf8)!
145
- _ = data.withUnsafeBytes { self.outputStream.write($0, maxLength: data.count) }
146
- }
150
+ let data = string.data(using: .utf8)!
151
+ _ = data.withUnsafeBytes { self.outputStream.write($0, maxLength: data.count) }
147
152
  }
148
153
 
149
154
  private func privateSend(string: String) {
150
- self.dispatchGroup.enter()
151
- sendThroughQueue(string: string)
152
-
153
- let timeoutSeconds = self.cleaningUpAfterDone ? 1 : self.commandTimeoutSeconds
154
- let timeToWait = DispatchTimeInterval.seconds(timeoutSeconds)
155
- let commandTimeout = DispatchTime.now() + timeToWait
156
- let timeoutResult = self.dispatchGroup.wait(timeout: commandTimeout)
157
-
158
- _ = testDispatchTimeoutResult(timeoutResult, failureMessage: "Ruby process didn't return after: \(SocketClient.connectTimeoutSeconds) seconds", timeToWait: timeToWait)
155
+ writeQueue.sync {
156
+ writeSemaphore.wait()
157
+ self.sendThroughQueue(string: string)
158
+ writeSemaphore.signal()
159
+ let timeoutSeconds = self.cleaningUpAfterDone ? 1 : self.commandTimeoutSeconds
160
+ let timeToWait = DispatchTimeInterval.seconds(timeoutSeconds)
161
+ let commandTimeout = DispatchTime.now() + timeToWait
162
+ let timeoutResult = writeSemaphore.wait(timeout: commandTimeout)
163
+
164
+ _ = self.testDispatchTimeoutResult(timeoutResult, failureMessage: "Ruby process didn't return after: \(SocketClient.connectTimeoutSeconds) seconds", timeToWait: timeToWait)
165
+
166
+ }
159
167
  }
160
168
 
161
169
  private func send(string: String) {
162
170
  guard !self.cleaningUpAfterDone else {
163
171
  // This will happen after we abort if there are commands waiting to be executed
164
172
  // Need to check state of SocketClient in command runner to make sure we can accept `send`
165
- socketDelegate?.commandExecuted(serverResponse: .alreadyClosedSockets)
173
+ socketDelegate?.commandExecuted(serverResponse: .alreadyClosedSockets) { _ in }
166
174
  return
167
175
  }
168
176
 
@@ -185,6 +193,15 @@ class SocketClient: NSObject {
185
193
  stopOutputSession()
186
194
  self.socketDelegate?.connectionsClosed()
187
195
  }
196
+
197
+ public func enter() {
198
+ dispatchGroup.enter()
199
+ }
200
+
201
+ public func leave() {
202
+ readSemaphore.signal()
203
+ writeSemaphore.signal()
204
+ }
188
205
  }
189
206
 
190
207
  extension SocketClient: StreamDelegate {
@@ -246,18 +263,24 @@ extension SocketClient: StreamDelegate {
246
263
  }
247
264
 
248
265
  func read() {
249
- var buffer = [UInt8](repeating: 0, count: maxReadLength)
250
- var output = ""
251
- while self.inputStream!.hasBytesAvailable {
252
- let bytesRead: Int = inputStream!.read(&buffer, maxLength: buffer.count)
253
- if bytesRead >= 0 {
254
- output += NSString(bytes: UnsafePointer(buffer), length: bytesRead, encoding: String.Encoding.utf8.rawValue)! as String
255
- } else {
256
- verbose(message: "Stream read() error")
266
+ readQueue.sync {
267
+ self.readSemaphore.wait()
268
+ var buffer = [UInt8](repeating: 0, count: maxReadLength)
269
+ var output = ""
270
+ while self.inputStream!.hasBytesAvailable {
271
+ let bytesRead: Int = inputStream!.read(&buffer, maxLength: buffer.count)
272
+ if bytesRead >= 0 {
273
+ guard let read = String(bytes: buffer[..<bytesRead], encoding: .utf8) else {
274
+ fatalError("Unable to decode bytes from buffer \(buffer[..<bytesRead])")
275
+ }
276
+ output.append(contentsOf: read)
277
+ } else {
278
+ verbose(message: "Stream read() error")
279
+ }
257
280
  }
281
+ self.processResponse(string: output, socket: self)
282
+ readSemaphore.signal()
258
283
  }
259
-
260
- processResponse(string: output)
261
284
  }
262
285
 
263
286
  func handleFailure(message: [String]) {
@@ -266,11 +289,12 @@ extension SocketClient: StreamDelegate {
266
289
  self.send(rubyCommand: shutdownCommand)
267
290
  }
268
291
 
269
- func processResponse(string: String) {
292
+ func processResponse(string: String, socket: SocketClient) {
270
293
  guard string.count > 0 else {
271
- self.socketDelegate?.commandExecuted(serverResponse: .malformedResponse)
272
- self.handleFailure(message: ["empty response from ruby process"])
273
-
294
+ self.socketDelegate?.commandExecuted(serverResponse: .malformedResponse) {
295
+ self.handleFailure(message: ["empty response from ruby process"])
296
+ $0.writeSemaphore.signal()
297
+ }
274
298
  return
275
299
  }
276
300
 
@@ -279,24 +303,31 @@ extension SocketClient: StreamDelegate {
279
303
  verbose(message: "response is: \(responseString)")
280
304
  switch socketResponse.responseType {
281
305
  case .clientInitiatedCancel:
282
- self.socketDelegate?.commandExecuted(serverResponse: .clientInitiatedCancelAcknowledged)
283
- self.closeSession(sendAbort: false)
306
+ self.socketDelegate?.commandExecuted(serverResponse: .clientInitiatedCancelAcknowledged) {
307
+ self.closeSession(sendAbort: false)
308
+ $0.writeSemaphore.signal()
309
+ }
310
+
284
311
 
285
312
  case .failure(let failureInformation):
286
- self.socketDelegate?.commandExecuted(serverResponse: .serverError)
287
- self.handleFailure(message: failureInformation)
313
+ self.socketDelegate?.commandExecuted(serverResponse: .serverError) {
314
+ self.handleFailure(message: failureInformation)
315
+ $0.writeSemaphore.signal()
316
+ }
317
+
288
318
 
289
319
  case .parseFailure(let failureInformation):
290
- self.socketDelegate?.commandExecuted(serverResponse: .malformedResponse)
291
- self.handleFailure(message: failureInformation)
320
+ self.socketDelegate?.commandExecuted(serverResponse: .malformedResponse) {
321
+ self.handleFailure(message: failureInformation)
322
+ $0.writeSemaphore.signal()
323
+ }
324
+
292
325
 
293
326
  case .readyForNext(let returnedObject, let closureArgumentValue):
294
- self.socketDelegate?.commandExecuted(serverResponse: .success(returnedObject: returnedObject, closureArgumentValue: closureArgumentValue))
295
- // cool, ready for next command
296
- break
327
+ self.socketDelegate?.commandExecuted(serverResponse: .success(returnedObject: returnedObject, closureArgumentValue: closureArgumentValue)) {
328
+ $0.writeSemaphore.signal()
329
+ }
297
330
  }
298
-
299
- self.dispatchGroup.leave() // should now pull the next piece of work
300
331
  }
301
332
  }
302
333