Skip to content

Allow swift-build to have async entrypoint #7369

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

Merged
merged 5 commits into from
Feb 26, 2024
Merged

Conversation

MaxDesiatov
Copy link
Contributor

@MaxDesiatov MaxDesiatov commented Feb 24, 2024

Due to a bug in Swift 5.9, we can't have multiple executables with async entrypoints linked into test products. This was the reason why we weren't able to make swift-build entrypoint async. Since we have a single swift-package-manager executable anyway, we can link that one into tests instead, and pass "simulated" executable name via an environment variable. This allows us to work around the bug and start using structured concurrency from swift-build entrypoint.

Also converted SwiftRunCommand entrypoint to async to reliably reproduce possible regressions highlighted by RunCommandTests.testSwiftRunSIGINT. These regressions were handled by resetting signals masks and closing file descriptors before a subsequent call to execv.

This change is a prerequisite for #7367.

Due to a bug in Swift 5.9, we can't have multiple executables with `async` entrypoints linked into test products. This was the reason why we weren't able to make `swift-build` entrypoint `async`. Since we have a single `swift-package-manager` executable anyway, we can link that one into tests instead, and pass "simulated" executable name via an environment variable. This allows us to work around the bug and start using structured concurrency from `swift-build` entrypoint.
@MaxDesiatov MaxDesiatov self-assigned this Feb 24, 2024
@MaxDesiatov MaxDesiatov added bug concurrency test suite improvements to SwiftPM test suite labels Feb 24, 2024
@MaxDesiatov
Copy link
Contributor Author

@swift-ci test

@MaxDesiatov
Copy link
Contributor Author

@swift-ci test

@MaxDesiatov
Copy link
Contributor Author

@swift-ci test windows

@MaxDesiatov
Copy link
Contributor Author

@swift-ci clean test windows

@MaxDesiatov
Copy link
Contributor Author

@swift-ci test windows

…xd/async-swift-build

# Conflicts:
#	Sources/Commands/SwiftBuildCommand.swift
#	Sources/swift-build/Entrypoint.swift
#	Sources/swift-package-manager/SwiftPM.swift
@MaxDesiatov
Copy link
Contributor Author

@swift-ci test

@MaxDesiatov
Copy link
Contributor Author

@swift-ci test windows

2 similar comments
@MaxDesiatov
Copy link
Contributor Author

@swift-ci test windows

@MaxDesiatov
Copy link
Contributor Author

@swift-ci test windows

Comment on lines 106 to 108
// FIXME: not sure how this was supposed to work previously, since `swift-run` uses `execv` that doesn't inherit
// signal handlers, and the spawned process doesn't install signal handlers on its own. `async` effect on
// `@main` arguably makes it behave correctly?
Copy link
Member

@kateinoigakukun kateinoigakukun Feb 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this issue is not only a problem in tests but causes a real problem. I could reproduce this uninterruptable swift-run when invoking just built swift-package-manager through symlink named swift-run.

Based on my quick research, it seems like libdispatch blocks SIGINT by sigmask and spawns a dedicated thread to catch those signals. I think that's why we haven't seen this before with sync entrypoints.

So the problematic code can be minimized as follows.

import Dispatch
import Foundation

DispatchQueue.main.async {
  let program = "/usr/bin/sleep"
  let args = ["sleep", "10"]

  let cArgs = UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>.allocate(capacity: args.count + 1)
  for (i, arg) in args.enumerated() {
    cArgs[i] = strdup(arg)
  }
  cArgs[args.count] = nil

  execv(program, cArgs)
  perror("execv")
}

dispatchMain()

I confirmed we can unblock signals as follows, but I'm not 100% sure it's a safe way.

var mask = sigset_t()
sigemptyset(&mask)
pthread_sigmask(SIG_SETMASK, &mask, nil)
execv(program, cArgs)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC Dispatch blocks delivery of SIGINT whenever some code is executed in DispatchQueue.main.async, is that correct? Isn't it a bug in Dispatch then and could be fixed on their side?

Copy link
Member

@kateinoigakukun kateinoigakukun Feb 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, libdispatch blocks deliverry of SIGINT during the whole program execution, but it creates a dedicated thread to listen to signals before blocking the signals in the main thread. SIGINT is usually handled by the dedicated thread and I guess the actual signal handling works are scheduled on some dispatch queues. So this is not a bug in libdispatch at all, but they don't provide a graceful way to reset signal mask states. Therefore I suggested resetting masks manually just before execv in SwiftPM as one of the options.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, I've addressed it in a subsequent commit. The signals reset code had to be slightly different for it to work in our swift run use case and I'm also closing file descriptors as suggested by @weissi

@MaxDesiatov
Copy link
Contributor Author

@swift-ci test

@MaxDesiatov MaxDesiatov enabled auto-merge (squash) February 26, 2024 13:19
@MaxDesiatov
Copy link
Contributor Author

@swift-ci clean test windows


// 2. close all file descriptors.
for i in 3..<getdtablesize() {
close(i)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any open fds managed by SwiftPM at this point? My concern is that this might corrupt the state of underlying libraries like libc and cause other problems for the case when exec fails for some reason. I think closing fds is not a part of this function's responsibilities but should be handled by those fds owners using O_CLOEXEC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weissi WDYT?

Copy link
Contributor Author

@MaxDesiatov MaxDesiatov Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any open fds managed by SwiftPM at this point?

I don't think we should, but I don't have any certainty our swift-tools-support-core or swift-corelibs-foundation APIs don't leak anything. I could see the FileHandle API or some reference or escapable type captured somewhere with no explicit close call on it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the discussion point here is whether to prioritize potential state corruption after the exec fails or potential fd leak during the exec'ed program execution. IMO the former issue is more critical than the latter since we don't have a large number of open fd's here and emitting proper error diagnostics when a problem occurs is essential for further investigation.

Copy link
Contributor Author

@MaxDesiatov MaxDesiatov Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC the argument that Johannes is making is that bash is doing it this way and it works fine for them. Whatever state underlying libraries have, it'll be gone with execv call anyway https://github.com/bminor/bash/blob/f3b6bd19457e260b65d11f2712ec3da56cef463f/execute_cmd.c#L6162C1-L6173

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's what UNIX programs are expected to do if they exec: close the file descriptors that they're not intending to pass on. LGTM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that makes sense to me 👍

@MaxDesiatov
Copy link
Contributor Author

@swift-ci test windows

@MaxDesiatov MaxDesiatov merged commit 8ec22d7 into main Feb 26, 2024
@MaxDesiatov MaxDesiatov deleted the maxd/async-swift-build branch February 26, 2024 16:40
furby-tm pushed a commit to wabiverse/swift-package-manager that referenced this pull request May 15, 2024
Due to a bug in Swift 5.9, we can't have multiple executables with
`async` entrypoints linked into test products. This was the reason why
we weren't able to make `swift-build` entrypoint `async`. Since we have
a single `swift-package-manager` executable anyway, we can link that one
into tests instead, and pass "simulated" executable name via an
environment variable. This allows us to work around the bug and start
using structured concurrency from `swift-build` entrypoint.

This change is a prerequisite for
swiftlang#7367.
furby-tm pushed a commit to wabiverse/swift-package-manager that referenced this pull request May 15, 2024
Due to a bug in Swift 5.9, we can't have multiple executables with
`async` entrypoints linked into test products. This was the reason why
we weren't able to make `swift-build` entrypoint `async`. Since we have
a single `swift-package-manager` executable anyway, we can link that one
into tests instead, and pass "simulated" executable name via an
environment variable. This allows us to work around the bug and start
using structured concurrency from `swift-build` entrypoint.

This change is a prerequisite for
swiftlang#7367.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug concurrency test suite improvements to SwiftPM test suite
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants