@@ -15,6 +15,9 @@ class ExecutionService: ObservableObject {
1515 @Published var status : ExecutionStatus = . idle
1616 @Published var output : String = " "
1717 private var cancellationRequested = false
18+ private let installTimeoutSeconds : TimeInterval = 20
19+ private let installMaxAttempts = 3
20+ private let installRetryDelayNanoseconds : UInt64 = 300_000_000
1821
1922 init (
2023 dockerService: DockerServiceProtocol ,
@@ -85,19 +88,70 @@ class ExecutionService: ObservableObject {
8588 private func setupEnvironment( for command: String , container: String ) async throws {
8689 if command. contains ( " npm " ) {
8790 await MainActor . run { self . output += " \n 📦 Installing Node.js... " }
88- _ = try dockerService. executeShell ( container: container, script: " apk add --no-cache nodejs npm " )
91+ try await installPackage (
92+ container: container,
93+ script: " apk add --no-cache nodejs npm " ,
94+ runtimeName: " Node.js "
95+ )
8996 } else if command. contains ( " python " ) {
9097 await MainActor . run { self . output += " \n 📦 Installing Python... " }
91- _ = try dockerService. executeShell ( container: container, script: " apk add --no-cache python3 " )
98+ try await installPackage (
99+ container: container,
100+ script: " apk add --no-cache python3 " ,
101+ runtimeName: " Python "
102+ )
92103 } else if command. contains ( " javac " ) || command. contains ( " mvn " ) || command. contains ( " gradle " ) {
93104 await MainActor . run { self . output += " \n 📦 Setting up Java environment... " }
94105 // Note: JDK should be pre-installed in the container image
95106 // This is handled by using the configured JDK image
96107 } else if command. contains ( " go " ) {
97108 await MainActor . run { self . output += " \n 📦 Installing Go... " }
98- _ = try dockerService. executeShell ( container: container, script: " apk add --no-cache go " )
109+ try await installPackage (
110+ container: container,
111+ script: " apk add --no-cache go " ,
112+ runtimeName: " Go "
113+ )
99114 }
100115 }
116+
117+ private func installPackage( container: String , script: String , runtimeName: String ) async throws {
118+ var lastError : Error ?
119+
120+ for attempt in 1 ... installMaxAttempts {
121+ do {
122+ let timeoutScript = " timeout \( Int ( installTimeoutSeconds) ) sh -lc \" \( escapeForDoubleQuotedShell ( script) ) \" "
123+ _ = try dockerService. executeShell ( container: container, script: timeoutScript)
124+ return
125+ } catch {
126+ lastError = error
127+
128+ if attempt == installMaxAttempts {
129+ let debugDetails = " runtime= \( runtimeName) attempts= \( installMaxAttempts) timeoutSeconds= \( installTimeoutSeconds) lastError= \( error. localizedDescription) "
130+ throw ZeroError . runtimeCommandFailed (
131+ userMessage: " Failed to install \( runtimeName) after \( installMaxAttempts) attempts. " ,
132+ debugDetails: debugDetails
133+ )
134+ }
135+
136+ await MainActor . run {
137+ self . output += " \n ⚠️ Retrying \( runtimeName) installation (attempt \( attempt + 1 ) / \( installMaxAttempts) )... "
138+ }
139+ try ? await Task . sleep ( nanoseconds: installRetryDelayNanoseconds)
140+ }
141+ }
142+
143+ let fallbackMessage = " Failed to install \( runtimeName) after \( installMaxAttempts) attempts. "
144+ throw ZeroError . runtimeCommandFailed (
145+ userMessage: fallbackMessage,
146+ debugDetails: " runtime= \( runtimeName) lastError= \( lastError? . localizedDescription ?? " unknown " ) "
147+ )
148+ }
149+
150+ private func escapeForDoubleQuotedShell( _ script: String ) -> String {
151+ script
152+ . replacingOccurrences ( of: " \\ " , with: " \\ \\ " )
153+ . replacingOccurrences ( of: " \" " , with: " \\ \" " )
154+ }
101155
102156 func createJavaContainer( name: String ) async throws -> String {
103157 let config = try buildConfigService. load ( )
0 commit comments