-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[stdlib] Adds functionality to spawn and manage processes from exec. file #3998
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
Open
izo0x90
wants to merge
17
commits into
modular:main
Choose a base branch
from
izo0x90:Hristo/Add-basic-process-exec-facilities
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
7ece193
Update file_descriptor `read_bytes` internals
izo0x90 446d8a9
Update `file_descriptor.read_bytes` do not alloc.
izo0x90 ae307a9
Adds functionality to spawn and manage processes from exec. file
izo0x90 6c1c302
Fixes/ updates to `process` functionality
izo0x90 2bf983b
Use interprocess pipe to signal exec error in forked process
izo0x90 63d5dc0
Update `process.Pipe` to take in data buffer
izo0x90 08acf1c
Merge branch 'main' of github-personal:izo0x90/mojo into Hristo/Add-b…
izo0x90 2d96bb9
Update code to work with current mojo syntax and conventions
izo0x90 d78ccb6
Merge branch 'main' of github-personal:izo0x90/mojo into Hristo/Add-b…
izo0x90 b4d0c02
Merge branch 'main' into Hristo/Add-basic-process-exec-facilities
izo0x90 1610ce3
Fixes for exec error checking
izo0x90 48f65d7
Merge branch 'Hristo/Add-basic-process-exec-facilities' of github-per…
izo0x90 bb0b684
Merge branch 'main' of github-personal:izo0x90/mojo into Hristo/Add-b…
izo0x90 2491143
Formatting fixes and remove unneeded change in _libc
izo0x90 cf9015e
Merge branch 'main' into Hristo/Add-basic-process-exec-facilities
izo0x90 5a55814
Merge branch 'main' of github-personal:izo0x90/mojo into Hristo/Add-b…
izo0x90 e3d181a
First steps to using posix_spawnp
izo0x90 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,3 +44,4 @@ from .os import ( | |
| unlink, | ||
| ) | ||
| from .pathlike import PathLike | ||
| from .process import Process, Pipe | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,377 @@ | ||
| # ===----------------------------------------------------------------------=== # | ||
| # Copyright (c) 2025, Modular Inc. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License v2.0 with LLVM Exceptions: | ||
| # https://llvm.org/LICENSE.txt | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # ===----------------------------------------------------------------------=== # | ||
| """Implements os methods for dealing with processes. | ||
|
|
||
| Example: | ||
|
|
||
| ```mojo | ||
| from os import Process | ||
| ``` | ||
| """ | ||
| from collections import List, Optional | ||
| from collections.string import StringSlice | ||
|
|
||
| from memory import LegacyUnsafePointer | ||
|
|
||
| from sys import CompilationTarget | ||
| from sys._libc import ( | ||
| waitpid, | ||
| vfork, | ||
| execvp, | ||
| posix_spawnp, | ||
| exit, | ||
| kill, | ||
| SignalCodes, | ||
| pipe, | ||
| fcntl, | ||
| FcntlCommands, | ||
| FcntlFDFlags, | ||
| close, | ||
| ) | ||
| from sys.ffi import c_char, c_int | ||
| from sys.os import sep | ||
|
|
||
|
|
||
| # ===----------------------------------------------------------------------=== # | ||
| # Process comm. | ||
| # ===----------------------------------------------------------------------=== # | ||
| struct Pipe: | ||
| """Create a pipe for interprocess communication. | ||
|
|
||
| Example usage: | ||
| ``` | ||
| pipe().write_bytes("TEST".as_bytes()) | ||
| ``` | ||
| """ | ||
|
|
||
| var fd_in: Optional[FileDescriptor] | ||
| """File descriptor for pipe input.""" | ||
| var fd_out: Optional[FileDescriptor] | ||
| """File descriptor for pipe output.""" | ||
|
|
||
| fn __init__( | ||
| out self, | ||
| in_close_on_exec: Bool = False, | ||
| out_close_on_exec: Bool = False, | ||
| ) raises: | ||
| """Struct to manage interprocess pipe comms. | ||
|
|
||
| Args: | ||
| in_close_on_exec: Close the read side of pipe if `exec` sys. call is issued in process. | ||
| out_close_on_exec: Close the write side of pipe if `exec` sys. call is issued in process. | ||
| """ | ||
| var pipe_fds = alloc[c_int](2) | ||
| if pipe(pipe_fds) < 0: | ||
| pipe_fds.free() | ||
| raise Error("Failed to create pipe") | ||
|
|
||
| if in_close_on_exec: | ||
| if not self._set_close_on_exec(pipe_fds[0]): | ||
| pipe_fds.free() | ||
| raise Error("Failed to configure input pipe close on exec") | ||
|
|
||
| if out_close_on_exec: | ||
| if not self._set_close_on_exec(pipe_fds[1]): | ||
| pipe_fds.free() | ||
| raise Error("Failed to configure output pipe close on exec") | ||
|
|
||
| self.fd_in = FileDescriptor(Int(pipe_fds[0])) | ||
| self.fd_out = FileDescriptor(Int(pipe_fds[1])) | ||
| pipe_fds.free() | ||
|
|
||
| fn __del__(deinit self): | ||
| """Ensures pipes input and output file descriptors are closed, when the object is destroyed. | ||
| """ | ||
| self.set_input_only() | ||
| self.set_output_only() | ||
|
|
||
| @staticmethod | ||
| fn _set_close_on_exec(fd: c_int) -> Bool: | ||
| return ( | ||
| fcntl( | ||
| fd, | ||
| FcntlCommands.F_SETFD, | ||
| fcntl(fd, FcntlCommands.F_GETFD, 0) | FcntlFDFlags.FD_CLOEXEC, | ||
| ) | ||
| == 0 | ||
| ) | ||
|
|
||
| @always_inline | ||
| fn set_input_only(mut self): | ||
| """Close the output descriptor/ channel for this side of the pipe.""" | ||
| if self.fd_out: | ||
| _ = close(rebind[Int](self.fd_out.value())) | ||
| self.fd_out = None | ||
|
|
||
| @always_inline | ||
| fn set_output_only(mut self): | ||
| """Close the input descriptor/ channel for this side of the pipe.""" | ||
| if self.fd_in: | ||
| _ = close(rebind[Int](self.fd_in.value())) | ||
| self.fd_in = None | ||
|
|
||
| @always_inline | ||
| fn write_bytes(mut self, bytes: Span[Byte, _]) raises: | ||
| """ | ||
| Write a span of bytes to the pipe. | ||
|
|
||
| Args: | ||
| bytes: The byte span to write to this pipe. | ||
|
|
||
| """ | ||
| if self.fd_out: | ||
| self.fd_out.value().write_bytes(bytes) | ||
| else: | ||
| raise Error("Can not write from read only side of pipe") | ||
|
|
||
| @always_inline | ||
| fn read_bytes(mut self, mut buffer: Span[mut=True, Byte]) raises -> UInt: | ||
| """ | ||
| Read a number of bytes from this pipe. | ||
|
|
||
| Args: | ||
| buffer: Span[Byte] of length n where to store read bytes. n = number of bytes to read. | ||
|
|
||
| Returns: | ||
| Actual number of bytes read. | ||
| """ | ||
| if self.fd_in: | ||
| return self.fd_in.value().read_bytes(buffer) | ||
|
|
||
| raise Error("Can not read from write only side of pipe") | ||
|
|
||
|
|
||
| # ===----------------------------------------------------------------------=== # | ||
| # Process execution | ||
| # ===----------------------------------------------------------------------=== # | ||
|
|
||
| alias ERR_STR_LEN = 8 | ||
|
|
||
|
|
||
| struct Process: | ||
| """Create and manage child processes from file executables. | ||
|
|
||
| Example usage: | ||
| ``` | ||
| child_process = Process.run("ls", List[String]("-lha")) | ||
| if child_process.interrupt(): | ||
| print("Successfully interrupted.") | ||
| ``` | ||
| """ | ||
|
|
||
| var child_pid: Int | ||
| """Child process id.""" | ||
|
|
||
| fn __init__(out self, child_pid: Int): | ||
| """Struct to manage metadata about child process. | ||
| Use the `run` static method to create new process. | ||
|
|
||
| Args: | ||
| child_pid: The pid of child processed returned by `vfork` that the struct will manage. | ||
| """ | ||
|
|
||
| self.child_pid = child_pid | ||
|
|
||
| fn __del__(deinit self): | ||
| """ """ | ||
| print("IN DEINIT") | ||
| if not self.wait_process_status(): | ||
| print("ERROR when waiting on subprocess") | ||
|
|
||
| fn _kill(self, signal: Int) -> Bool: | ||
| # `kill` returns 0 on success and -1 on failure | ||
| return kill(self.child_pid, signal) > -1 | ||
|
|
||
| fn hangup(self) -> Bool: | ||
| """Send the Hang up signal to the managed child process. | ||
|
|
||
| Returns: | ||
| Upon successful completion, True is returned else False. | ||
| """ | ||
| return self._kill(SignalCodes.HUP) | ||
|
|
||
| fn interrupt(self) -> Bool: | ||
| """Send the Interrupt signal to the managed child process. | ||
|
|
||
| Returns: | ||
| Upon successful completion, True is returned else False. | ||
| """ | ||
| return self._kill(SignalCodes.INT) | ||
|
|
||
| fn kill(self) -> Bool: | ||
| """Send the Kill signal to the managed child process. | ||
|
|
||
| Returns: | ||
| Upon successful completion, True is returned else False. | ||
| """ | ||
| return self._kill(SignalCodes.KILL) | ||
|
|
||
| fn wait_process_status(self) -> Bool: | ||
| """Wait on stuff. | ||
|
|
||
| Returns: | ||
| Upon successful completion, True is returned else False. | ||
| """ | ||
| print("Start wait...") | ||
| var status: c_int = 0 | ||
| var chk_pid = waitpid(self.child_pid, UnsafePointer(to=status), 0) | ||
| print("Done wait ...", chk_pid, "==?", self.child_pid) | ||
| return self.child_pid == chk_pid | ||
|
|
||
| @staticmethod | ||
| fn run(var path: String, argv: List[String]) raises -> Process: | ||
| """Spawn new process from file executable. | ||
|
|
||
| Args: | ||
| path: The path to the file. | ||
| argv: A list of string arguments to be passed to executable. | ||
|
|
||
| Returns: | ||
| An instance of `Process` struct. | ||
| """ | ||
|
|
||
| print("Called run") | ||
|
|
||
| @parameter | ||
| if CompilationTarget.is_linux() or CompilationTarget.is_macos(): | ||
| var file_name = String(path.split(sep)[-1]) | ||
|
|
||
| var arg_count = len(argv) | ||
| var argv_array_ptr_cstr_ptr = LegacyUnsafePointer[ | ||
| LegacyUnsafePointer[c_char, mut=False] | ||
| ].alloc(arg_count + 2) | ||
| var offset = 0 | ||
| # Arg 0 in `argv` ptr array should be the file name | ||
| argv_array_ptr_cstr_ptr[offset] = file_name.unsafe_cstr_ptr() | ||
| offset += 1 | ||
|
|
||
| for var arg in argv: | ||
| argv_array_ptr_cstr_ptr[offset] = arg.unsafe_cstr_ptr() | ||
| offset += 1 | ||
|
|
||
| # `argv` ptr array terminates with NULL PTR | ||
| argv_array_ptr_cstr_ptr[offset] = LegacyUnsafePointer[c_char]() | ||
|
|
||
| var path_cptr = path.unsafe_cstr_ptr() | ||
|
|
||
| var pid: Int = 0 | ||
|
|
||
| print("Before s") | ||
|
|
||
| var has_error_code = posix_spawnp( | ||
| UnsafePointer(to=pid), | ||
| path_cptr, | ||
| argv_array_ptr_cstr_ptr, | ||
| LegacyUnsafePointer[LegacyUnsafePointer[Int8, mut=False]](), | ||
| ) | ||
| print(has_error_code) | ||
|
|
||
| print("After s") | ||
|
|
||
| if has_error_code > 0: | ||
| raise Error( | ||
| "Failed to execute " | ||
| + path | ||
| + ", EINT error code: " | ||
| + String(has_error_code) | ||
| ) | ||
|
|
||
| return Process(child_pid=pid) | ||
| else: | ||
| constrained[ | ||
| False, "Unknown platform process execution not implemented" | ||
| ]() | ||
| return abort[Process]() | ||
|
|
||
| # @staticmethod | ||
| # fn run_e(var path: String, argv: List[String]) raises -> Process: | ||
| # """Spawn new process from file executable. | ||
| # | ||
| # Args: | ||
| # path: The path to the file. | ||
| # argv: A list of string arguments to be passed to executable. | ||
| # | ||
| # Returns: | ||
| # An instance of `Process` struct. | ||
| # """ | ||
| # | ||
| # @parameter | ||
| # if CompilationTarget.is_linux() or CompilationTarget.is_macos(): | ||
| # var file_name = String(path.split(sep)[-1]) | ||
| # var pipe = Pipe(out_close_on_exec=True) | ||
| # var exec_err_code = StaticString("EXEC_ERR") | ||
| # | ||
| # var pid = vfork() | ||
| # | ||
| # if pid == 0: | ||
| # # Child process. | ||
| # pipe.set_output_only() | ||
| # | ||
| # var arg_count = len(argv) | ||
| # var argv_array_ptr_cstr_ptr = LegacyUnsafePointer[ | ||
| # LegacyUnsafePointer[c_char, mut=False] | ||
| # ].alloc(arg_count + 2) | ||
| # var offset = 0 | ||
| # # Arg 0 in `argv` ptr array should be the file name | ||
| # argv_array_ptr_cstr_ptr[offset] = file_name.unsafe_cstr_ptr() | ||
| # offset += 1 | ||
| # | ||
| # for var arg in argv: | ||
| # argv_array_ptr_cstr_ptr[offset] = arg.unsafe_cstr_ptr() | ||
| # offset += 1 | ||
| # | ||
| # # `argv` ptr array terminates with NULL PTR | ||
| # argv_array_ptr_cstr_ptr[offset] = LegacyUnsafePointer[c_char]() | ||
| # | ||
| # var path_cptr = path.unsafe_cstr_ptr() | ||
| # | ||
| # _ = execvp(path_cptr, argv_array_ptr_cstr_ptr) | ||
| # | ||
| # # This will only get reached if exec call fails to replace currently executing code | ||
| # argv_array_ptr_cstr_ptr.free() | ||
| # | ||
| # # Canonical fork/ exec error handling pattern of using a pipe that closes on exec is | ||
| # # used to signal error to parent process `https://cr.yp.to/docs/selfpipe.html` | ||
| # pipe.write_bytes(exec_err_code.as_bytes()) | ||
| # | ||
| # exit(1) | ||
| # | ||
| # elif pid < 0: | ||
| # raise Error("Unable to fork parent") | ||
| # | ||
| # var err: Optional[StringSlice[MutAnyOrigin]] = None | ||
| # var err_buff_data = InlineArray[Byte, ERR_STR_LEN](fill=0) | ||
| # | ||
| # try: | ||
| # pipe.set_input_only() | ||
| # var buf = Span[Byte, MutAnyOrigin]( | ||
| # ptr=err_buff_data.unsafe_ptr(), length=ERR_STR_LEN | ||
| # ) | ||
| # var bytes_read = pipe.read_bytes(buf) | ||
| # err = StringSlice(unsafe_from_utf8=buf) | ||
| # except e: | ||
| # raise Error( | ||
| # "Failed to read child process response from pipe, exception" | ||
| # " was: " | ||
| # + String(e) | ||
| # ) | ||
| # | ||
| # if err and len(err.value()) > 0 and err.value() == exec_err_code: | ||
| # raise Error("Failed to execute " + path) | ||
| # | ||
| # return Process(child_pid=pid) | ||
| # else: | ||
| # constrained[ | ||
| # False, "Unknown platform process execution not implemented" | ||
| # ]() | ||
| # return abort[Process]() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the default should be true.