diff --git a/Tests/ContainerizationOSTests/TerminalRawModeTests.swift b/Tests/ContainerizationOSTests/TerminalRawModeTests.swift new file mode 100644 index 000000000..7ad1f5ed3 --- /dev/null +++ b/Tests/ContainerizationOSTests/TerminalRawModeTests.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation +import Testing + +@testable import ContainerizationOS + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif + +@Suite("Terminal raw mode tests") +struct TerminalRawModeTests { + private func termiosAttributes(of descriptor: Int32) throws -> termios { + var attrs = termios() + try #require(tcgetattr(descriptor, &attrs) == 0, "tcgetattr failed, errno: \(errno)") + return attrs + } + + private func has(_ flag: tcflag_t, in flags: tcflag_t) -> Bool { + flags & flag != 0 + } + + @Test("pty slave is in canonical mode before raw-mode setup") + func ptySlaveIsCanonicalBeforeSetup() throws { + let (parent, child) = try Terminal.create(initialSize: Terminal.Size(width: 120, height: 40)) + defer { + try? parent.close() + try? child.close() + } + + // A freshly allocated pty slave is canonical by default (ICANON set). + let attrs = try termiosAttributes(of: child.handle.fileDescriptor) + #expect(has(tcflag_t(ICANON), in: attrs.c_lflag)) + } + + @Test("setraw clears ICANON and preserves OPOST") + func setrawClearsCanonicalAndPreservesOutputPostProcessing() throws { + let (parent, child) = try Terminal.create(initialSize: Terminal.Size(width: 120, height: 40)) + defer { + try? parent.close() + try? child.close() + } + + try child.setraw() + + let attrs = try termiosAttributes(of: child.handle.fileDescriptor) + #expect(!has(tcflag_t(ICANON), in: attrs.c_lflag), "setraw must clear canonical mode") + #expect(has(tcflag_t(OPOST), in: attrs.c_oflag), "setraw must preserve OPOST for output post-processing") + #expect(has(tcflag_t(ONLCR), in: attrs.c_oflag), "setraw must preserve ONLCR for CRLF output translation") + } +} diff --git a/vminitd/Sources/vmexec/Console.swift b/vminitd/Sources/vmexec/Console.swift index b868049f5..95541af46 100644 --- a/vminitd/Sources/vmexec/Console.swift +++ b/vminitd/Sources/vmexec/Console.swift @@ -14,6 +14,7 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import ContainerizationOS import FoundationEssentials import LCShim @@ -55,6 +56,12 @@ class Console { } defer { _ = _close(slaveFD) } + // Put the pty slave into raw mode so the tty line discipline does not + // buffer or truncate large output writes (the container's stdout goes + // through this slave). `Terminal.setraw()` applies cfmakeraw and keeps + // OPOST, so newline translation to CRLF on output is preserved. + try Terminal(descriptor: slaveFD, setInitState: false).setraw() + for fd: Int32 in 0...2 { guard dup3(slaveFD, fd, 0) != -1 else { throw App.Errno(stage: "dup3")