Skip to content

Commit 84c62a1

Browse files
committed
Fixes
1 parent e618857 commit 84c62a1

File tree

8 files changed

+234
-43
lines changed

8 files changed

+234
-43
lines changed

Plugins/container-compose/Sources/CLI/ComposeLogs.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ struct ComposeLogs: AsyncParsableCommand {
3939
var tail: Int?
4040

4141
@Flag(name: [.customLong("timestamps"), .customShort("t")], help: "Show timestamps")
42-
var timestamps: Bool = false
42+
var timestamps: Bool = false
43+
44+
@Flag(name: .long, help: "Include boot/system logs (vminitd) in output")
45+
var boot: Bool = false
4346

4447
@Flag(name: .long, help: "Disable log prefixes (container-name |)")
4548
var noLogPrefix: Bool = false
@@ -79,7 +82,8 @@ struct ComposeLogs: AsyncParsableCommand {
7982
services: services,
8083
follow: follow,
8184
tail: tail,
82-
timestamps: timestamps
85+
timestamps: timestamps,
86+
includeBoot: boot
8387
)
8488

8589
// Compute padding width for aligned prefixes

Plugins/container-compose/Sources/CLI/ComposeRestart.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ struct ComposeRestart: AsyncParsableCommand {
3232
@OptionGroup
3333
var global: Flags.Global
3434

35-
@Option(name: [.customLong("timeout"), .customShort("t")], help: "Specify a shutdown timeout in seconds (default: 10)")
36-
var timeout: Int = 10
37-
38-
@Argument(help: "Services to restart (omit to restart all)")
39-
var services: [String] = []
35+
@Option(name: [.customLong("timeout"), .customShort("t")], help: "Specify a shutdown timeout in seconds (default: 10)")
36+
var timeout: Int = 10
37+
38+
@Argument(help: "Services to restart (omit to restart all)")
39+
var services: [String] = []
40+
41+
@Flag(name: .long, help: "Disable healthchecks during orchestration")
42+
var noHealthcheck: Bool = false
4043

4144
func run() async throws {
4245
// Set environment variables
@@ -96,6 +99,7 @@ struct ComposeRestart: AsyncParsableCommand {
9699
project: project,
97100
services: services,
98101
timeout: timeout,
102+
disableHealthcheck: noHealthcheck,
99103
progressHandler: progress.handler
100104
)
101105

Plugins/container-compose/Sources/CLI/ComposeStart.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ struct ComposeStart: AsyncParsableCommand {
3232

3333
@OptionGroup
3434
var global: Flags.Global
35-
36-
@Argument(help: "Services to start")
37-
var services: [String] = []
35+
36+
@Argument(help: "Services to start")
37+
var services: [String] = []
38+
39+
@Flag(name: .long, help: "Disable healthchecks during orchestration")
40+
var noHealthcheck: Bool = false
3841

3942
func run() async throws {
4043
// Load .env and set environment variables
@@ -95,6 +98,7 @@ struct ComposeStart: AsyncParsableCommand {
9598
try await orchestrator.start(
9699
project: project,
97100
services: services,
101+
disableHealthcheck: noHealthcheck,
98102
progressHandler: progress.handler
99103
)
100104

Plugins/container-compose/Sources/CLI/ComposeUp.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ struct ComposeUp: AsyncParsableCommand {
7272
@Flag(name: .long, help: "Disable colored output")
7373
var noColor: Bool = false
7474

75+
@Flag(name: .long, help: "Disable healthchecks during orchestration")
76+
var noHealthcheck: Bool = false
77+
7578
@Argument(help: "Services to start")
7679
var services: [String] = []
7780

@@ -148,7 +151,8 @@ struct ComposeUp: AsyncParsableCommand {
148151
}
149152
}(),
150153
wait: wait,
151-
waitTimeoutSeconds: waitTimeout
154+
waitTimeoutSeconds: waitTimeout,
155+
disableHealthcheck: noHealthcheck
152156
)
153157

154158
progress.finish()

Plugins/container-compose/Sources/Core/Orchestrator/Orchestrator.swift

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,8 @@ public actor Orchestrator {
534534
progressHandler: ProgressUpdateHandler? = nil,
535535
pullPolicy: PullPolicy = .missing,
536536
wait: Bool = false,
537-
waitTimeoutSeconds: Int? = nil
537+
waitTimeoutSeconds: Int? = nil,
538+
disableHealthcheck: Bool = false
538539
) async throws {
539540
log.info("Starting project '\(project.name)'")
540541

@@ -595,14 +596,15 @@ public actor Orchestrator {
595596
noRecreate: noRecreate,
596597
removeOnExit: removeOnExit,
597598
progressHandler: progressHandler,
598-
pullPolicy: pullPolicy
599+
pullPolicy: pullPolicy,
600+
disableHealthcheck: disableHealthcheck
599601
)
600602

601603
// If --wait is set, wait for selected services to be healthy/running
602604
if wait {
603605
let timeout = waitTimeoutSeconds ?? 300
604606
for (name, svc) in targetServices {
605-
if svc.healthCheck != nil {
607+
if !disableHealthcheck, svc.healthCheck != nil {
606608
try await waitUntilHealthy(project: project, serviceName: name, service: svc)
607609
} else {
608610
let cid = svc.containerName ?? "\(project.name)_\(name)"
@@ -667,7 +669,8 @@ public actor Orchestrator {
667669
noRecreate: Bool,
668670
removeOnExit: Bool,
669671
progressHandler: ProgressUpdateHandler?,
670-
pullPolicy: PullPolicy
672+
pullPolicy: PullPolicy,
673+
disableHealthcheck: Bool
671674
) async throws {
672675
// Sort services by dependencies
673676
let resolution = try DependencyResolver.resolve(services: services)
@@ -678,7 +681,7 @@ public actor Orchestrator {
678681

679682
do {
680683
// Wait for dependency conditions before starting this service
681-
try await waitForDependencyConditions(project: project, serviceName: serviceName, services: services)
684+
try await waitForDependencyConditions(project: project, serviceName: serviceName, services: services, disableHealthcheck: disableHealthcheck)
682685

683686
try await createAndStartContainer(
684687
project: project,
@@ -692,12 +695,8 @@ public actor Orchestrator {
692695
pullPolicy: pullPolicy
693696
)
694697

695-
// Optionally kick health monitor for the service
696-
if service.healthCheck != nil {
697-
Task { [weak self] in
698-
_ = try? await self?.runHealthCheckOnce(project: project, serviceName: serviceName, service: service)
699-
}
700-
}
698+
// Do not run background health probes by default.
699+
// Health checks are evaluated only when --wait is explicitly requested.
701700
} catch {
702701
log.error("Failed to start service '\(serviceName)': \(error)")
703702
throw error
@@ -706,7 +705,7 @@ public actor Orchestrator {
706705
}
707706

708707
/// Wait for dependencies according to compose depends_on conditions
709-
private func waitForDependencyConditions(project: Project, serviceName: String, services: [String: Service]) async throws {
708+
private func waitForDependencyConditions(project: Project, serviceName: String, services: [String: Service], disableHealthcheck: Bool) async throws {
710709
guard let svc = services[serviceName] else { return }
711710

712711
// Wait for service_started
@@ -715,7 +714,7 @@ public actor Orchestrator {
715714
try await waitUntilContainerRunning(containerId: depId, timeoutSeconds: 120)
716715
}
717716
// Wait for service_healthy
718-
for dep in svc.dependsOnHealthy {
717+
for dep in svc.dependsOnHealthy where !disableHealthcheck {
719718
if let depSvc = services[dep] {
720719
try await waitUntilHealthy(project: project, serviceName: dep, service: depSvc)
721720
}
@@ -903,16 +902,24 @@ public actor Orchestrator {
903902

904903
// Recreate the container
905904
log.info("Recreating existing container '\(existing.id)' for service '\(serviceName)'")
906-
// Stop with a longer timeout and wait until it's fully stopped before deleting
905+
// 1) Ask it to stop gracefully (SIGTERM) with longer timeout
907906
do { try await existing.stop(opts: ContainerStopOptions(timeoutInSeconds: 15, signal: SIGTERM)) }
908907
catch { log.warning("failed to stop \(existing.id): \(error)") }
909-
do { try await waitUntilContainerStopped(containerId: existing.id, timeoutSeconds: 20) }
910-
catch { log.warning("timeout waiting for \(existing.id) to stop: \(error)") }
908+
// 2) Wait until it is actually stopped; if not, escalate to SIGKILL and wait briefly
909+
do {
910+
try await waitUntilContainerStopped(containerId: existing.id, timeoutSeconds: 20)
911+
} catch {
912+
log.warning("timeout waiting for \(existing.id) to stop: \(error); sending SIGKILL")
913+
do { try await existing.kill(SIGKILL) } catch { log.warning("failed to SIGKILL \(existing.id): \(error)") }
914+
// small wait after SIGKILL
915+
try? await Task.sleep(nanoseconds: 700_000_000)
916+
}
917+
// 3) Try to delete (force on retry)
911918
do { try await existing.delete() }
912919
catch {
913-
log.warning("failed to delete \(existing.id): \(error); retrying once after short delay")
920+
log.warning("failed to delete \(existing.id): \(error); retrying forced delete after short delay")
914921
try? await Task.sleep(nanoseconds: 700_000_000)
915-
do { try await existing.delete() } catch { log.warning("second delete attempt failed for \(existing.id): \(error)") }
922+
do { try await existing.delete(force: true) } catch { log.warning("forced delete attempt failed for \(existing.id): \(error)") }
916923
}
917924
projectState[project.name]?.containers.removeValue(forKey: serviceName)
918925
}
@@ -1128,12 +1135,25 @@ public actor Orchestrator {
11281135
// Add volume mounts (ensure named/anonymous volumes exist and use their host paths)
11291136
config.mounts = try await resolveComposeMounts(project: project, serviceName: serviceName, mounts: service.volumes)
11301137

1131-
// Add resource limits
1138+
// Add resource limits (compose-style parsing for memory like "2g", "2048MB").
11321139
if let cpus = service.cpus {
11331140
config.resources.cpus = Int(cpus) ?? 4
11341141
}
1135-
if let memory = service.memory {
1136-
config.resources.memoryInBytes = UInt64(memory) ?? 1024.mib()
1142+
if let memStr = service.memory, !memStr.isEmpty {
1143+
do {
1144+
if memStr.lowercased() == "max" {
1145+
// Treat "max" as no override: keep the runtime/default value (set below if needed).
1146+
// Intentionally do nothing here.
1147+
} else {
1148+
let res = try Parser.resources(cpus: nil, memory: memStr)
1149+
if let bytes = res.memoryInBytes as UInt64? { config.resources.memoryInBytes = bytes }
1150+
}
1151+
} catch {
1152+
log.warning("Invalid memory value '\\(memStr)'; using default. Error: \\(error)")
1153+
}
1154+
} else {
1155+
// Safer default for dev servers (was 1 GiB)
1156+
config.resources.memoryInBytes = 2048.mib()
11371157
}
11381158

11391159
// TTY support from compose service
@@ -1670,7 +1690,8 @@ public actor Orchestrator {
16701690
services: [String] = [],
16711691
follow: Bool = false,
16721692
tail: Int? = nil,
1673-
timestamps: Bool = false
1693+
timestamps: Bool = false,
1694+
includeBoot: Bool = false
16741695
) async throws -> AsyncThrowingStream<LogEntry, Error> {
16751696
// Resolve target services
16761697
let selected = services.isEmpty ? Set(project.services.keys) : Set(services)
@@ -1702,6 +1723,8 @@ public actor Orchestrator {
17021723

17031724
final class Emitter: @unchecked Sendable {
17041725
let cont: AsyncThrowingStream<LogEntry, Error>.Continuation
1726+
// Strongly retain file handles so readabilityHandler keeps firing.
1727+
private var retained: [FileHandle] = []
17051728
init(_ c: AsyncThrowingStream<LogEntry, Error>.Continuation) { self.cont = c }
17061729
func emit(_ svc: String, _ containerName: String, _ stream: LogEntry.LogStream, data: Data) {
17071730
guard !data.isEmpty else { return }
@@ -1711,13 +1734,11 @@ public actor Orchestrator {
17111734
}
17121735
}
17131736
}
1737+
func retain(_ fh: FileHandle) { retained.append(fh) }
17141738
func finish() { cont.finish() }
17151739
func fail(_ error: Error) { cont.yield(with: .failure(error)) }
17161740
}
17171741
let emitter = Emitter(continuation)
1718-
// Retain FileHandles so readabilityHandler continues firing while the stream is active
1719-
actor HandleRetainer { var fhs: [FileHandle] = []; func add(_ fh: FileHandle){ fhs.append(fh) } }
1720-
let retainer = HandleRetainer()
17211742
actor Counter { var value: Int; init(_ v: Int){ value = v } ; func dec() -> Int { value -= 1; return value } }
17221743
let counter = Counter(targets.count)
17231744

@@ -1732,20 +1753,20 @@ public actor Orchestrator {
17321753
fh.readabilityHandler = { handle in
17331754
emitter.emit(svc, container.id, .stdout, data: handle.availableData)
17341755
}
1735-
await retainer.add(fh)
1756+
emitter.retain(fh)
17361757
}
1737-
if fds.indices.contains(1) {
1758+
if includeBoot, fds.indices.contains(1) {
17381759
let fh = fds[1]
17391760
fh.readabilityHandler = { handle in
17401761
emitter.emit(svc, container.id, .stderr, data: handle.availableData)
17411762
}
1742-
await retainer.add(fh)
1763+
emitter.retain(fh)
17431764
}
17441765
} else {
17451766
if fds.indices.contains(0) {
17461767
emitter.emit(svc, container.id, .stdout, data: fds[0].readDataToEndOfFile())
17471768
}
1748-
if fds.indices.contains(1) {
1769+
if includeBoot, fds.indices.contains(1) {
17491770
emitter.emit(svc, container.id, .stderr, data: fds[1].readDataToEndOfFile())
17501771
}
17511772
let left = await counter.dec()
@@ -1765,10 +1786,11 @@ public actor Orchestrator {
17651786
public func start(
17661787
project: Project,
17671788
services: [String] = [],
1789+
disableHealthcheck: Bool = false,
17681790
progressHandler: ProgressUpdateHandler? = nil
17691791
) async throws {
1770-
// For build functionality, we just call up
1771-
try await up(project: project, services: services, progressHandler: progressHandler)
1792+
// Reuse up() path with defaults
1793+
try await up(project: project, services: services, progressHandler: progressHandler, disableHealthcheck: disableHealthcheck)
17721794
}
17731795

17741796

@@ -1789,10 +1811,11 @@ public actor Orchestrator {
17891811
project: Project,
17901812
services: [String] = [],
17911813
timeout: Int = 10,
1814+
disableHealthcheck: Bool = false,
17921815
progressHandler: ProgressUpdateHandler? = nil
17931816
) async throws {
17941817
_ = try await down(project: project, progressHandler: progressHandler)
1795-
try await up(project: project, services: services, progressHandler: progressHandler)
1818+
try await up(project: project, services: services, progressHandler: progressHandler, disableHealthcheck: disableHealthcheck)
17961819
}
17971820

17981821
/// Execute command in a service

0 commit comments

Comments
 (0)