Skip to content
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

Add LibTTY providing access to the VM library for spawning a process connected to a pseudo-terminal #16427

Merged
merged 4 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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