Serenity Operating System
at master 183 lines 5.2 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "TerminalWrapper.h" 9#include <AK/DeprecatedString.h> 10#include <LibCore/System.h> 11#include <LibGUI/Application.h> 12#include <LibGUI/BoxLayout.h> 13#include <LibGUI/MessageBox.h> 14#include <LibVT/TerminalWidget.h> 15#include <fcntl.h> 16#include <signal.h> 17#include <stdio.h> 18#include <stdlib.h> 19#include <string.h> 20#include <sys/ioctl.h> 21#include <sys/types.h> 22#include <sys/wait.h> 23#include <unistd.h> 24 25namespace HackStudio { 26 27ErrorOr<void> TerminalWrapper::run_command(DeprecatedString const& command, Optional<DeprecatedString> working_directory, WaitForExit wait_for_exit, Optional<StringView> failure_message) 28{ 29 if (m_pid != -1) { 30 GUI::MessageBox::show(window(), 31 "A command is already running in this TerminalWrapper"sv, 32 "Can't run command"sv, 33 GUI::MessageBox::Type::Error); 34 return {}; 35 } 36 37 auto ptm_fd = TRY(setup_master_pseudoterminal()); 38 39 m_child_exited = false; 40 m_child_exit_status.clear(); 41 42 m_pid = TRY(Core::System::fork()); 43 44 if (m_pid > 0) { 45 if (wait_for_exit == WaitForExit::Yes) { 46 GUI::Application::the()->event_loop().spin_until([this]() { 47 return m_child_exited; 48 }); 49 50 VERIFY(m_child_exit_status.has_value()); 51 if (m_child_exit_status.value() != 0) 52 return Error::from_string_view(failure_message.value_or("Command execution failed"sv)); 53 } 54 55 return {}; 56 } 57 58 if (working_directory.has_value()) 59 TRY(Core::System::chdir(working_directory->view())); 60 61 TRY(setup_slave_pseudoterminal(ptm_fd)); 62 63 auto args = command.split_view(' '); 64 VERIFY(!args.is_empty()); 65 TRY(Core::System::exec(args[0], args, Core::System::SearchInPath::Yes)); 66 VERIFY_NOT_REACHED(); 67} 68 69ErrorOr<int> TerminalWrapper::setup_master_pseudoterminal(WaitForChildOnExit wait_for_child) 70{ 71 int ptm_fd = TRY(Core::System::posix_openpt(O_RDWR | O_CLOEXEC)); 72 bool error_happened = true; 73 74 ScopeGuard close_ptm { [&]() { 75 if (error_happened) { 76 if (auto result = Core::System::close(ptm_fd); result.is_error()) 77 warnln("{}", result.release_error()); 78 } 79 } }; 80 81 TRY(Core::System::grantpt(ptm_fd)); 82 TRY(Core::System::unlockpt(ptm_fd)); 83 84 m_terminal_widget->set_pty_master_fd(ptm_fd); 85 m_terminal_widget->on_command_exit = [this, wait_for_child] { 86 if (wait_for_child == WaitForChildOnExit::Yes) { 87 auto result = Core::System::waitpid(m_pid, 0); 88 if (result.is_error()) { 89 warnln("{}", result.error()); 90 VERIFY_NOT_REACHED(); 91 } 92 int wstatus = result.release_value().status; 93 94 if (WIFEXITED(wstatus)) { 95 m_terminal_widget->inject_string(DeprecatedString::formatted("\033[{};1m(Command exited with code {})\033[0m\r\n", wstatus == 0 ? 32 : 31, WEXITSTATUS(wstatus))); 96 } else if (WIFSTOPPED(wstatus)) { 97 m_terminal_widget->inject_string("\033[34;1m(Command stopped!)\033[0m\r\n"sv); 98 } else if (WIFSIGNALED(wstatus)) { 99 m_terminal_widget->inject_string(DeprecatedString::formatted("\033[34;1m(Command signaled with {}!)\033[0m\r\n", strsignal(WTERMSIG(wstatus)))); 100 } 101 102 m_child_exit_status = WEXITSTATUS(wstatus); 103 m_child_exited = true; 104 } 105 m_pid = -1; 106 107 if (on_command_exit) 108 on_command_exit(); 109 }; 110 111 terminal().scroll_to_bottom(); 112 113 error_happened = false; 114 115 return ptm_fd; 116} 117 118ErrorOr<void> TerminalWrapper::setup_slave_pseudoterminal(int master_fd) 119{ 120 setsid(); 121 122 auto tty_name = TRY(Core::System::ptsname(master_fd)); 123 124 close(master_fd); 125 126 int pts_fd = TRY(Core::System::open(tty_name, O_RDWR)); 127 128 tcsetpgrp(pts_fd, getpid()); 129 130 // NOTE: It's okay if this fails. 131 ioctl(0, TIOCNOTTY); 132 133 close(0); 134 close(1); 135 close(2); 136 137 TRY(Core::System::dup2(pts_fd, 0)); 138 TRY(Core::System::dup2(pts_fd, 1)); 139 TRY(Core::System::dup2(pts_fd, 2)); 140 141 TRY(Core::System::close(pts_fd)); 142 143 TRY(Core::System::ioctl(0, TIOCSCTTY)); 144 145 setenv("TERM", "xterm", true); 146 147 return {}; 148} 149 150ErrorOr<void> TerminalWrapper::kill_running_command() 151{ 152 VERIFY(m_pid != -1); 153 154 // Kill our child process and its whole process group. 155 TRY(Core::System::killpg(m_pid, SIGTERM)); 156 return {}; 157} 158 159void TerminalWrapper::clear_including_history() 160{ 161 m_terminal_widget->clear_including_history(); 162} 163 164TerminalWrapper::TerminalWrapper(bool user_spawned) 165 : m_user_spawned(user_spawned) 166{ 167 set_layout<GUI::VerticalBoxLayout>(); 168 169 m_terminal_widget = add<VT::TerminalWidget>(-1, false); 170 if (user_spawned) { 171 auto maybe_error = run_command("Shell"); 172 if (maybe_error.is_error()) 173 warnln("{}", maybe_error.release_error()); 174 } 175} 176 177int TerminalWrapper::child_exit_status() const 178{ 179 VERIFY(m_child_exit_status.has_value()); 180 return m_child_exit_status.value(); 181} 182 183}