Skip to content

Commit

Permalink
Merge pull request #16427 from Rinzwind/libtty
Browse files Browse the repository at this point in the history
Add LibTTY providing access to the VM library for spawning a process connected to a pseudo-terminal
  • Loading branch information
guillep authored Apr 15, 2024
2 parents b82cba2 + 50574e2 commit 8630bb8
Show file tree
Hide file tree
Showing 4 changed files with 411 additions and 3 deletions.
265 changes: 265 additions & 0 deletions src/UnifiedFFI-Tests/LibTTYTest.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
Class {
#name : 'LibTTYTest',
#superclass : 'TestCase',
#category : 'UnifiedFFI-Tests-Libraries',
#package : 'UnifiedFFI-Tests',
#tag : 'Libraries'
}

{ #category : 'asserting' }
LibTTYTest >> assertProcessSpawnedWithFileDescriptor: fileDescriptor path: path arguments: arguments environment: environment input: inputString writes: expectedOutputString hasStatus: expectedStatus [

| pid status writeValue inputByteArray outputByteArray |

pid := -1.
status := nil.
outputByteArray :=
[
pid := LibTTY uniqueInstance ttySpawn: fileDescriptor path: path
arguments: arguments
environment: environment.
self deny: pid equals: -1.
inputString ifNotEmpty: [
inputByteArray := inputString utf8Encoded.
writeValue := LibC uniqueInstance write: fileDescriptor buffer: inputByteArray size: inputByteArray size.
self assert: writeValue equals: inputByteArray size ].
self readOutput: fileDescriptor
] ensure: [
LibC uniqueInstance close: fileDescriptor.
status := pid ~= -1 ifTrue: [ self getProcessStatus: pid ] ].
self assert: outputByteArray utf8Decoded equals: expectedOutputString.
self assert: status equals: expectedStatus.
]

{ #category : 'asserting' }
LibTTYTest >> assertProcessSpawnedWithPseudoTerminalPath: path arguments: arguments environment: environment input: inputString writes: expectedOutputString hasStatus: expectedStatus [

self assertProcessSpawnedWithFileDescriptor: self openPseudoTerminal
path: path
arguments: arguments
environment: environment
input: inputString
writes: expectedOutputString
hasStatus: expectedStatus
]

{ #category : 'asserting' }
LibTTYTest >> assertShellProcessWithCommand: shellCommand hasStatus: expectedStatus [

self assertShellProcessWithCommand: shellCommand writes: '' hasStatus: expectedStatus
]

{ #category : 'asserting' }
LibTTYTest >> assertShellProcessWithCommand: shellCommand input: inputString writes: expectedOutputString hasStatus: expectedStatus [

self assertProcessSpawnedWithPseudoTerminalPath: '/bin/sh'
arguments: { '/bin/sh'. '-c'. shellCommand }
environment: Smalltalk os environment
input: inputString
writes: expectedOutputString
hasStatus: expectedStatus
]

{ #category : 'asserting' }
LibTTYTest >> assertShellProcessWithCommand: shellCommand writes: expectedOutputString hasStatus: expectedStatus [

self assertShellProcessWithCommand: shellCommand input: '' writes: expectedOutputString hasStatus: expectedStatus
]

{ #category : 'private' }
LibTTYTest >> flagOpenRDWR [

^ 2
]

{ #category : 'private' }
LibTTYTest >> getProcessStatus: pid [

| statusArray |

statusArray := FFIExternalArray externalNewType: 'int' size: 1.
^ [
| pid2 statusInteger upper lower |
pid2 := LibC uniqueInstance waitpid: pid status: statusArray getHandle options: 0.
pid2 = pid ifFalse: [
Error signal: 'Could not get process status' ].
statusInteger := statusArray first.
upper := (statusInteger bitShift: -8) bitAnd: 16rFF.
lower := statusInteger bitAnd: 16r7F.
(lower > 0) ifTrue: [ 128 + lower ] ifFalse: [ upper ]
] ensure: [
statusArray free ]
]

{ #category : 'private' }
LibTTYTest >> openPseudoTerminal [

| fdm |

(fdm := LibC uniqueInstance posix_openpt: self flagOpenRDWR) ~= -1 ifFalse: [
Error signal: 'Could not open pseudo-terminal device' ].
(LibC uniqueInstance grantpt: fdm) ~= -1 ifFalse: [
Error signal: 'Could not grant access to the slave pseudo-terminal device' ].
(LibC uniqueInstance unlockpt: fdm) ~= -1 ifFalse: [
Error signal: 'Could not unlock the slave pseudo-terminal device' ].
^ fdm
]

{ #category : 'private' }
LibTTYTest >> performTest [

Smalltalk os isWindows ifTrue: [
self skip ].
super performTest
]

{ #category : 'private' }
LibTTYTest >> readOutput: fdm [

^ ExternalAddress allocate: 1024 bytesDuring: [ :buffer |
ByteArray streamContents: [ :stream |
| count |
[ (count := LibC uniqueInstance read: fdm buffer: buffer size: buffer size) > 0 ] whileTrue: [
1 to: count do: [ :index |
stream nextPut: (buffer byteAt: index) ] ] ] ]
]

{ #category : 'tests' }
LibTTYTest >> test1 [

| path fileDescriptorPseudoTerminal template fileDescriptorFile |

path := (#('/bin' '/usr/bin') collect: [ :element | element , '/true' ])
detect: [ :element | element asFileReference exists ].

fileDescriptorPseudoTerminal := self openPseudoTerminal.
self assertProcessSpawnedWithFileDescriptor: fileDescriptorPseudoTerminal
path: path
arguments: { path }
environment: Smalltalk os environment
input: ''
writes: ''
hasStatus: 0.

(template := ExternalAddress fromString: '/tmp/file.XXXXXX')
autoRelease.
fileDescriptorFile := LibC uniqueInstance mkstemp: template.
self deny: fileDescriptorFile equals: -1.
[
self assertProcessSpawnedWithFileDescriptor: fileDescriptorFile
path: path
arguments: { path }
environment: Smalltalk os environment
input: ''
writes: ''
hasStatus: 127
] ensure: [
template utf8StringFromCString asFileReference delete ].
]

{ #category : 'tests' }
LibTTYTest >> test2 [

self assertProcessSpawnedWithPseudoTerminalPath: '/bin/echo'
arguments: #('/bin/echo' 'Argument1' 'Argument2' 'Argument3')
environment: Smalltalk os environment
input: ''
writes: 'Argument1 Argument2 Argument3' , String crlf
hasStatus: 0.

self assertProcessSpawnedWithPseudoTerminalPath: '/bin/echo'
arguments: #('/bin/echo' 'ĀĂĄ')
environment: Smalltalk os environment
input: ''
writes: 'ĀĂĄ' , String crlf
hasStatus: 0.

self assertProcessSpawnedWithPseudoTerminalPath: '/usr/bin/env'
arguments: #('/usr/bin/env')
environment: (OrderedDictionary with: 'VAR1' -> 'value1' with: 'VAR2' -> 'value2' with: 'VAR3' -> 'value3')
input: ''
writes: (String crlf join: #('VAR1=value1' 'VAR2=value2' 'VAR3=value3' ''))
hasStatus: 0.

self assertProcessSpawnedWithPseudoTerminalPath: '/usr/bin/env'
arguments: #('/usr/bin/env')
environment: (OrderedDictionary with: 'VAR' -> 'ĀĂĄ')
input: ''
writes: (String crlf join: #('VAR=ĀĂĄ' ''))
hasStatus: 0.

self assertProcessSpawnedWithPseudoTerminalPath: '/usr/bin/head'
arguments: #('/usr/bin/head' '-n' '2')
environment: Smalltalk os environment
input: (String lf join: #('Line1' 'Line2' 'Line3' ''))
writes: (String crlf join: #('Line1' 'Line2' 'Line3' 'Line1' 'Line2' ''))
hasStatus: 0.

self assertProcessSpawnedWithPseudoTerminalPath: '/bin/doesnotexist'
arguments: #('/bin/doesnotexist')
environment: Smalltalk os environment
input: ''
writes: 'Error in tty_spawn at execve(path, argv, envp): No such file or directory' , String crlf
hasStatus: 127.
]

{ #category : 'tests' }
LibTTYTest >> test3 [

| formatDictionary echoesControlCharacters |

self assertShellProcessWithCommand: 'exit 42'
input: ''
writes: ''
hasStatus: 42.

formatDictionary := Dictionary <- {
'lf' -> Character lf.
'crlf' -> String crlf.
'end' -> Character end.
'bs' -> Character backspace }.
echoesControlCharacters := OSPlatform current isMacOSX.
self assertShellProcessWithCommand: 'tail -n 1'
input: ('Line 1{lf}Line 2{lf}{end}' format: formatDictionary)
writes: ('Line 1{crlf}Line 2{crlf}{echoedend}Line 2{crlf}' format: formatDictionary ,
(Dictionary with: 'echoedend' ->
(echoesControlCharacters ifTrue: [ '^D{bs}{bs}' format: formatDictionary ] ifFalse: [ '' ])))
hasStatus: 0.
]

{ #category : 'tests' }
LibTTYTest >> test4 [

self assertShellProcessWithCommand: 'printf FOO'
writes: 'FOO'
hasStatus: 0.
self assertShellProcessWithCommand: 'printf BAR 1>&2'
writes: 'BAR'
hasStatus: 0.
self assertShellProcessWithCommand: 'exit 123'
writes: ''
hasStatus: 123.
]

{ #category : 'tests' }
LibTTYTest >> test5 [

self assertShellProcessWithCommand: 'true'
hasStatus: 0.
self assertShellProcessWithCommand: '! true'
hasStatus: 1.
self assertShellProcessWithCommand: 'test -t 0'
hasStatus: 0.
self assertShellProcessWithCommand: 'test -t 0 </dev/zero'
hasStatus: 1.
self assertShellProcessWithCommand: 'test -t 1'
hasStatus: 0.
self assertShellProcessWithCommand: 'test -t 1 >/dev/null'
hasStatus: 1.
self assertShellProcessWithCommand: 'test -t 2'
hasStatus: 0.
self assertShellProcessWithCommand: 'test -t 2 2>/dev/null'
hasStatus: 1.
self assertShellProcessWithCommand: 'kill -s KILL $$'
hasStatus: 128 + 9.
]
57 changes: 54 additions & 3 deletions src/UnifiedFFI/LibC.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Under windows I provide similar functionality through the functions defined in m
Class {
#name : 'LibC',
#superclass : 'FFILibrary',
#pools : [
'LibCTypes'
],
#category : 'UnifiedFFI-Libraries',
#package : 'UnifiedFFI',
#tag : 'Libraries'
Expand Down Expand Up @@ -57,11 +60,17 @@ LibC class >> system: command [
^ self uniqueInstance system: command
]

{ #category : 'api - input/output' }
LibC >> close: fildes [

^ self ffiCall: #(int close(int fildes))
]

{ #category : 'api - processes' }
LibC >> currentProcessId [
"Returns the process identifier (PID) of the calling process."

^self ffiCall: #(int getpid(void))
^self ffiCall: #(pid_t getpid(void))
]

{ #category : 'api - accessing' }
Expand All @@ -75,7 +84,13 @@ LibC >> fgetc: stream [
LibC >> getpid [
"Get PID of current process."

^self ffiCall: #(int getpid(void))
^self ffiCall: #(pid_t getpid(void))
]

{ #category : 'api - pseudo-terminals' }
LibC >> grantpt: fildes [

^ self ffiCall: #(int grantpt(int fildes))
]

{ #category : 'private - accessing' }
Expand All @@ -89,11 +104,17 @@ LibC >> memCopy: src to: dest size: n [
ffiCall: #(#void #* #memcpy #(#void #* #dest #, #const #void #* #src #, #size_t #n))
]

{ #category : 'api - misc' }
LibC >> mkstemp: template [

^ self ffiCall: #(int mkstemp #(char* template))
]

{ #category : 'api - processes' }
LibC >> parentProcessId [
"Returns the process ID of the parent of the calling process."

^self ffiCall: #(int getppid(void))
^self ffiCall: #(pid_t getppid(void))
]

{ #category : 'api - piping' }
Expand All @@ -114,6 +135,18 @@ LibC >> pipeClose: stream [
ifFalse: [ #(int* pclose(void* stream)) ])
]

{ #category : 'api - pseudo-terminals' }
LibC >> posix_openpt: oflag [

^ self ffiCall: #(int posix_openpt(int oflag))
]

{ #category : 'api - input/output' }
LibC >> read: fildes buffer: buf size: nbyte [

^ self ffiCall: #(ssize_t read(int fildes, void* buf, size_t nbyte))
]

{ #category : 'process actions' }
LibC >> resultOfCommand: cmd [
|file last s |
Expand Down Expand Up @@ -142,8 +175,26 @@ LibC >> unixLibraryName [
^ 'libc.so.6'
]

{ #category : 'api - pseudo-terminals' }
LibC >> unlockpt: fildes [

^ self ffiCall: #(int unlockpt(int fildes))
]

{ #category : 'api - processes' }
LibC >> waitpid: pid status: stat_loc options: options [

^ self ffiCall: #(pid_t waitpid(pid_t pid, int* stat_loc, int options))
]

{ #category : 'private - accessing' }
LibC >> win32LibraryName [
"While this is not a 'libc' properly, msvcrt has the functions we are defining here"
^ 'msvcrt.dll'
]

{ #category : 'api - input/output' }
LibC >> write: fildes buffer: buf size: nbyte [

^ self ffiCall: #(ssize_t write(int fildes, const void* buf, size_t nbyte))
]
21 changes: 21 additions & 0 deletions src/UnifiedFFI/LibCTypes.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"
LibCTypes defines types used in `LibC`.
"
Class {
#name : 'LibCTypes',
#superclass : 'SharedPool',
#classVars : [
'pid_t',
'ssize_t'
],
#category : 'UnifiedFFI-Libraries',
#package : 'UnifiedFFI',
#tag : 'Libraries'
}

{ #category : 'class initialization' }
LibCTypes class >> initialize [

ssize_t := #long.
pid_t := #int.
]
Loading

0 comments on commit 8630bb8

Please sign in to comment.