diff --git a/Sources/tart/Commands/Exec.swift b/Sources/tart/Commands/Exec.swift index 61b1613..676f993 100644 --- a/Sources/tart/Commands/Exec.swift +++ b/Sources/tart/Commands/Exec.swift @@ -45,7 +45,7 @@ struct Exec: AsyncParsableCommand { // // [1]: https://blog.8-p.info/en/2020/06/11/unix-domain-socket-length/ if let baseURL = vmDir.controlSocketURL.baseURL { - FileManager.default.changeCurrentDirectoryPath(baseURL.path()) + FileManager.default.changeCurrentDirectoryPath(baseURL.absoluteURL.path(percentEncoded: false)) } // Switch controlling terminal into raw mode when remote pseudo-terminal is requested diff --git a/Sources/tart/Commands/IP.swift b/Sources/tart/Commands/IP.swift index b4e116e..4550a55 100644 --- a/Sources/tart/Commands/IP.swift +++ b/Sources/tart/Commands/IP.swift @@ -73,7 +73,7 @@ struct IP: AsyncParsableCommand { // // [1]: https://blog.8-p.info/en/2020/06/11/unix-domain-socket-length/ if let baseURL = controlSocketURL.baseURL { - FileManager.default.changeCurrentDirectoryPath(baseURL.path()) + FileManager.default.changeCurrentDirectoryPath(baseURL.absoluteURL.path(percentEncoded: false)) } if let ip = try await AgentResolver.ResolveIP(controlSocketURL.relativePath) { diff --git a/Sources/tart/ControlSocket.swift b/Sources/tart/ControlSocket.swift index a8fbc59..f872fda 100644 --- a/Sources/tart/ControlSocket.swift +++ b/Sources/tart/ControlSocket.swift @@ -22,14 +22,14 @@ class ControlSocket { // Remove control socket file from previous "tart run" invocations, // if any, otherwise we may get the "address already in use" error - try? FileManager.default.removeItem(atPath: controlSocketURL.path()) + try? FileManager.default.removeItem(at: controlSocketURL) // Change the current working directory to a VM's base directory // to work around Unix domain socket 104 byte limitation [1] // // [1]: https://blog.8-p.info/en/2020/06/11/unix-domain-socket-length/ if let baseURL = controlSocketURL.baseURL { - FileManager.default.changeCurrentDirectoryPath(baseURL.path()) + FileManager.default.changeCurrentDirectoryPath(baseURL.absoluteURL.path(percentEncoded: false)) } do { diff --git a/Tests/TartTests/ControlSocketTests.swift b/Tests/TartTests/ControlSocketTests.swift index 3e2a5fd..fb896ef 100644 --- a/Tests/TartTests/ControlSocketTests.swift +++ b/Tests/TartTests/ControlSocketTests.swift @@ -1,6 +1,10 @@ +import NIO import XCTest @testable import tart +// Avoid NSObject.bind and Tart's Darwin type shadowing the system function. +private let bindTestSocket = bind + @available(macOS 14, *) final class ControlSocketTests: XCTestCase { func testInitializerCreatesControlSocketBeforeReturning() async throws { @@ -44,6 +48,43 @@ final class ControlSocketTests: XCTestCase { } } + func testInitializerReplacesStaleSocketInLongEncodedPath() async throws { + let temporaryDirectory = try makeTemporaryDirectory() + let originalDirectory = FileManager.default.currentDirectoryPath + defer { + FileManager.default.changeCurrentDirectoryPath(originalDirectory) + try? FileManager.default.removeItem(at: temporaryDirectory) + } + + let vmDirectory = temporaryDirectory.appendingPathComponent( + "Tart Home %# 虚拟机 " + String(repeating: "v", count: 104), isDirectory: true + ) + try FileManager.default.createDirectory(at: vmDirectory, withIntermediateDirectories: false) + let socketURL = URL(fileURLWithPath: "control.sock", relativeTo: vmDirectory) + XCTAssertGreaterThan(socketURL.path.utf8.count, 104) + + // Closing a POSIX socket leaves its path behind, as exiting "tart run" does. + XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(vmDirectory.path)) + let expectedDirectory = FileManager.default.currentDirectoryPath + let address = try SocketAddress(unixDomainSocketPath: "control.sock") + let descriptor = socket(AF_UNIX, SOCK_STREAM, 0) + XCTAssertGreaterThanOrEqual(descriptor, 0) + XCTAssertEqual(address.withSockAddr { bindTestSocket(descriptor, $0, socklen_t($1)) }, 0) + XCTAssertEqual(close(descriptor), 0) + XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(originalDirectory)) + + var controlSocket: ControlSocket? = try await ControlSocket(socketURL) + let eventLoopGroup = try XCTUnwrap(controlSocket?.eventLoopGroup) + do { + let serverChannel = try XCTUnwrap(controlSocket?.serverChannel) + XCTAssertEqual(FileManager.default.currentDirectoryPath, expectedDirectory) + XCTAssertTrue(FileManager.default.fileExists(atPath: socketURL.path)) + try await serverChannel.executeThenClose { _ in } + } + controlSocket = nil + try await eventLoopGroup.shutdownGracefully() + } + private func makeTemporaryDirectory() throws -> URL { let directory = FileManager.default.temporaryDirectory.appendingPathComponent( UUID().uuidString,