mirror of
https://github.com/apple/container.git
synced 2026-09-11 18:25:41 +00:00
Change to calling close on the underlying FileHandle as it already has logic to set the fd to an invalid value and prevent double closes.
210 lines
6.6 KiB
Swift
210 lines
6.6 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
|
|
//
|
|
// 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
|
|
|
|
/// `Terminal` provides a clean interface to deal with terminal interactions on Unix platforms.
|
|
public struct Terminal: Sendable {
|
|
private let initState: termios?
|
|
|
|
private var descriptor: Int32 {
|
|
handle.fileDescriptor
|
|
}
|
|
public let handle: FileHandle
|
|
|
|
public init(descriptor: Int32, setInitState: Bool = true) throws {
|
|
if setInitState {
|
|
self.initState = try Self.getattr(descriptor)
|
|
} else {
|
|
initState = nil
|
|
}
|
|
self.handle = .init(fileDescriptor: descriptor, closeOnDealloc: false)
|
|
}
|
|
|
|
/// Write the provided data to the tty device.
|
|
public func write(_ data: Data) throws {
|
|
try handle.write(contentsOf: data)
|
|
}
|
|
|
|
/// The winsize for a pty.
|
|
public struct Size: Sendable {
|
|
let size: winsize
|
|
|
|
/// The width or `col` of the pty.
|
|
public var width: UInt16 {
|
|
size.ws_col
|
|
}
|
|
/// The height or `rows` of the pty.
|
|
public var height: UInt16 {
|
|
size.ws_row
|
|
}
|
|
|
|
init(_ size: winsize) {
|
|
self.size = size
|
|
}
|
|
|
|
/// Set the size for use with a pty.
|
|
public init(width cols: UInt16, height rows: UInt16) {
|
|
self.size = winsize(ws_row: rows, ws_col: cols, ws_xpixel: 0, ws_ypixel: 0)
|
|
}
|
|
}
|
|
|
|
/// Return the current pty attached to any of the STDIO descriptors.
|
|
public static var current: Terminal {
|
|
get throws {
|
|
for i in [STDERR_FILENO, STDOUT_FILENO, STDIN_FILENO] {
|
|
do {
|
|
return try Terminal(descriptor: i)
|
|
} catch {}
|
|
}
|
|
throw Error.notAPty
|
|
}
|
|
}
|
|
|
|
/// The current window size for the pty.
|
|
public var size: Size {
|
|
get throws {
|
|
var ws = winsize()
|
|
try fromSyscall(ioctl(descriptor, UInt(TIOCGWINSZ), &ws))
|
|
return Size(ws)
|
|
}
|
|
}
|
|
|
|
/// Create a new pty pair.
|
|
/// - Parameter initialSize: An initial size of the child pty.
|
|
public static func create(initialSize: Size? = nil) throws -> (parent: Terminal, child: Terminal) {
|
|
var parent: Int32 = 0
|
|
var child: Int32 = 0
|
|
let size = initialSize ?? Size(width: 120, height: 40)
|
|
var ws = size.size
|
|
|
|
try fromSyscall(openpty(&parent, &child, nil, nil, &ws))
|
|
return (
|
|
parent: try Terminal(descriptor: parent, setInitState: false),
|
|
child: try Terminal(descriptor: child, setInitState: false)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: Errors
|
|
|
|
extension Terminal {
|
|
public enum Error: Swift.Error, CustomStringConvertible {
|
|
case notAPty
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .notAPty:
|
|
return "the provided fd is not a pty"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Terminal {
|
|
/// Resize the current pty from the size of the provided pty.
|
|
/// - Parameter pty: A pty to resize from.
|
|
public func resize(from pty: Terminal) throws {
|
|
var ws = try pty.size
|
|
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
|
|
}
|
|
|
|
/// Resize the pty to the provided window size.
|
|
/// - Parameter size: A window size for a pty.
|
|
public func resize(size: Size) throws {
|
|
var ws = size.size
|
|
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
|
|
}
|
|
|
|
/// Resize the pty to the provided window size.
|
|
/// - Parameter width: A width or cols of the terminal.
|
|
/// - Parameter height: A height or rows of the terminal.
|
|
public func resize(width: UInt16, height: UInt16) throws {
|
|
var ws = Size(width: width, height: height)
|
|
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
|
|
}
|
|
}
|
|
|
|
extension Terminal {
|
|
/// Enable raw mode for the pty.
|
|
public func setraw() throws {
|
|
var attr = try Self.getattr(descriptor)
|
|
cfmakeraw(&attr)
|
|
attr.c_oflag = attr.c_oflag | tcflag_t(OPOST)
|
|
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
|
|
}
|
|
|
|
/// Enable echo support.
|
|
/// Chars typed will be displayed to the terminal.
|
|
public func enableEcho() throws {
|
|
var attr = try Self.getattr(descriptor)
|
|
attr.c_iflag &= ~tcflag_t(ICRNL)
|
|
attr.c_lflag &= ~tcflag_t(ICANON | ECHO)
|
|
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
|
|
}
|
|
|
|
/// Disable echo support.
|
|
/// Chars typed will not be displayed back to the terminal.
|
|
public func disableEcho() throws {
|
|
var attr = try Self.getattr(descriptor)
|
|
attr.c_lflag &= ~tcflag_t(ECHO)
|
|
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
|
|
}
|
|
|
|
private static func getattr(_ fd: Int32) throws -> termios {
|
|
var attr = termios()
|
|
try fromSyscall(tcgetattr(fd, &attr))
|
|
return attr
|
|
}
|
|
}
|
|
|
|
// MARK: Reset
|
|
|
|
extension Terminal {
|
|
/// Close this pty's file descriptor.
|
|
public func close() throws {
|
|
do {
|
|
// Use FileHandle's close directly as it sets the underlying fd in the object
|
|
// to -1 for us.
|
|
try self.handle.close()
|
|
} catch {
|
|
if let error = error as NSError?, error.domain == NSPOSIXErrorDomain {
|
|
throw POSIXError(.init(rawValue: Int32(error.code))!)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/// Reset the pty to its initial state.
|
|
public func reset() throws {
|
|
if var attr = initState {
|
|
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
|
|
}
|
|
}
|
|
|
|
/// Reset the pty to its initial state masking any errors.
|
|
/// This is commonly used in a `defer` body to reset the current pty where the error code is not generally useful.
|
|
public func tryReset() {
|
|
try? reset()
|
|
}
|
|
}
|
|
|
|
private func fromSyscall(_ status: Int32) throws {
|
|
guard status == 0 else {
|
|
throw POSIXError(.init(rawValue: errno)!)
|
|
}
|
|
}
|