Skip to content

ProcessID enhacement #8618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions Sources/Basics/Concurrency/PID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation

public protocol PIDFileHandler {
var scratchDirectory: AbsolutePath { get set }

init(scratchDirectory: AbsolutePath)

func readPID() throws -> Int32
func deletePIDFile() throws
func writePID(pid: Int32) throws
func getCurrentPID() -> Int32
}

public struct PIDFile: PIDFileHandler {
public var scratchDirectory: AbsolutePath

public init(scratchDirectory: AbsolutePath) {
self.scratchDirectory = scratchDirectory
}

/// Return the path of the PackageManager.lock.pid file where the PID is located
private var lockFilePath: AbsolutePath {
self.scratchDirectory.appending(component: "PackageManager.lock.pid")
}

/// Read the pid file
public func readPID() throws -> Int32 {
// Check if the file exists
let filePath = self.lockFilePath.pathString
guard FileManager.default.fileExists(atPath: filePath) else {
throw PIDError.noSuchPiDFile
}

let pidString = try String(contentsOf: lockFilePath.asURL, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)

// Try to convert to Int32, or throw an error
guard let pid = Int32(pidString) else {
throw PIDError.invalidPIDFormat
}

return pid
}

/// Get the current PID of the proces
public func getCurrentPID() -> Int32 {
ProcessInfo.processInfo.processIdentifier
}

/// Write .pid file containing PID of process currently using .build directory
public func writePID(pid: Int32) throws {
let parent = self.lockFilePath.parentDirectory
try FileManager.default.createDirectory(
at: parent.asURL,
withIntermediateDirectories: true,
attributes: nil
)

try "\(pid)".write(to: self.lockFilePath.asURL, atomically: true, encoding: .utf8)
}

/// Delete PID file at URL
public func deletePIDFile() throws {
do {
try FileManager.default.removeItem(at: self.lockFilePath.asURL)
} catch {
throw PIDError.noSuchPiDFile
}
}

public enum PIDError: Error {
case invalidPIDFormat
case noSuchPiDFile
}
}
42 changes: 34 additions & 8 deletions Sources/CoreCommands/SwiftCommandState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ public final class SwiftCommandState {

private let hostTriple: Basics.Triple?

private let pidManipulator: PIDFileHandler

package var preferredBuildConfiguration = BuildConfiguration.debug

/// Create an instance of this tool.
Expand Down Expand Up @@ -324,7 +326,8 @@ public final class SwiftCommandState {
createPackagePath: Bool,
hostTriple: Basics.Triple? = nil,
fileSystem: any FileSystem = localFileSystem,
environment: Environment = .current
environment: Environment = .current,
pidManipulator: PIDFileHandler? = nil
) throws {
self.hostTriple = hostTriple
self.fileSystem = fileSystem
Expand Down Expand Up @@ -408,6 +411,8 @@ public final class SwiftCommandState {
explicitDirectory: options.locations.swiftSDKsDirectory ?? options.locations.deprecatedSwiftSDKsDirectory
)

self.pidManipulator = pidManipulator ?? PIDFile(scratchDirectory: self.scratchDirectory)

// set global process logging handler
AsyncProcess.loggingHandler = { self.observabilityScope.emit(debug: $0) }
}
Expand Down Expand Up @@ -1059,16 +1064,24 @@ public final class SwiftCommandState {
let workspaceLock = try FileLock.prepareLock(fileToLock: self.scratchDirectory)
let lockFile = self.scratchDirectory.appending(".lock").pathString

var lockAcquired = false

// Try a non-blocking lock first so that we can inform the user about an already running SwiftPM.
do {
try workspaceLock.lock(type: .exclusive, blocking: false)
let pid = ProcessInfo.processInfo.processIdentifier
try? String(pid).write(toFile: lockFile, atomically: true, encoding: .utf8)
lockAcquired = true
} catch ProcessLockError.unableToAquireLock(let errno) {
if errno == EWOULDBLOCK {
let lockingPID = try? String(contentsOfFile: lockFile, encoding: .utf8)
let pidInfo = lockingPID.map { "(PID: \($0)) " } ?? ""

var existingProcessPID: Int32? = nil

// Attempt to read the PID
do {
existingProcessPID = try self.pidManipulator.readPID()
} catch {
self.observabilityScope.emit(debug: "Cannot read PID file: \(error)")
}

let pidInfo = existingProcessPID.map { "(PID: \($0)) " } ?? ""
if self.options.locations.ignoreLock {
self.outputStream
.write(
Expand All @@ -1087,13 +1100,20 @@ public final class SwiftCommandState {
// Only if we fail because there's an existing lock we need to acquire again as blocking.
try workspaceLock.lock(type: .exclusive, blocking: true)

let pid = ProcessInfo.processInfo.processIdentifier
try? String(pid).write(toFile: lockFile, atomically: true, encoding: .utf8)
lockAcquired = true
}
}
}

self.workspaceLock = workspaceLock

if lockAcquired || self.options.locations.ignoreLock {
do {
try self.pidManipulator.writePID(pid: self.pidManipulator.getCurrentPID())
} catch {
self.observabilityScope.emit(debug: "Failed to write to PID file: \(error)")
}
}
}

fileprivate func releaseLockIfNeeded() {
Expand All @@ -1105,6 +1125,12 @@ public final class SwiftCommandState {
self.workspaceLockState = .unlocked

self.workspaceLock?.unlock()

do {
try self.pidManipulator.deletePIDFile()
} catch {
self.observabilityScope.emit(warning: "Failed to delete PID file: \(error)")
}
}
}

Expand Down
168 changes: 168 additions & 0 deletions Tests/BasicsTests/Concurrency/PIDTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//
// PIDTests.swift
// SwiftPM
//
// Created by John Bute on 2025-05-14.
//

import Basics
import Foundation
import Testing

struct PIDTests {
@Test
func testWritePIDMultipleCalls() throws {
try withTemporaryDirectory { tmpDir in
let scratchPath = tmpDir.appending(component: "scratch")
try localFileSystem.createDirectory(scratchPath)

let pidHandler = PIDFile(scratchDirectory: scratchPath)

let pid1: Int32 = 1234
let pid2: Int32 = 5678
let pid3: Int32 = 9012

try pidHandler.writePID(pid: pid1)
try pidHandler.writePID(pid: pid2)
try pidHandler.writePID(pid: pid3)

#expect(throws: Never.self) {
try pidHandler.readPID()
}

let pid = try pidHandler.readPID()

#expect(pid == pid3)
}
}

@Test
func testDeleteExistingPIDFile() async throws {
try withTemporaryDirectory { tmpDir in
let scratchPath = tmpDir.appending(component: "scratch")
try localFileSystem.createDirectory(scratchPath)

let pidHandler = PIDFile(scratchDirectory: scratchPath)
let currentPID = pidHandler.getCurrentPID()
try pidHandler.writePID(pid: currentPID)

#expect(throws: Never.self) {
try pidHandler.deletePIDFile()
}
}
}

@Test
func testDeleteNonExistingPIDFile() async throws {
try withTemporaryDirectory { tmpDir in
let filePath = tmpDir.appending(component: "scratch")

let handler = PIDFile(scratchDirectory: filePath)

#expect(throws: PIDFile.PIDError.noSuchPiDFile) {
try handler.deletePIDFile()
}
}
}

@Test
func testFileDoesNotExist() throws {
// Create a temporary directory
try withTemporaryDirectory { tmpDir in
let filePath = tmpDir.appending(component: "scratch")
let handler = PIDFile(scratchDirectory: filePath)

#expect(throws: PIDFile.PIDError.noSuchPiDFile) {
try handler.readPID()
}
}
}

@Test
func testInvalidPIDFormat() async throws {
// Create a temporary directory
try withTemporaryDirectory { tmpDir in
let scratchPath = tmpDir.appending(component: "scratch")
try localFileSystem.createDirectory(scratchPath)

let pidFilePath = scratchPath.appending(component: "PackageManager.lock.pid")
let handler = PIDFile(scratchDirectory: scratchPath)

// Write invalid content (non-numeric PID)
let invalidPIDContent = "invalidPID"
try localFileSystem.writeFileContents(pidFilePath, bytes: .init(encodingAsUTF8: invalidPIDContent))

#expect(throws: PIDFile.PIDError.invalidPIDFormat) {
try handler.readPID()
}
}
}

// Test case to check if the function works when a valid PID is in the file
@Test
func testValidPIDFormat() throws {
// Create a temporary directory
try withTemporaryDirectory { tmpDir in
let scratchPath = tmpDir.appending(component: "scratch")
try localFileSystem.createDirectory(scratchPath)

let pidFilePath = scratchPath.appending(component: "PackageManager.lock.pid")

let handler = PIDFile(scratchDirectory: scratchPath)

// Write a valid PID content
let validPIDContent = "12345"
try localFileSystem.writeFileContents(pidFilePath, bytes: .init(encodingAsUTF8: validPIDContent))

let pid = try handler.readPID()
#expect(pid == 12345)
}
}

@Test
func testPIDFileHandlerLifecycle() throws {
try withTemporaryDirectory { tmpDir in
let scratchPath = tmpDir.appending(component: "scratch")
try localFileSystem.createDirectory(scratchPath)

let pidHandler = PIDFile(scratchDirectory: scratchPath)

// Ensure no PID file exists initially
#expect(throws: PIDFile.PIDError.noSuchPiDFile) {
try pidHandler.readPID()
}

// Write current PID
let currentPID = pidHandler.getCurrentPID()
try pidHandler.writePID(pid: currentPID)

// Read PID back
let readPID = try pidHandler.readPID()
#expect(readPID == currentPID, "PID read should match written PID")

// Delete the file
try pidHandler.deletePIDFile()

// Ensure file is gone
#expect(throws: PIDFile.PIDError.noSuchPiDFile) {
try pidHandler.readPID()
}
}
}

@Test
func testMalformedPIDFile() throws {
try withTemporaryDirectory { tmpDir in
let scratchPath = tmpDir.appending(component: "scratch")
try localFileSystem.createDirectory(scratchPath)

let pidPath = scratchPath.appending(component: "PackageManager.lock.pid")
try localFileSystem.writeFileContents(pidPath, bytes: "notanumber")

let pidHandler = PIDFile(scratchDirectory: scratchPath)
#expect(throws: PIDFile.PIDError.invalidPIDFormat) {
try pidHandler.readPID()
}
}
}
}