// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "externalterminalprocessimpl.h" #include "algorithm.h" #include "qtcprocess.h" #include "terminalcommand.h" #include "utilstr.h" #include #include #include Q_LOGGING_CATEGORY(logTE, "terminal.externalprocess", QtWarningMsg) namespace Utils { ExternalTerminalProcessImpl::ExternalTerminalProcessImpl() { setStubCreator(new ProcessStubCreator(this)); } QString ExternalTerminalProcessImpl::openTerminalScriptAttached() { static const QLatin1String script{R"( tell application "Terminal" activate set windowId to 0 set newTab to do script "%1 && exit" -- Try to get window id try -- We have seen this work on macOS 13, and 12.5.1, but not on 14.0 or 14.1 set windowId to (the id of window 1 where its tab 1 = newTab) as text on error eMsg number eNum -- If we get an error we try to generate a known error that will contain the window id in its message try set windowId to window of newTab on error eMsg number eNum if eNum = -1728 then try -- Search for "window id " in the error message, examples of error messages are: -- „Terminal“ hat einen Fehler erhalten: „window of tab 1 of window id 4018“ kann nicht gelesen werden. -- Terminal got an error: Can't get window of tab 1 of window id 4707. set windowIdPrefix to "window id " set theOffset to (offset of windowIdPrefix in eMsg) if theOffset = 0 then log "Failed to parse window id from error message: " & eMsg else set windowIdPosition to theOffset + (length of windowIdPrefix) set windowId to (first word of (text windowIdPosition thru -1 of eMsg)) as integer end if on error eMsg2 number eNum2 log "Failed to parse window id from error message: " & eMsg2 & " (" & eMsg & ") " & theOffset end try end if end try end try repeat until ((count of processes of newTab) = 0) delay 0.1 end repeat if windowId is not equal to 0 then close window id windowId else log "Cannot close window, sorry." end if end tell )"}; return script; } ProcessStubCreator::ProcessStubCreator(TerminalInterface *interface) : m_interface(interface) {} static const QLatin1String TerminalAppScriptDetached{R"( tell application "Terminal" activate do script "%1 && exit" end tell )"}; struct AppScript { QString attached; QString detached; }; Result ProcessStubCreator::startStubProcess(const ProcessSetupData &setupData) { const TerminalCommand terminal = TerminalCommand::terminalEmulator(); bool detached = setupData.m_terminalMode == TerminalMode::Detached; if (HostOsInfo::isMacHost()) { // There is a bug in macOS 14.0 where the script fails if it tries to find // the window id. We will have to check in future versions of macOS if they fixed // the issue. static const QVersionNumber osVersionNumber = QVersionNumber::fromString( QSysInfo::productVersion()); static const QMap terminalMap = { {"Terminal.app", {ExternalTerminalProcessImpl::openTerminalScriptAttached(), TerminalAppScriptDetached}}, }; if (terminalMap.contains(terminal.command.path())) { const QString env = Utils::transform(setupData.m_environment.toStringList(), [](const QString &env) { return CommandLine{"export", {env}}.toUserOutput(); }).join('\n'); Process *process = new Process(detached ? nullptr : this); if (detached) { QObject::connect(process, &Process::done, process, &Process::deleteLater); } QTemporaryFile shFile; shFile.setAutoRemove(false); QTC_ASSERT(shFile.open(), return ResultError(Tr::tr("Failed to open temporary script file."))); const QString shScript = QString("cd '%1'\n%2\nclear\n'%3' %4\nrm '%5'\n") .arg(setupData.m_workingDirectory.nativePath()) .arg(env) .arg(setupData.m_commandLine.executable().nativePath()) .arg(setupData.m_commandLine.arguments()) .arg(shFile.fileName()); shFile.write(shScript.toUtf8()); shFile.close(); FilePath::fromUserInput(shFile.fileName()) .setPermissions(QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther | QFile::ReadUser | QFile::ReadGroup | QFile::ReadOther | QFile::WriteUser | QFile::WriteGroup | QFile::WriteOther); const QString script = (detached ? terminalMap.value(terminal.command.path()).detached : terminalMap.value(terminal.command.path()).attached) .arg(shFile.fileName()); process->setCommand({"osascript", {"-"}}); process->setWriteData(script.toUtf8()); process->start(); if (!process->waitForStarted()) { return ResultError( Tr::tr("Failed to start terminal process: \"%1\".").arg(process->errorString())); } QObject::connect(process, &Process::readyReadStandardOutput, process, [process] { const QString output = process->readAllStandardOutput(); if (!output.isEmpty()) qCWarning(logTE).noquote() << output; }); QObject::connect(process, &Process::readyReadStandardError, process, [process] { const QString output = process->readAllStandardError(); if (!output.isEmpty()) qCCritical(logTE).noquote() << output; }); QObject::connect(process, &Process::done, m_interface, &TerminalInterface::onStubExited); return 0; } } Process *process = new Process(detached ? nullptr : this); if (detached) QObject::connect(process, &Process::done, process, &Process::deleteLater); process->setWorkingDirectory(setupData.m_workingDirectory); if constexpr (HostOsInfo::isWindowsHost()) { process->setCommand(setupData.m_commandLine); process->setCreateConsoleOnWindows(true); process->setProcessMode(ProcessMode::Writer); } else { QString extraArgsFromOptions = terminal.executeArgs; CommandLine cmdLine{terminal.command}; if (!extraArgsFromOptions.isEmpty()) cmdLine.addArgs(extraArgsFromOptions, CommandLine::Raw); if (terminal.needsQuotes) cmdLine.addCommandLineAsSingleArg(setupData.m_commandLine); else cmdLine.addCommandLineAsArgs(setupData.m_commandLine, CommandLine::Raw); process->setCommand(cmdLine); } process->setEnvironment( setupData.m_environment.appliedToEnvironment(Environment::systemEnvironment())); process->start(); process->waitForStarted(); if (process->error() != QProcess::UnknownError) { return ResultError( Tr::tr("Failed to start terminal process: \"%1\".").arg(process->errorString())); } qint64 pid = process->processId(); return pid; } } // namespace Utils