Listening to Stdin in Swift

Listening to stdin in Swift

Normally standard input buffers everything until a newline is entered, that's why a typical standard input is read by lines:

while let line = readLine() {
print(line)
}

(press CTRL+D to send EOF, that is end the input)

To really read every character separately, you need to enter raw mode and that means use the low level terminal functions:

// see https://stackoverflow.com/a/24335355/669586
func initStruct<S>() -> S {
let struct_pointer = UnsafeMutablePointer<S>.allocate(capacity: 1)
let struct_memory = struct_pointer.pointee
struct_pointer.deallocate()
return struct_memory
}

func enableRawMode(fileHandle: FileHandle) -> termios {
var raw: termios = initStruct()
tcgetattr(fileHandle.fileDescriptor, &raw)

let original = raw

raw.c_lflag &= ~(UInt(ECHO | ICANON))
tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &raw);

return original
}

func restoreRawMode(fileHandle: FileHandle, originalTerm: termios) {
var term = originalTerm
tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &term);
}

let stdIn = FileHandle.standardInput
let originalTerm = enableRawMode(fileHandle: stdIn)

var char: UInt8 = 0
while read(stdIn.fileDescriptor, &char, 1) == 1 {
if char == 0x04 { // detect EOF (Ctrl+D)
break
}
print(char)
}

// It would be also nice to disable raw input when exiting the app.
restoreRawMode(fileHandle: stdIn, originalTerm: originalTerm)

Reference https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html

Objective C : How to keep listening to incoming data on stdin?

The solution is:

int main(int argc, char * argv[]) {
@autoreleasepool {
NSFileHandle *stdIn = [NSFileHandle fileHandleWithStandardInput];

[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification
object:stdIn
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
NSError * stdinError = nil;
NSData * rawReqLen = [stdIn readDataUpToLength:4 error:&stdinError];
if(rawReqLen == nil || stdinError != nil) exit(1);
uint32_t reqLen;
[rawReqLen getBytes:&reqLen length:4];
reqLen = OSSwapLittleToHostInt32(reqLen);

NSData * req = [stdIn readDataUpToLength:reqLen error:&stdinError];
if(stdinError != nil) exit(1);

handleRequest(req);
[stdIn waitForDataInBackgroundAndNotify];
}];
[stdIn waitForDataInBackgroundAndNotify];
}

NSRunLoop *loop = [NSRunLoop currentRunLoop];
[loop acceptInputForMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
[loop run];
return 0;
}

Calling exit(1) could also be replaced by a new call to [stdIn waitForDataInBackgroundAndNotify]; if you want to keep the port opened instead.

This handles incoming requests well, but breaks the response: writing to stdout does not work anymore, I might need to stop the run loop for each response and restart it when done. (not tested yet)

Reading stdio from Mac Application

NSFileHandle, a wrapper for file descriptor is the solution.You can use file handle objects to access data associated with files, sockets, pipes, and devices. For files, you can read, write, and seek within the file.

DispatchQueue.global(qos: .background).async {
let input = FileHandle.standardInput
var aStr : String!
while true {
//Read first 4 bytes to get message length details(As per NativeMessagingAPI protocol)
let lengthDetails = input.readData(ofLength:4)
let length = lengthDetails.withUnsafeBytes { (ptr: UnsafePointer<Int32>) -> Int32 in
return ptr.pointee
}
let data = input.readData(ofLength:Int(length))//input.availableData
if (data.count > 0) {
var aStr = String(data:data, encoding:String.Encoding.utf8)
if (aStr != nil) {
DispatchQueue.main.async {
//Handle data logic here
}
}
}
}

}

Getting data from the nstask - communicating with command line - objective c

Give an NSPipe or an NSFileHandle as the task's standardOutput, and read from that.

NSTask * list = [[NSTask alloc] init];
[list setLaunchPath:@"/bin/ls"];
[list setCurrentDirectoryPath:@"/"];

NSPipe * out = [NSPipe pipe];
[list setStandardOutput:out];

[list launch];
[list waitUntilExit];
[list release];

NSFileHandle * read = [out fileHandleForReading];
NSData * dataRead = [read readDataToEndOfFile];
NSString * stringRead = [[[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding] autorelease];
NSLog(@"output: %@", stringRead);

Note that if you use a pipe, you have to worry about the pipe filling up. If you provide an NSFileHandle instead, the task can output all it wants without you having to worry about losing any, but you also get the overhead of having to write the data out to disk.

swift main run / build from command line

the problem

the command command line specify sub-command but miss the executable product

$ swift run read-aliases

the solution

you must use the executable product before sub-command

swift run easy-aliaser read-aliases

steps to reproduce

[so-test]$ git clone https://github.com/CreaTorAleXander/easy-aliaser
Cloning into 'easy-aliaser'...
remote: Enumerating objects: 41, done.
remote: Counting objects: 100% (41/41), done.
remote: Compressing objects: 100% (32/32), done.
remote: Total 41 (delta 2), reused 38 (delta 1), pack-reused 0
Unpacking objects: 100% (41/41), done.
[so-test]$ cd easy-aliaser

#
# [easy-aliaser (main)]$ swift package generate-xcodeproj
# edited the wired filename in your code just to refer to an existing file
# run the project in XCode without problems
# back to the command line
#

[easy-aliaser (main)]$ swift run easy-aliaser read-aliases
Fetching https://github.com/apple/swift-argument-parser
Cloning https://github.com/apple/swift-argument-parser
Resolving https://github.com/apple/swift-argument-parser at 0.3.1
/Users/me/projects/so-test/easy-aliaser/Sources/easy-aliaser/main.swift:23:13: warning: result of call to 'readFile(path:)' is unused
readFile(path: "/Users/me/projects/so-test/65203567/myzshrc")
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[3/3] Linking easy-aliaser
These are your Aliases
who_listening='sudo lsof -nP -iTCP -sTCP:LISTEN'
du-docker="du -h ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2 && docker images"
du-openshift=" /Users/ronda/.docker/machine/machines/openshift/disk.vmdk && du -h ~/.docker/machine/machines/openshift/boot2docker.iso"


Related Topics



Leave a reply



Submit