Skip to content

Commit

Permalink
Handle Ctrl + C properly (#73)
Browse files Browse the repository at this point in the history
* Terminate when Ctrl+C is encountered when entering credentials

* Handle Ctrl+C by catching SIGINT and converting it to task cancellation

* maxCharacters instead of (buf.count - 2)

* readStdinCredential: fix maxCharacters to be 255

* "user" variable should be named "credential"
  • Loading branch information
edigaryev authored May 16, 2022
1 parent fa9e314 commit 60b7054
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 43 deletions.
79 changes: 42 additions & 37 deletions Sources/tart/Commands/Run.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,55 @@ struct Run: AsyncParsableCommand {
let vmDir = try VMStorageLocal().open(name)
vm = try VM(vmDir: vmDir)

Task {
do {
try await vm!.run()
await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
do {
try await vm!.run()

Foundation.exit(0)
} catch {
print(error)
Foundation.exit(0)
} catch {
print(error)

Foundation.exit(1)
Foundation.exit(1)
}
}
}

if noGraphics {
dispatchMain()
} else {
// UI mumbo-jumbo
let nsApp = NSApplication.shared
nsApp.setActivationPolicy(.regular)
nsApp.activate(ignoringOtherApps: true)

nsApp.applicationIconImage = NSImage(data: AppIconData)

struct MainApp: App {
var body: some Scene {
WindowGroup(vm!.name) {
Group {
VMView(vm: vm!).onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height))
}.commands {
// Remove some standard menu options
CommandGroup(replacing: .help, addition: {})
CommandGroup(replacing: .newItem, addition: {})
CommandGroup(replacing: .pasteboard, addition: {})
CommandGroup(replacing: .textEditing, addition: {})
CommandGroup(replacing: .undoRedo, addition: {})
CommandGroup(replacing: .windowSize, addition: {})
}
}
if noGraphics {
dispatchMain()
} else {
runUI()
}
}
}

MainApp.main()
private func runUI() {
let nsApp = NSApplication.shared
nsApp.setActivationPolicy(.regular)
nsApp.activate(ignoringOtherApps: true)

nsApp.applicationIconImage = NSImage(data: AppIconData)

struct MainApp: App {
var body: some Scene {
WindowGroup(vm!.name) {
Group {
VMView(vm: vm!).onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height))
}.commands {
// Remove some standard menu options
CommandGroup(replacing: .help, addition: {})
CommandGroup(replacing: .newItem, addition: {})
CommandGroup(replacing: .pasteboard, addition: {})
CommandGroup(replacing: .textEditing, addition: {})
CommandGroup(replacing: .undoRedo, addition: {})
CommandGroup(replacing: .windowSize, addition: {})
}
}
}

MainApp.main()
}
}

Expand Down
28 changes: 23 additions & 5 deletions Sources/tart/Credentials.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import Foundation

enum CredentialsError: Error {
case CredentialRequired(which: String)
case CredentialTooLong(message: String)
}

class Credentials {
static func retrieveKeychain(host: String) throws -> (String, String)? {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
Expand Down Expand Up @@ -34,13 +39,26 @@ class Credentials {
}

static func retrieveStdin() throws -> (String, String) {
print("User: ", terminator: "")
let user = readLine() ?? ""
let user = try readStdinCredential(name: "username", prompt: "User: ", isSensitive: false)
let password = try readStdinCredential(name: "password", prompt: "Password: ", isSensitive: true)

return (user, password)
}

let rawPass = getpass("Password: ")
let pass = String(cString: rawPass!, encoding: .utf8)!
private static func readStdinCredential(name: String, prompt: String, maxCharacters: Int = 255, isSensitive: Bool) throws -> String {
var buf = [CChar](repeating: 0, count: maxCharacters + 1 /* sentinel */ + 1 /* NUL */)
guard let rawCredential = readpassphrase(prompt, &buf, buf.count, isSensitive ? RPP_ECHO_OFF : RPP_ECHO_ON) else {
throw CredentialsError.CredentialRequired(which: name)
}

let credential = String(cString: rawCredential).trimmingCharacters(in: .newlines)

if credential.count > maxCharacters {
throw CredentialsError.CredentialTooLong(
message: "\(name) should contain no more than \(maxCharacters) characters")
}

return (user, pass)
return credential
}

static func store(host: String, user: String, password: String) throws {
Expand Down
24 changes: 24 additions & 0 deletions Sources/tart/Root.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ArgumentParser
import Foundation

@main
struct Root: AsyncParsableCommand {
Expand All @@ -16,4 +17,27 @@ struct Root: AsyncParsableCommand {
Push.self,
Delete.self,
])

public static func main() async throws {
// Handle cancellation by Ctrl+C
let task = withUnsafeCurrentTask { $0 }!
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT)
sigintSrc.setEventHandler {
task.cancel()
}
sigintSrc.activate()

// Parse and run command
do {
var command = try parseAsRoot()

if var asyncCommand = command as? AsyncParsableCommand {
try await asyncCommand.run()
} else {
try command.run()
}
} catch {
exit(withError: error)
}
}
}
14 changes: 13 additions & 1 deletion Sources/tart/VM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,19 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
}

sema.wait()
await withTaskCancellationHandler(operation: {
sema.wait()
}, onCancel: {
sema.signal()
})

if Task.isCancelled {
DispatchQueue.main.sync {
Task {
try await self.virtualMachine.stop()
}
}
}
}

static func craftConfiguration(diskURL: URL, auxStorage: VZMacAuxiliaryStorage, vmConfig: VMConfig) throws -> VZVirtualMachineConfiguration {
Expand Down

0 comments on commit 60b7054

Please sign in to comment.