fastlane 2.149.0 → 2.150.0.rc4

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -82
  3. data/deliver/lib/deliver.rb +0 -1
  4. data/deliver/lib/deliver/app_screenshot.rb +26 -25
  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 +134 -39
  12. data/fastlane/lib/fastlane/actions/docs/capture_ios_screenshots.md +1 -1
  13. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +77 -96
  14. data/fastlane/lib/fastlane/actions/docs/upload_to_play_store.md +3 -2
  15. data/fastlane/lib/fastlane/actions/download_dsyms.rb +7 -1
  16. data/fastlane/lib/fastlane/actions/google_play_track_release_names.rb +74 -0
  17. data/fastlane/lib/fastlane/actions/set_changelog.rb +23 -20
  18. data/fastlane/lib/fastlane/actions/slack.rb +1 -1
  19. data/fastlane/lib/fastlane/actions/spm.rb +7 -0
  20. data/fastlane/lib/fastlane/actions/upload_symbols_to_crashlytics.rb +1 -32
  21. data/fastlane/lib/fastlane/lane.rb +3 -3
  22. data/fastlane/lib/fastlane/swift_fastlane_function.rb +8 -4
  23. data/fastlane/lib/fastlane/version.rb +1 -1
  24. data/fastlane/swift/ControlCommand.swift +1 -0
  25. data/fastlane/swift/Deliverfile.swift +1 -1
  26. data/fastlane/swift/Fastlane.swift +48 -12
  27. data/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/xcshareddata/xcschemes/FastlaneRunner.xcscheme +3 -9
  28. data/fastlane/swift/Gymfile.swift +1 -1
  29. data/fastlane/swift/LaneFileProtocol.swift +2 -5
  30. data/fastlane/swift/Matchfile.swift +1 -1
  31. data/fastlane/swift/MatchfileProtocol.swift +1 -1
  32. data/fastlane/swift/Precheckfile.swift +1 -1
  33. data/fastlane/swift/RubyCommand.swift +29 -6
  34. data/fastlane/swift/RubyCommandable.swift +1 -0
  35. data/fastlane/swift/Runner.swift +85 -13
  36. data/fastlane/swift/Scanfile.swift +1 -1
  37. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  38. data/fastlane/swift/Screengrabfile.swift +1 -1
  39. data/fastlane/swift/Snapshotfile.swift +1 -1
  40. data/fastlane/swift/SnapshotfileProtocol.swift +3 -3
  41. data/fastlane/swift/SocketClient.swift +76 -45
  42. data/fastlane/swift/SocketClientDelegateProtocol.swift +1 -1
  43. data/fastlane/swift/SocketResponse.swift +1 -0
  44. data/fastlane_core/lib/fastlane_core/configuration/config_item.rb +1 -3
  45. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +89 -52
  46. data/fastlane_core/lib/fastlane_core/pkg_file_analyser.rb +7 -0
  47. data/fastlane_core/lib/fastlane_core/project.rb +0 -1
  48. data/frameit/lib/frameit/device_types.rb +100 -100
  49. data/produce/lib/produce/itunes_connect.rb +32 -21
  50. data/produce/lib/produce/options.rb +3 -3
  51. data/sigh/lib/assets/resign.sh +7 -7
  52. data/snapshot/lib/assets/SnapshotHelper.swift +5 -5
  53. data/snapshot/lib/assets/SnapshotHelperXcode8.swift +3 -3
  54. data/snapshot/lib/snapshot/options.rb +0 -1
  55. data/snapshot/lib/snapshot/reports_generator.rb +8 -1
  56. data/spaceship/lib/spaceship/.DS_Store +0 -0
  57. data/spaceship/lib/spaceship/client.rb +4 -3
  58. data/spaceship/lib/spaceship/connect_api.rb +25 -2
  59. data/spaceship/lib/spaceship/connect_api/client.rb +97 -31
  60. data/spaceship/lib/spaceship/connect_api/file_uploader.rb +98 -0
  61. data/spaceship/lib/spaceship/connect_api/model.rb +1 -1
  62. data/spaceship/lib/spaceship/connect_api/models/age_rating_declaration.rb +113 -0
  63. data/spaceship/lib/spaceship/connect_api/models/app.rb +120 -3
  64. data/spaceship/lib/spaceship/connect_api/models/app_category.rb +94 -0
  65. data/spaceship/lib/spaceship/connect_api/models/app_info.rb +67 -0
  66. data/spaceship/lib/spaceship/connect_api/models/app_info_localization.rb +38 -0
  67. data/spaceship/lib/spaceship/connect_api/models/app_preview.rb +129 -0
  68. data/spaceship/lib/spaceship/connect_api/models/app_preview_set.rb +71 -0
  69. data/spaceship/lib/spaceship/connect_api/models/app_price.rb +22 -0
  70. data/spaceship/lib/spaceship/connect_api/models/app_price_tier.rb +12 -0
  71. data/spaceship/lib/spaceship/connect_api/models/app_review_attachment.rb +71 -0
  72. data/spaceship/lib/spaceship/connect_api/models/app_screenshot.rb +93 -0
  73. data/spaceship/lib/spaceship/connect_api/models/app_screenshot_set.rb +101 -0
  74. data/spaceship/lib/spaceship/connect_api/models/app_store_review_detail.rb +51 -0
  75. data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +183 -0
  76. data/spaceship/lib/spaceship/connect_api/models/app_store_version_localization.rb +86 -0
  77. data/spaceship/lib/spaceship/connect_api/models/app_store_version_phased_release.rb +36 -0
  78. data/spaceship/lib/spaceship/connect_api/models/app_store_version_submission.rb +26 -0
  79. data/spaceship/lib/spaceship/connect_api/models/build.rb +4 -0
  80. data/spaceship/lib/spaceship/connect_api/models/idfa_declaration.rb +40 -0
  81. data/spaceship/lib/spaceship/connect_api/models/reset_ratings_request.rb +26 -0
  82. data/spaceship/lib/spaceship/connect_api/models/territory.rb +27 -0
  83. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +10 -3
  84. data/spaceship/lib/spaceship/connect_api/tunes/client.rb +33 -0
  85. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +841 -0
  86. data/spaceship/lib/spaceship/spaceauth_runner.rb +2 -2
  87. data/supply/lib/supply/client.rb +19 -0
  88. data/supply/lib/supply/reader.rb +16 -0
  89. metadata +44 -39
  90. data/deliver/lib/deliver/upload_assets.rb +0 -27
  91. data/fastlane/lib/assets/.s3_html_template.erb.swp +0 -0
  92. data/fastlane/lib/fastlane/actions/.hipchat.rb.swp +0 -0
  93. data/fastlane/lib/fastlane/actions/.slack.rb.swp +0 -0
  94. data/scan/lib/scan/.options.rb.swp +0 -0
@@ -26,10 +26,7 @@
26
26
  buildConfiguration = "Debug"
27
27
  selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28
28
  selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29
- language = ""
30
29
  shouldUseLaunchSchemeArgsEnv = "YES">
31
- <Testables>
32
- </Testables>
33
30
  <MacroExpansion>
34
31
  <BuildableReference
35
32
  BuildableIdentifier = "primary"
@@ -39,14 +36,13 @@
39
36
  ReferencedContainer = "container:FastlaneSwiftRunner.xcodeproj">
40
37
  </BuildableReference>
41
38
  </MacroExpansion>
42
- <AdditionalOptions>
43
- </AdditionalOptions>
39
+ <Testables>
40
+ </Testables>
44
41
  </TestAction>
45
42
  <LaunchAction
46
43
  buildConfiguration = "Debug"
47
44
  selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
48
45
  selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
49
- language = ""
50
46
  launchStyle = "0"
51
47
  useCustomWorkingDirectory = "NO"
52
48
  ignoresPersistentStateOnLaunch = "NO"
@@ -65,7 +61,7 @@
65
61
  </BuildableProductRunnable>
66
62
  <CommandLineArguments>
67
63
  <CommandLineArgument
68
- argument = "lane test"
64
+ argument = "lane testLane"
69
65
  isEnabled = "YES">
70
66
  </CommandLineArgument>
71
67
  <CommandLineArgument
@@ -73,8 +69,6 @@
73
69
  isEnabled = "YES">
74
70
  </CommandLineArgument>
75
71
  </CommandLineArguments>
76
- <AdditionalOptions>
77
- </AdditionalOptions>
78
72
  </LaunchAction>
79
73
  <ProfileAction
80
74
  buildConfiguration = "Release"
@@ -18,4 +18,4 @@ class Gymfile: GymfileProtocol {
18
18
 
19
19
 
20
20
 
21
- // Generated with fastlane 2.149.0
21
+ // Generated with fastlane 2.149.1
@@ -19,7 +19,6 @@ public protocol LaneFileProtocol: class {
19
19
  static func runLane(named: String, parameters: [String : String]) -> Bool
20
20
 
21
21
  func recordLaneDescriptions()
22
- func beforeAll(currentLane: String, parameters: [String : String])
23
22
  func beforeAll()
24
23
  func afterAll(currentLane: String)
25
24
  func onError(currentLane: String, errorInfo: String)
@@ -27,7 +26,6 @@ public protocol LaneFileProtocol: class {
27
26
 
28
27
  public extension LaneFileProtocol {
29
28
  var fastlaneVersion: String { return "" } // default "" because that means any is fine
30
- func beforeAll(currentLane: String, parameters: [String : String]) { } // no op by default
31
29
  func beforeAll() { } // no op by default
32
30
  func afterAll(currentLane: String) { } // no op by default
33
31
  func onError(currentLane: String, errorInfo: String) {} // no op by default
@@ -39,8 +37,7 @@ public class LaneFile: NSObject, LaneFileProtocol {
39
37
  private(set) static var fastfileInstance: Fastfile?
40
38
 
41
39
  // Called before any lane is executed.
42
- private func setupAllTheThings(lane: String, parameters: [String : String]) {
43
- LaneFile.fastfileInstance!.beforeAll(currentLane: lane, parameters: parameters)
40
+ private func setupAllTheThings() {
44
41
  LaneFile.fastfileInstance!.beforeAll()
45
42
  }
46
43
 
@@ -126,7 +123,7 @@ public class LaneFile: NSObject, LaneFileProtocol {
126
123
  }
127
124
 
128
125
  // call all methods that need to be called before we start calling lanes
129
- fastfileInstance.setupAllTheThings(lane: named, parameters: parameters)
126
+ fastfileInstance.setupAllTheThings()
130
127
 
131
128
  // We need to catch all possible errors here and display a nice message
132
129
  _ = fastfileInstance.perform(NSSelectorFromString(laneMethod), with: parameters)
@@ -18,4 +18,4 @@ class Matchfile: MatchfileProtocol {
18
18
 
19
19
 
20
20
 
21
- // Generated with fastlane 2.149.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.149.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.149.0
21
+ // Generated with fastlane 2.149.1
@@ -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.30]
261
+ // FastlaneRunnerAPIVersion [0.9.23]
@@ -18,4 +18,4 @@ class Screengrabfile: ScreengrabfileProtocol {
18
18
 
19
19
 
20
20
 
21
- // Generated with fastlane 2.149.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.149.0
21
+ // Generated with fastlane 2.149.1
@@ -64,7 +64,7 @@ protocol SnapshotfileProtocol: class {
64
64
  var addVideos: [String]? { get }
65
65
 
66
66
  /// A path to screenshots.html template
67
- var htmlTemplate: String { get }
67
+ var htmlTemplate: String? { get }
68
68
 
69
69
  /// The directory where to store the build log
70
70
  var buildlogPath: String { get }
@@ -149,7 +149,7 @@ extension SnapshotfileProtocol {
149
149
  var appIdentifier: String? { return nil }
150
150
  var addPhotos: [String]? { return nil }
151
151
  var addVideos: [String]? { return nil }
152
- var htmlTemplate: String { return "/Users/josh/Projects/fastlane/fastlane/snapshot/lib/snapshot/page.html.erb" }
152
+ var htmlTemplate: String? { return nil }
153
153
  var buildlogPath: String { return "~/Library/Logs/snapshot" }
154
154
  var clean: Bool { return false }
155
155
  var testWithoutBuilding: Bool? { return nil }
@@ -174,4 +174,4 @@ extension SnapshotfileProtocol {
174
174
 
175
175
  // Please don't remove the lines below
176
176
  // They are used to detect outdated files
177
- // FastlaneRunnerAPIVersion [0.9.10]
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