From b02dc28a9e1b9512d6ad29dd94ff8be06056122d Mon Sep 17 00:00:00 2001 From: KingToolbox Date: Tue, 22 Dec 2020 16:48:48 +0800 Subject: [PATCH] Add Unix pty, which supports Linux/MacOs bash and so on. --- src/Pty/UnixPty.cpp | 328 ++++++++++++++++++++++++++++++++++++++++++++ src/Pty/UnixPty.h | 83 +++++++++++ 2 files changed, 411 insertions(+) create mode 100644 src/Pty/UnixPty.cpp create mode 100644 src/Pty/UnixPty.h diff --git a/src/Pty/UnixPty.cpp b/src/Pty/UnixPty.cpp new file mode 100644 index 0000000..051da86 --- /dev/null +++ b/src/Pty/UnixPty.cpp @@ -0,0 +1,328 @@ + /* + * Copyright 2020, WindTerm. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +#include "UnixPty.h" +#ifdef Q_OS_UNIX + +#include +#include + +#include +#include +#include +#include +#include +#include + +constexpr int BUFFER_SIZE = 0x800; // 2048 bytes + +ShellProcess::ShellProcess() + : QProcess() + , m_handleMaster(-1) + , m_handleSlave(-1) +{ + setProcessChannelMode(QProcess::MergedChannels); +} + +void ShellProcess::setupChildProcess() { + dup2(m_handleSlave, STDIN_FILENO); + dup2(m_handleSlave, STDOUT_FILENO); + dup2(m_handleSlave, STDERR_FILENO); + + pid_t sid = setsid(); + ioctl(m_handleSlave, TIOCSCTTY, 0); + tcsetpgrp(m_handleSlave, sid); + + struct utmpx utmpxInfo; + memset(&utmpxInfo, 0, sizeof(utmpxInfo)); + + strncpy(utmpxInfo.ut_user, qgetenv("USER"), sizeof(utmpxInfo.ut_user)); + + QString device(m_handleSlaveName); + if (device.startsWith("/dev/")) { + device = device.mid(5); + } + const char *d = device.toLatin1().constData(); + strncpy(utmpxInfo.ut_line, d, sizeof(utmpxInfo.ut_line)); + strncpy(utmpxInfo.ut_id, d + strlen(d) - sizeof(utmpxInfo.ut_id), sizeof(utmpxInfo.ut_id)); + + struct timeval tv; + gettimeofday(&tv, 0); + utmpxInfo.ut_tv.tv_sec = tv.tv_sec; + utmpxInfo.ut_tv.tv_usec = tv.tv_usec; + + utmpxInfo.ut_type = USER_PROCESS; + utmpxInfo.ut_pid = getpid(); + + utmpxname(_PATH_UTMPX); + setutxent(); + pututxline(&utmpxInfo); + endutxent(); + +#if !defined(Q_OS_UNIX) + updwtmpx(_PATH_UTMPX, &loginInfo); +#endif +} + +UnixPty::UnixPty(QObject *parent /*= nullptr*/) + : Pty(parent) + , m_pid(0) + , m_readMasterNotify(nullptr) +{ + m_buffer = static_cast(malloc(BUFFER_SIZE)); +} + +UnixPty::~UnixPty() { + stop(); + + free(m_buffer); + m_buffer = nullptr; +} + +bool UnixPty::createProcess(QString command, const QString &arguments, + const QString &workingDirectory, const QProcessEnvironment &environment, + int rows, int columns) { + if (m_shellProcess.state() == QProcess::Running) { + return false; + } + + QFileInfo fileInfo(command); + if (fileInfo.isRelative() || fileInfo.exists() == false) { + setErrorString(tr("UnixPty: shell file path must be absolute")); + return false; + } + + bool success = false; + do { + int result = 0; + + m_shellProcess.m_handleMaster = ::posix_openpt(O_RDWR | O_NOCTTY); + if (m_shellProcess.m_handleMaster < 0) { + setError("Unable to open master"); + break; + } + + m_shellProcess.m_handleSlaveName = ptsname(m_shellProcess.m_handleMaster); + if (m_shellProcess.m_handleSlaveName.isEmpty()) { + setError("Unable to get slave name"); + break; + } + + result = grantpt(m_shellProcess.m_handleMaster); + if (result != 0) { + setError("Unable to change perms for slave"); + break; + } + + result = unlockpt(m_shellProcess.m_handleMaster); + if (result != 0) { + setError("Unable to unlock slave"); + break; + } + + m_shellProcess.m_handleSlave = ::open(m_shellProcess.m_handleSlaveName.toLatin1().data(), O_RDWR | O_NOCTTY); + if (m_shellProcess.m_handleSlave < 0) { + setError("Unable to open slave"); + break; + } + + int flags = fcntl(m_shellProcess.m_handleMaster, F_GETFL, 0); + result = fcntl(m_shellProcess.m_handleMaster, F_SETFL, flags | O_NONBLOCK); + if (result == -1) { + setError("Unable to set non-blocking mode for master"); + break; + } + + result = fcntl(m_shellProcess.m_handleMaster, F_SETFD, FD_CLOEXEC); + if (result == -1) { + setError("Unable to set flags for master"); + break; + } + + result = fcntl(m_shellProcess.m_handleSlave, F_SETFD, FD_CLOEXEC); + if (result == -1) { + setError("Unable to set flags for slave"); + break; + } + + struct ::termios ttmode; + if (terminalAttributes(&ttmode) == false) { + break; + } + ttmode.c_iflag |= IXON; + ttmode.c_iflag |= IUTF8; + ttmode.c_lflag |= ECHO; + +#ifdef Q_OS_MACOS + ttmode.c_cc[VDSUSP] = 25; + ttmode.c_cc[VSTATUS] = 20; +#endif + if (setTerminalAttributes(&ttmode) == false) { + break; + } + + m_readMasterNotify = new QSocketNotifier(m_shellProcess.m_handleMaster, QSocketNotifier::Read, &m_shellProcess); + m_readMasterNotify->setEnabled(true); + m_readMasterNotify->moveToThread(m_shellProcess.thread()); + + QObject::connect(m_readMasterNotify, &QSocketNotifier::activated, [this](int socket) { + Q_UNUSED(socket) + int readSize = 0; + QByteArray readData; + + do { + readSize = ::read(m_shellProcess.m_handleMaster, m_buffer, BUFFER_SIZE); + + if (readSize == -1) { + if (errno == EAGAIN) { + QThread::yieldCurrentThread(); + continue; + } else { + break; + } + } + readData.append(QByteArray::fromRawData(m_buffer, readSize)); + } while (readSize == BUFFER_SIZE || (readSize == -1 && errno == EAGAIN)); //last data block always < readSize + + if (readData.isEmpty() == false) { + m_shellReadBuffer.append(readData); + emit readyRead(); + } + }); + + m_shellProcess.setWorkingDirectory(workingDirectory); + m_shellProcess.setProcessEnvironment(environment); + m_shellProcess.setReadChannel(QProcess::StandardOutput); + m_shellProcess.start(command, arguments.isEmpty() ? QStringList() : QStringList(arguments)); + m_shellProcess.waitForStarted(); + + m_pid = m_shellProcess.processId(); + success = resizeWindow(rows, columns); + } while(0); + + if (success == false) { + stop(); + } + Q_ASSERT(success == true); + return success; +} + +bool UnixPty::resizeWindow(int rows, int columns) { + if (rows != m_rows || columns != m_columns) { + bool success = false; + struct winsize winSize; + + memset(&winSize, 0, sizeof(winSize)); + winSize.ws_row = static_cast(rows); + winSize.ws_col = static_cast(columns); + + if (ioctl(m_shellProcess.m_handleMaster, TIOCSWINSZ, &winSize) == 0 + && ioctl(m_shellProcess.m_handleSlave, TIOCSWINSZ, &winSize) == 0) { + m_rows = rows; + m_columns = columns; + success = true; + } + return success; + } + return true; +} + +void UnixPty::setError(const char *error) { + Pty::setErrorString(QString("%1: %2 -> %3").arg(tr("UnixPty"), tr(error), strerror(errno))); +} + +bool UnixPty::setTerminalAttributes(struct ::termios *ttmode) { + Q_ASSERT(m_shellProcess.m_handleMaster >= 0); + int result = -1; + + if (m_shellProcess.m_handleMaster >=0) { + result = tcsetattr(m_shellProcess.m_handleMaster, TCSANOW, ttmode); + + if (result != 0) { + setError("Unable to set terminal attributes"); + } + } + return (result == 0); +} + +void UnixPty::stop() { + m_shellProcess.m_handleSlaveName = QString(); + + if (m_shellProcess.m_handleSlave >= 0) { + ::close(m_shellProcess.m_handleSlave); + m_shellProcess.m_handleSlave = -1; + } + + if (m_shellProcess.m_handleMaster >= 0) { + ::close(m_shellProcess.m_handleMaster); + m_shellProcess.m_handleMaster = -1; + } + + if (m_shellProcess.state() == QProcess::Running) { + m_readMasterNotify->disconnect(); + m_readMasterNotify->deleteLater(); + + m_shellProcess.terminate(); + m_shellProcess.waitForFinished(1000); + + if (m_shellProcess.state() == QProcess::Running) { + QProcess::startDetached(QString("kill -9 %1").arg(m_pid)); + m_shellProcess.kill(); + m_shellProcess.waitForFinished(1000); + } + Q_ASSERT(m_shellProcess.state() == QProcess::NotRunning); + } +} + +bool UnixPty::terminalAttributes(struct ::termios *ttmode) { + Q_ASSERT(m_shellProcess.m_handleMaster >= 0); + int result = -1; + + if (m_shellProcess.m_handleMaster >= 0) { + result = tcgetattr(m_shellProcess.m_handleMaster, ttmode); + + if (result != 0) { + setError("Unable to get terminal attributes"); + } + } + return (result == 0); +} + +QByteArray UnixPty::readAll() { + QByteArray shellReadBuffer = m_shellReadBuffer; + + m_shellReadBuffer.clear(); + return shellReadBuffer; +} + +qint64 UnixPty::write(const QByteArray &text) { + int writeSize = ::write(m_shellProcess.m_handleMaster, text.constData(), text.size()); + Q_ASSERT(writeSize == text.size()); + + if (writeSize == -1) { + setError("Unable to write output"); + } + return writeSize; +} + +bool UnixPty::isAvailable() { + return true; +} + +void UnixPty::moveToThread(QThread *targetThread) { + m_shellProcess.moveToThread(targetThread); +} + +#endif // Q_OS_UNIX \ No newline at end of file diff --git a/src/Pty/UnixPty.h b/src/Pty/UnixPty.h new file mode 100644 index 0000000..77fabbc --- /dev/null +++ b/src/Pty/UnixPty.h @@ -0,0 +1,83 @@ + /* + * Copyright 2020, WindTerm. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +#ifndef UNIXPTY_H +#define UNIXPTY_H + +#pragma once + +#include + +#ifdef Q_OS_UNIX + +#include "Pty.h" +#include +#include + +#include + +class ShellProcess + : public QProcess +{ + Q_OBJECT + +public: + ShellProcess(); + +private: + void setupChildProcess() final; + +private: + int m_handleMaster; + int m_handleSlave; + QString m_handleSlaveName; + +private: + friend class UnixPty; +}; + +class UnixPty + : public Pty +{ +public: + UnixPty(QObject *parent = nullptr); + virtual ~UnixPty(); + + bool createProcess(QString command, const QString &arguments, + const QString &workingDirectory, const QProcessEnvironment &environment, + int rows, int columns) final; + static bool isAvailable(); + bool resizeWindow(int rows, int columns) final; + QByteArray readAll() final; + qint64 write(const QByteArray &text) final; + +private: + void moveToThread(QThread *targetThread); + void setError(const char *error); + bool setTerminalAttributes(struct ::termios *ttmode); + void stop(); + bool terminalAttributes(struct ::termios *ttmode); + +private: + qint64 m_pid; + ShellProcess m_shellProcess; + QSocketNotifier *m_readMasterNotify; + + char *m_buffer; + QByteArray m_shellReadBuffer; +}; + +#endif // Q_OS_UNIX +#endif // UNIXPTY_H