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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
76 changes: 76 additions & 0 deletions Sources/Basics/Concurrency/PID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// PID.swift
// SwiftPM
//
// Created by John Bute on 2025-04-24.
//

import Foundation

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

init(scratchDirectory: AbsolutePath)

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



public struct pidFile: pidFileManipulator {

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 pidFilePath: AbsolutePath {
return self.scratchDirectory.appending(component: "PackageManager.lock.pid")
}

/// Read the pid file
public func readPID() -> Int32? {
// Check if the file exists
let filePath = pidFilePath.pathString
guard FileManager.default.fileExists(atPath: filePath) else {
print("File does not exist at path: \(filePath)")
return nil
}

do {
// Read the contents of the file
let pidString = try String(contentsOf: pidFilePath.asURL, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)

// Check if the PID string can be converted to an Int32
if let pid = Int32(pidString) {
return pid
} else {
return nil
}
} catch {
// Catch any errors and print them
return nil
}
}

/// Get the current PID of the process
public func getCurrentPID() -> Int32 {
return getpid()
}

/// Write .pid file containing PID of process currently using .build directory
public func writePID(pid: pid_t) throws {
try "\(pid)".write(to: pidFilePath.asURL, atomically: true, encoding: .utf8)
}

/// Delete PID file at URL
public func deletePIDFile() throws {
try FileManager.default.removeItem(at: pidFilePath.asURL)
}

}
29 changes: 25 additions & 4 deletions Sources/CoreCommands/SwiftCommandState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import _Concurrency
import ArgumentParser
import Basics
import Dispatch
import class Foundation.NSLock
import class Foundation.NSDistributedLock
import class Foundation.ProcessInfo
import PackageGraph
import PackageLoading
@_spi(SwiftPMInternal)
import PackageModel
import SPMBuildCore
import Workspace
import Foundation.NSFileManager

#if USE_IMPL_ONLY_IMPORTS
@_implementationOnly
Expand Down Expand Up @@ -287,6 +288,7 @@ public final class SwiftCommandState {

private let hostTriple: Basics.Triple?

private let pidManipulator: pidFileManipulator
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: pidFileManipulator? = nil
) throws {
self.hostTriple = hostTriple
self.fileSystem = fileSystem
Expand Down Expand Up @@ -407,6 +410,8 @@ public final class SwiftCommandState {
self.sharedSwiftSDKsDirectory = try fileSystem.getSharedSwiftSDKsDirectory(
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 @@ -1056,33 +1061,43 @@ public final class SwiftCommandState {

let workspaceLock = try FileLock.prepareLock(fileToLock: self.scratchDirectory)

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)
lockAcquired = true
} catch ProcessLockError.unableToAquireLock(let errno) {
if errno == EWOULDBLOCK {
let existingPID = self.pidManipulator.readPID()
let pidInfo = existingPID.map { "(PID: \($0)) " } ?? ""
if self.options.locations.ignoreLock {
self.outputStream
.write(
"Another instance of SwiftPM is already running using '\(self.scratchDirectory)', but this will be ignored since `--ignore-lock` has been passed"
"Another instance of SwiftPM (pid \(pidInfo) is already running using '\(self.scratchDirectory)', but this will be ignored since `--ignore-lock` has been passed"
.utf8
)
self.outputStream.flush()
} else {
self.outputStream
.write(
"Another instance of SwiftPM is already running using '\(self.scratchDirectory)', waiting until that process has finished execution..."
"Another instance of SwiftPM (pid \(pidInfo) is already running using '\(self.scratchDirectory)', waiting until that process has finished execution..."
.utf8
)
self.outputStream.flush()

// Only if we fail because there's an existing lock we need to acquire again as blocking.
try workspaceLock.lock(type: .exclusive, blocking: true)
lockAcquired = true
}
}
}

self.workspaceLock = workspaceLock

if lockAcquired || self.options.locations.ignoreLock {
try self.pidManipulator.writePID(pid: self.pidManipulator.getCurrentPID())
}
}

fileprivate func releaseLockIfNeeded() {
Expand All @@ -1094,6 +1109,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