Skip to content

Commit

Permalink
Adds functionality to spawn and manage processes from exec. file
Browse files Browse the repository at this point in the history
- Adds fork, execvp, kill system call utils. to Mojos cLib binds

- Adds utility struct. to allow spawning and managing child processes
  from an executable file
  - `run` factory method that creates process and returns "management"
    object
  - Instance methods to send signals to child process, INT, HUP, KIL

Signed-off-by: Hristo I. Gueorguiev <[email protected]>
  • Loading branch information
izo0x90 committed Feb 15, 2025
1 parent fee4887 commit acbed32
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 3 deletions.
1 change: 1 addition & 0 deletions stdlib/src/os/__init__.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ from .os import (
SEEK_CUR,
SEEK_END,
SEEK_SET,
Process,
abort,
getuid,
listdir,
Expand Down
92 changes: 90 additions & 2 deletions stdlib/src/os/os.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ from os import listdir

from collections import InlineArray, List
from collections.string import StringSlice
from sys import external_call, is_gpu, os_is_linux, os_is_windows
from sys.ffi import OpaquePointer, c_char
from sys import (
external_call,
is_gpu,
os_is_linux,
os_is_macos,
os_is_windows,
)
from sys._libc import fork, execvp, kill, SignalCodes
from sys.ffi import OpaquePointer, c_char, c_int, c_str_ptr

from memory import UnsafePointer

Expand Down Expand Up @@ -415,3 +422,84 @@ def removedirs[PathLike: os.PathLike](path: PathLike) -> None:
except:
break
head, tail = os.path.split(head)


# ===----------------------------------------------------------------------=== #
# Process execution
# ===----------------------------------------------------------------------=== #


struct Process:
"""Create and manage child processes from file executables.
TODO: Add windows support.
"""

var child_pid: c_int

fn __init__(mut self, child_pid: c_int):
self.child_pid = child_pid

fn _kill(self, signal: Int):
kill(self.child_pid, signal)

fn hangup(self):
self._kill(SignalCodes.HUP)

fn interrupt(self):
self._kill(SignalCodes.INT)

fn kill(self):
self._kill(SignalCodes.KILL)

@staticmethod
fn run(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 os_is_linux() or os_is_macos():
var file_name = path.split(sep)[-1]
var pid = fork()
if pid == 0:
var arg_count = len(argv)
var argv_array_ptr_cstr_ptr = UnsafePointer[c_str_ptr].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 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] = c_str_ptr()

_ = execvp(path.unsafe_cstr_ptr(), 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()
raise Error("Failed to execute " + path)
elif pid < 0:
raise Error("Unable to fork parent")

return Process(child_pid=pid)
elif os_is_windows():
constrained[
False, "Windows process execution currently not implemented"
]()
return abort[Process]()
else:
constrained[
False, "Unknown platform process execution not implemented"
]()
return abort[Process]()
29 changes: 28 additions & 1 deletion stdlib/src/sys/_libc.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ functionality in the rest of the Mojo standard library.
"""

from sys import os_is_windows
from sys.ffi import OpaquePointer, c_char, c_int, c_size_t
from sys.ffi import OpaquePointer, c_char, c_int, c_size_t, c_str_ptr

from memory import UnsafePointer

Expand Down Expand Up @@ -106,6 +106,33 @@ fn dup(oldfd: c_int) -> c_int:
return external_call[name, c_int](oldfd)


@always_inline
fn execvp(file: UnsafePointer[c_char], argv: UnsafePointer[c_str_ptr]) -> c_int:
"""`execvp` expects that the c_str_ptr array is terminated with a NULL pointer.
"""
return external_call["execvp", c_int](file, argv)


@always_inline
fn fork() -> c_int:
return external_call["fork", c_int]()


struct SignalCodes:
alias HUP = 1 # (hang up)
alias INT = 2 # (interrupt)
alias QUIT = 3 # (quit)
alias ABRT = 6 # (abort)
alias KILL = 9 # (non-catchable, non-ignorable kill)
alias ALRM = 14 # (alarm clock)
alias TERM = 15 # (software termination signal)


@always_inline
fn kill(pid: c_int, sig: c_int):
external_call["kill", NoneType](pid, sig)


# ===-----------------------------------------------------------------------===#
# dlfcn.h — dynamic library operations
# ===-----------------------------------------------------------------------===#
Expand Down
3 changes: 3 additions & 0 deletions stdlib/src/sys/ffi.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ alias c_float = Float32
alias c_double = Float64
"""C `double` type."""

alias c_str_ptr = UnsafePointer[Int8]
"""C `*char` type"""

alias OpaquePointer = UnsafePointer[NoneType]
"""An opaque pointer, equivalent to the C `void*` type."""

Expand Down
45 changes: 45 additions & 0 deletions stdlib/test/os/test_process.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ===----------------------------------------------------------------------=== #
# 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.
# ===----------------------------------------------------------------------=== #
# REQUIRES: system-linux || system-darwin
# RUN: %mojo %s | FileCheck %s

from collections import List
from os.path import exists
from os import Process

from testing import assert_false, assert_raises


# CHECK-LABEL: TEST
def test_process_run():
_ = Process.run("echo", List[String]("== TEST"))


def test_process_run_missing():
missing_executable_file = "ThIsFiLeCoUlDNoTPoSsIbLlYExIsT.NoTAnExTeNsIoN"

# verify that the test file does not exist before starting the test
assert_false(
exists(missing_executable_file),
"Unexpected file '" + missing_executable_file + "' it should not exist",
)

# Forking appears to break asserts
with assert_raises():
_ = Process.run(missing_executable_file, List[String]())


def main():
test_process_run()
# TODO: How can exception being raised on missing file be asserted
# test_process_run_missing()

0 comments on commit acbed32

Please sign in to comment.