I'm developing a macOS app where I need to connect using telnet to a device and send commands to it.
(BTW, I'm connecting to a Roku)
This is as far as I got
func telnet(to host: String, port: String) {
let process = Process()
let pipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/local/bin/telnet")
process.arguments = [host, port]
process.standardOutput = pipe
do {
try process.run()
} catch {
return
}
let resultData = pipe.fileHandleForReading.readDataToEndOfFile()
let result = String (data: resultData, encoding: .utf8) ?? ""
print(result)
}
telnet(to: "10.0.1.11", port: "8080")
The problem is that the Process is closed immediately. I need to leave the Process open and somehow send commands to the device and then close the connection.
So those are my questions:
How can I leave the Process running?
How can I send commands to the device through the telnet connection?
Your best bet will be to avoid using Process and make a network connection directly from your application. (This will also free you from your dependency on /usr/local/bin/telnet, which is not present in a clean install of macOS.)
The SwiftSocket pod looks like an excellent way of doing this. The "Client socket example" in the documentation appears to cover most of your questions.
Related
I built a Vapor 4 app that is currently deployed on a local Ubuntu 18 server VM, running behind NGINX and serving users without any issues.
Now I would like one of my web server routes to react to specific POSTs by executing a Bash command via Process (this, in order to send messages to a dedicated Slack channel via slack-cli, a tool I already use for other purposes and that is already configured and working both on my development machine and on the Ubuntu server).
With the following code, everything's working as desired when I run my Vapor app on my local machine (i.e.: immediately after the POST to the route the expected message appears in the Slack channel):
// What follows is placed inside my dedicated app.post route, after checking the response is valid...
let slackCLIPath = "/home/linuxbrew/.linuxbrew/bin/" // This is the slack-cli path on the Linux VM; I swap it with "/opt/homebrew/bin/" when running the app on my local Mac
_ = runShellScript("\(slackCLIPath)slack chat send '\(myMessageComingFromThePOST)' '#myChannel'")
// ...
// runShellScript() called above is the dedicated function (coming from [this SO answer](https://stackoverflow.com/a/43014767/3765705) that executes the Shell process, and its code follows:
func runShellScript(_ cmd: String) -> String? {
let pipe = Pipe()
let process = Process()
process.launchPath = "/bin/sh"
process.arguments = ["-c", String(format:"%#", cmd)]
process.standardOutput = pipe
let fileHandle = pipe.fileHandleForReading
process.launch()
return String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)
}
My issue is that, when I deploy my app on the Ubuntu server, both in debug and production, the execution of the Shell process does not happen like it did on my Mac: I have no errors logged by Vapor when I POST to that route, but no messages appear in Slack, even if I wait a while!
But here's the tricky part: as soon as I stop my Vapor app on the server, all messages are sent to Slack at once.
After a lot of testing (which obviously included confirming that from the server was possible to post without delays to Slack by using the exact same command passed to NSTask's Process class in my Vapor app), it appears like the Bash command is not executed until my Vapor app quits.
Clearly I'm missing something on how to make Process work "in realtime" with Vapor, and I'll be grateful for all the help I can get.
You need to wait until the task has finished. Looks like you're deadlocking yourself. This is how I run stuff on Linux:
// MARK: - Functions
#discardableResult
func shell(_ args: String..., returnStdOut: Bool = false, stdIn: Pipe? = nil) throws -> (Int32, Pipe) {
return try shell(args, returnStdOut: returnStdOut, stdIn: stdIn)
}
#discardableResult
func shell(_ args: [String], returnStdOut: Bool = false, stdIn: Pipe? = nil) throws -> (Int32, Pipe) {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/env")
task.arguments = args
let pipe = Pipe()
if returnStdOut {
task.standardOutput = pipe
}
if let stdIn = stdIn {
task.standardInput = stdIn
}
try task.run()
task.waitUntilExit()
return (task.terminationStatus, pipe)
}
extension Pipe {
func string() -> String? {
let data = self.fileHandleForReading.readDataToEndOfFile()
let result: String?
if let string = String(data: data, encoding: String.Encoding.utf8) {
result = string
} else {
result = nil
}
return result
}
}
Important lines being starting it with try task.run() and waiting with task.waitUntilExit()
I have a simple shell script which launches an X11 app. When I execute this shell script form my login shell / terminal xQuartz starts and I get a display. However the process doesn't get a display for xQuartz when running the script from within swift. Any idea how I can get the display?
Also what is the best way to detect if xQuartz is installed? Checking if xterm exists?
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/sh")
let startScriptURL = Bundle.main.url(forResource: "run", withExtension: "sh")
guard let startScriptPath = startScriptURL?.path else {
return
}
process.arguments = [startScriptPath]
do {
try process.run()
} catch let error {
print(error)
}
run.sh:
#!/bin/sh
/opt/X11/bin/xeyes
I figured our how to pass the DISPLAY environment or any environmental variable to Process.
The current environment can be obtained by:
ProcessInfo().environment
So I use now this:
let process = Process()
guard let envDisplay = ProcessInfo().environment["DISPLAY"] else {
print("Please install xQuartz")
return
}
process.environment = ["DISPLAY": envDisplay]
I got the idea from here: Issue launching X11 app via NSTask
I am trying to make my program connect to another computer using SSH. It would be generic (the user would provide the hostname, IP, and password), so keys can't be used. This is the code I have so far:
func terminalSSH(host:String, password:String, IP:String) {
let pipe = Pipe()
let args = ["-p", password, "ssh", "\(host)#\(IP)"]
let task = Process()
task.launchPath = "/usr/local/bin/sshpass"
task.arguments = args
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
print(output)
}
My issue is that I can't see if the connection was successful using sshpass. I want the user to be notified the moment the connection succeeded or failed. Also, it seems like sshpass is the only command I can use to SSH because Terminal forces the user to input the password. I know this code works to establish a connection because the system.log on my test target computer displays it. Thanks.
Firstly, you should not be using sshpass and a plain text password, but private keys.
To get the tasks exit code, you can use the following:
task.waitUntilExit()
let status = task.terminationStatus
Do know however, that waitUntilExit will block the return of this method until the session has ended.
Whether the session ends in 10 minutes, or immediately due to an error, you can evaluate the terminationStatus to see how the program exited.
If the return code is 0, everything exited nicely; if there is a non-zero value, you can take different actions based on the reason for failure.
task.waitUntilExit()
let status = task.terminationStatus
if status == 0 {
print("Graceful exit.")
} else {
print("Failed with code " + status)
}
Detecting and responding to a successful connection will be more difficult here, as the waitUntilExit call may block for as long as the ssh session is open.
You also have the option of using a loop, and checking task.isRunning.
Recently, I found a pure swift socket server and client called IBM BlueSocket.
It is suitable for me that it does server-cleint communication.
It has a pretty simple sample. but I encountered some problems.
1. How to run it on a GUI application's run loop?
2. How to run it and support multi connections?
For what it's worth, I present the world's simplest chat client.
import Foundation
import Socket
// Very simplified chat client for BlueSocket - no UI, it just connects to the echo server,
// exchanges a couple of messages and then disconnects.
// You can run two instances of Xcode on your Mac, with the BlueSocketEchoServer running in one and
// this program running in the other. It has been tested running in the iPhone simulator, i.e.,
// under iOS, without problems.
// License: Public domain.
public class BlueSocketChatClient {
public func runClient() {
do {
let chatSocket = try Socket.create(family: .inet6)
try chatSocket.connect(to: "127.0.0.1", port: 1337)
print("Connected to: \(chatSocket.remoteHostname) on port \(chatSocket.remotePort)")
try readFromServer(chatSocket)
try chatSocket.write(from: "Hello to you too!")
try readFromServer(chatSocket)
try chatSocket.write(from: "Bye now!\n")
try chatSocket.write(from: "QUIT")
sleep(1) // Be nice to the server
chatSocket.close()
}
catch {
guard let socketError = error as? Socket.Error else {
print("Unexpected error ...")
return
}
print("Error reported:\n \(socketError.description)")
}
}
// This is very simple-minded. It blocks until there is input, and it then assumes that all the
// relevant input has been read in one go.
func readFromServer(_ chatSocket : Socket) throws {
var readData = Data(capacity: chatSocket.readBufferSize)
let bytesRead = try chatSocket.read(into: &readData)
guard bytesRead > 0 else {
print("Zero bytes read.")
return
}
guard let response = String(data: readData, encoding: .utf8) else {
print("Error decoding response ...")
return
}
print(response)
}
}
Bill Abt: If you can use this in any way you're welcome to it.
The sample has been recently updated and illustrates use of the GCD based Dispatch API to do multi-threading and supporting multiple connections. It also should give you a idea on how to run it on the main queue (which'll work for either a GUI or server application).
I want to find Mac and IP addresses of all the devices connected to the local network in Swift. I looked into AFNetworking/Alamofire, but it does not seem have the functionality I want. So, which API or Library I can use?
AFNetworking and Alamofire "only" handle requests.
But as i understood you want to scan the local network for devices.
A quick Google search brought me to this: https://stackoverflow.com/a/21992359/2753395
You can try to type "arp -a" in your Terminal, or use this in swift
let theOutput = Pipe()
func shell(Path:String ,args: String...) -> Int32 {
let task = Process()
task.launchPath = Path
task.arguments = args
task.standardOutput = theOutput
task.standardError = theOutput
task.launch()
task.waitUntilExit()
return task.terminationStatus
}
shell(Path:"/usr/sbin/arp",args: "-a")
let theTaskData = theOutput.fileHandleForReading.readDataToEndOfFile()
let stringResult = String(data: theTaskData, encoding: .utf8)
print(stringResult!)