Merge remote-tracking branch 'origin/4.9'

Change-Id: I873266bc23680ea02aaff6387790f2a834335113
This commit is contained in:
Eike Ziller
2019-02-05 09:02:01 +01:00
20 changed files with 112 additions and 60 deletions

View File

@@ -19,7 +19,14 @@ Product {
Depends { name: "cpp" } Depends { name: "cpp" }
Depends { name: "qtc" } Depends { name: "qtc" }
Depends { name: product.name + " dev headers"; required: false } Depends {
name: product.name + " dev headers";
required: false
Properties {
condition: Utilities.versionCompare(qbs.version, "1.13") >= 0
enableFallback: false
}
}
Depends { name: "Qt.core"; versionAtLeast: "5.9.0" } Depends { name: "Qt.core"; versionAtLeast: "5.9.0" }
// TODO: Should fall back to what came from Qt.core for Qt < 5.7, but we cannot express that // TODO: Should fall back to what came from Qt.core for Qt < 5.7, but we cannot express that

View File

@@ -97,7 +97,7 @@ void FileNameValidatingLineEdit::setForceFirstCapitalLetter(bool b)
#define SLASHES "/\\" #define SLASHES "/\\"
static const char notAllowedCharsSubDir[] = ",^@=+{}[]~!?:&*\"|#%<>$\"'();`' "; static const char notAllowedCharsSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' ";
static const char notAllowedCharsNoSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' " SLASHES; static const char notAllowedCharsNoSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' " SLASHES;
static const char *notAllowedSubStrings[] = {".."}; static const char *notAllowedSubStrings[] = {".."};

View File

@@ -53,6 +53,7 @@
namespace { namespace {
Q_LOGGING_CATEGORY(androidRunWorkerLog, "qtc.android.run.androidrunnerworker", QtWarningMsg) Q_LOGGING_CATEGORY(androidRunWorkerLog, "qtc.android.run.androidrunnerworker", QtWarningMsg)
static const int GdbTempFileMaxCounter = 20;
} }
using namespace std; using namespace std;
@@ -248,19 +249,60 @@ bool AndroidRunnerWorker::runAdb(const QStringList &args, QString *stdOut,
return result.success(); return result.success();
} }
bool AndroidRunnerWorker::uploadFile(const QString &from, const QString &to, const QString &flags) bool AndroidRunnerWorker::uploadGdbServer()
{ {
QFile f(from); // Push the gdbserver to temp location and then to package dir.
if (!f.open(QIODevice::ReadOnly)) // the files can't be pushed directly to package because of permissions.
qCDebug(androidRunWorkerLog) << "Uploading GdbServer";
bool foundUnique = true;
auto cleanUp = [this, &foundUnique] (QString *p) {
if (foundUnique && !runAdb({"shell", "rm", "-f", *p}))
qCDebug(androidRunWorkerLog) << "Gdbserver cleanup failed.";
delete p;
};
std::unique_ptr<QString, decltype (cleanUp)>
tempGdbServerPath(new QString("/data/local/tmp/%1"), cleanUp);
// Get a unique temp file name for gdbserver copy
int count = 0;
while (deviceFileExists(tempGdbServerPath->arg(++count))) {
if (count > GdbTempFileMaxCounter) {
qCDebug(androidRunWorkerLog) << "Can not get temporary file name";
foundUnique = false;
return false;
}
}
*tempGdbServerPath = tempGdbServerPath->arg(count);
// Copy gdbserver to temp location
if (!runAdb({"push", m_gdbserverPath , *tempGdbServerPath})) {
qCDebug(androidRunWorkerLog) << "Gdbserver upload to temp directory failed";
return false; return false;
runAdb({"shell", "run-as", m_packageName, "rm", to}); }
const QByteArray data = f.readAll();
// Copy gdbserver from temp location to app directory
if (!runAdb({"shell", "run-as", m_packageName, "cp" , *tempGdbServerPath, "./gdbserver"})) {
qCDebug(androidRunWorkerLog) << "Gdbserver copy from temp directory failed";
return false;
}
QTC_ASSERT(runAdb({"shell", "run-as", m_packageName, "chmod", "+x", "./gdbserver"}),
qCDebug(androidRunWorkerLog) << "Gdbserver chmod +x failed.");
return true;
}
bool AndroidRunnerWorker::deviceFileExists(const QString &filePath)
{
QString output; QString output;
const bool res = runAdb({"shell", "run-as", m_packageName, QString("sh -c 'base64 -d > %1'").arg(to)}, const bool success = runAdb({"shell", "ls", filePath, "2>/dev/null"}, &output);
&output, data.toBase64()); return success && !output.trimmed().isEmpty();
if (!res || output.contains("base64: not found")) }
return false;
return runAdb({"shell", "run-as", m_packageName, "chmod", flags, to}); bool AndroidRunnerWorker::packageFileExists(const QString &filePath)
{
QString output;
const bool success = runAdb({"shell", "run-as", m_packageName, "ls", filePath, "2>/dev/null"}, &output);
return success && !output.trimmed().isEmpty();
} }
void AndroidRunnerWorker::adbKill(qint64 pid) void AndroidRunnerWorker::adbKill(qint64 pid)
@@ -403,26 +445,28 @@ void AndroidRunnerWorker::asyncStartHelper()
// e.g. on Android 8 with NDK 10e // e.g. on Android 8 with NDK 10e
runAdb({"shell", "run-as", m_packageName, "chmod", "a+x", packageDir.trimmed()}); runAdb({"shell", "run-as", m_packageName, "chmod", "a+x", packageDir.trimmed()});
QString gdbServerExecutable; QString gdbServerExecutable = "gdbserver";
QString gdbServerPrefix = "./lib/"; QString gdbServerPrefix = "./lib/";
if (m_gdbserverPath.isEmpty() || !uploadFile(m_gdbserverPath, "gdbserver")) { auto findGdbServer = [this, &gdbServerExecutable, gdbServerPrefix](const QString& gdbEx) {
// upload failed - check for old devices if (!packageFileExists(gdbServerPrefix + gdbEx))
QString output; return false;
if (runAdb({"shell", "run-as", m_packageName, "ls", "lib/"}, &output)) { gdbServerExecutable = gdbEx;
for (const auto &line: output.split('\n')) { return true;
if (line.indexOf("gdbserver") != -1/* || line.indexOf("lldb-server") != -1*/) { };
gdbServerExecutable = line.trimmed();
break; if (!findGdbServer("gdbserver") && !findGdbServer("libgdbserver.so")) {
} // Armv8. symlink lib is not available.
} // Kill the previous instances of gdbserver. Do this before copying the gdbserver.
} runAdb({"shell", "run-as", m_packageName, "killall", gdbServerExecutable});
if (gdbServerExecutable.isEmpty()) { if (!m_gdbserverPath.isEmpty() && uploadGdbServer()) {
gdbServerPrefix = "./";
} else {
emit remoteProcessFinished(tr("Cannot find/copy C++ debug server.")); emit remoteProcessFinished(tr("Cannot find/copy C++ debug server."));
return; return;
} }
} else { } else {
gdbServerPrefix = "./"; qCDebug(androidRunWorkerLog) << "Found GDB server under ./lib";
gdbServerExecutable = "gdbserver"; runAdb({"shell", "run-as", m_packageName, "killall", gdbServerExecutable});
} }
QString debuggerServerErr; QString debuggerServerErr;
@@ -484,7 +528,6 @@ bool AndroidRunnerWorker::startDebuggerServer(const QString &packageDir,
QString *errorStr) QString *errorStr)
{ {
QString gdbServerSocket = packageDir + "/debug-socket"; QString gdbServerSocket = packageDir + "/debug-socket";
runAdb({"shell", "run-as", m_packageName, "killall", gdbServerExecutable});
runAdb({"shell", "run-as", m_packageName, "rm", gdbServerSocket}); runAdb({"shell", "run-as", m_packageName, "rm", gdbServerSocket});
QString gdbProcessErr; QString gdbProcessErr;

View File

@@ -47,7 +47,6 @@ public:
AndroidRunnerWorker(ProjectExplorer::RunWorker *runner, const QString &packageName); AndroidRunnerWorker(ProjectExplorer::RunWorker *runner, const QString &packageName);
~AndroidRunnerWorker() override; ~AndroidRunnerWorker() override;
bool uploadFile(const QString &from, const QString &to, const QString &flags = QString("+x"));
bool runAdb(const QStringList &args, QString *stdOut = nullptr, const QByteArray &writeData = {}); bool runAdb(const QStringList &args, QString *stdOut = nullptr, const QByteArray &writeData = {});
void adbKill(qint64 pid); void adbKill(qint64 pid);
QStringList selector() const; QStringList selector() const;
@@ -71,10 +70,13 @@ signals:
void remoteOutput(const QString &output); void remoteOutput(const QString &output);
void remoteErrorOutput(const QString &output); void remoteErrorOutput(const QString &output);
protected: private:
void asyncStartHelper(); void asyncStartHelper();
bool startDebuggerServer(const QString &packageDir, const QString &gdbServerPrefix, bool startDebuggerServer(const QString &packageDir, const QString &gdbServerPrefix,
const QString &gdbServerExecutable, QString *errorStr = nullptr); const QString &gdbServerExecutable, QString *errorStr = nullptr);
bool deviceFileExists(const QString &filePath);
bool packageFileExists(const QString& filePath);
bool uploadGdbServer();
enum class JDBState { enum class JDBState {
Idle, Idle,

View File

@@ -145,7 +145,7 @@ void TestResultItem::updateResult(bool &changed, Result::Type addedChildType)
? Result::MessageTestCaseSuccess : old; ? Result::MessageTestCaseSuccess : old;
break; break;
default: default:
return; break;
} }
changed = old != newResult; changed = old != newResult;
if (changed) if (changed)

View File

@@ -51,16 +51,6 @@ HelpItem::HelpItem(const QUrl &url, const QString &docMark, HelpItem::Category c
, m_category(category) , m_category(category)
{} {}
HelpItem::HelpItem(const QUrl &url,
const QString &docMark,
HelpItem::Category category,
const QMap<QString, QUrl> &helpLinks)
: m_helpUrl(url)
, m_docMark(docMark)
, m_category(category)
, m_helpLinks(helpLinks)
{}
HelpItem::HelpItem(const QString &helpId, const QString &docMark, Category category) HelpItem::HelpItem(const QString &helpId, const QString &docMark, Category category)
: HelpItem(QStringList(helpId), docMark, category) : HelpItem(QStringList(helpId), docMark, category)
{} {}
@@ -105,6 +95,11 @@ void HelpItem::setCategory(Category cat)
HelpItem::Category HelpItem::category() const HelpItem::Category HelpItem::category() const
{ return m_category; } { return m_category; }
bool HelpItem::isEmpty() const
{
return m_helpUrl.isEmpty() && m_helpIds.isEmpty();
}
bool HelpItem::isValid() const bool HelpItem::isValid() const
{ {
if (m_helpUrl.isEmpty() && m_helpIds.isEmpty()) if (m_helpUrl.isEmpty() && m_helpIds.isEmpty())

View File

@@ -59,8 +59,6 @@ public:
HelpItem(const QStringList &helpIds, const QString &docMark, Category category); HelpItem(const QStringList &helpIds, const QString &docMark, Category category);
explicit HelpItem(const QUrl &url); explicit HelpItem(const QUrl &url);
HelpItem(const QUrl &url, const QString &docMark, Category category); HelpItem(const QUrl &url, const QString &docMark, Category category);
HelpItem(const QUrl &url, const QString &docMark, Category category,
const QMap<QString, QUrl> &helpLinks);
void setHelpUrl(const QUrl &url); void setHelpUrl(const QUrl &url);
const QUrl &helpUrl() const; const QUrl &helpUrl() const;
@@ -74,6 +72,7 @@ public:
void setCategory(Category cat); void setCategory(Category cat);
Category category() const; Category category() const;
bool isEmpty() const;
bool isValid() const; bool isValid() const;
QString extractContent(bool extended) const; QString extractContent(bool extended) const;

View File

@@ -1714,7 +1714,7 @@ void GdbEngine::detachDebugger()
{ {
CHECK_STATE(InferiorStopOk); CHECK_STATE(InferiorStopOk);
QTC_CHECK(runParameters().startMode != AttachCore); QTC_CHECK(runParameters().startMode != AttachCore);
DebuggerCommand cmd("detach", ExitRequest); DebuggerCommand cmd("detach", NativeCommand | ExitRequest);
cmd.callback = [this](const DebuggerResponse &) { cmd.callback = [this](const DebuggerResponse &) {
CHECK_STATE(InferiorStopOk); CHECK_STATE(InferiorStopOk);
notifyInferiorExited(); notifyInferiorExited();

View File

@@ -652,7 +652,7 @@ void HelpPluginPrivate::requestContextHelp()
? tipHelpValue.value<HelpItem>() ? tipHelpValue.value<HelpItem>()
: HelpItem(tipHelpValue.toString()); : HelpItem(tipHelpValue.toString());
IContext *context = ICore::currentContextObject(); IContext *context = ICore::currentContextObject();
if (!tipHelp.isValid() && context) if (tipHelp.isEmpty() && context)
context->contextHelp([this](const HelpItem &item) { showContextHelp(item); }); context->contextHelp([this](const HelpItem &item) { showContextHelp(item); });
else else
showContextHelp(tipHelp); showContextHelp(tipHelp);

View File

@@ -430,7 +430,8 @@ void TextBrowserHelpWidget::mouseReleaseEvent(QMouseEvent *e)
bool controlPressed = e->modifiers() & Qt::ControlModifier; bool controlPressed = e->modifiers() & Qt::ControlModifier;
const QString link = linkAt(e->pos()); const QString link = linkAt(e->pos());
if ((controlPressed || e->button() == Qt::MidButton) && !link.isEmpty()) { if (m_parent->isActionVisible(HelpViewer::Action::NewPage)
&& (controlPressed || e->button() == Qt::MidButton) && !link.isEmpty()) {
emit m_parent->newPageRequested(QUrl(link)); emit m_parent->newPageRequested(QUrl(link));
return; return;
} }

View File

@@ -591,8 +591,9 @@ void Client::handleCodeActionResponse(const CodeActionRequest::Response &respons
for (const Utils::variant<Command, CodeAction> &item : *list) { for (const Utils::variant<Command, CodeAction> &item : *list) {
if (auto action = Utils::get_if<CodeAction>(&item)) if (auto action = Utils::get_if<CodeAction>(&item))
updateCodeActionRefactoringMarker(this, *action, uri); updateCodeActionRefactoringMarker(this, *action, uri);
else if (auto command = Utils::get_if<Command>(&item)) else if (auto command = Utils::get_if<Command>(&item)) {
; // todo Q_UNUSED(command); // todo
}
} }
} }
} }

View File

@@ -230,7 +230,7 @@ bool AutoCompleter::contextAllowsAutoQuotes(const QTextCursor &cursor,
} }
// never insert ' into string literals, it adds spurious ' when writing contractions // never insert ' into string literals, it adds spurious ' when writing contractions
if (textToInsert.at(0) == QLatin1Char('\'')) if (textToInsert.at(0) == QLatin1Char('\'') && quote != '\'')
return false; return false;
if (textToInsert.at(0) != quote || isCompleteStringLiteral(tokenText)) if (textToInsert.at(0) != quote || isCompleteStringLiteral(tokenText))

View File

@@ -185,8 +185,7 @@ bool QmlJSHoverHandler::setQmlTypeHelp(const ScopeChain &scopeChain, const Docum
// Use the URL, to disambiguate different versions // Use the URL, to disambiguate different versions
const HelpItem helpItem(filteredUrlMap.first(), const HelpItem helpItem(filteredUrlMap.first(),
qName.join(QLatin1Char('.')), qName.join(QLatin1Char('.')),
HelpItem::QmlComponent, HelpItem::QmlComponent);
filteredUrlMap);
setLastHelpItemIdentified(helpItem); setLastHelpItemIdentified(helpItem);
return true; return true;
} }

View File

@@ -8020,7 +8020,7 @@ void BaseTextEditor::setContextHelp(const HelpItem &item)
void TextEditorWidget::contextHelpItem(const IContext::HelpCallback &callback) void TextEditorWidget::contextHelpItem(const IContext::HelpCallback &callback)
{ {
if (!d->m_contextHelpItem.isValid() && !d->m_hoverHandlers.isEmpty()) { if (d->m_contextHelpItem.isEmpty() && !d->m_hoverHandlers.isEmpty()) {
d->m_hoverHandlers.first()->contextHelpId(this, d->m_hoverHandlers.first()->contextHelpId(this,
Text::wordStartCursor(textCursor()).position(), Text::wordStartCursor(textCursor()).position(),
callback); callback);

View File

@@ -173,7 +173,7 @@ bool AddDebuggerOperation::test() const
QVariantMap AddDebuggerOperation::addDebugger(const QVariantMap &map, QVariantMap AddDebuggerOperation::addDebugger(const QVariantMap &map,
const QString &id, const QString &displayName, const QString &id, const QString &displayName,
const quint32 &engine, const QString &binary, int engine, const QString &binary,
const QStringList &abis, const KeyValuePairList &extra) const QStringList &abis, const KeyValuePairList &extra)
{ {
// Sanity check: Make sure autodetection source is not in use already: // Sanity check: Make sure autodetection source is not in use already:

View File

@@ -46,7 +46,7 @@ public:
static QVariantMap addDebugger(const QVariantMap &map, static QVariantMap addDebugger(const QVariantMap &map,
const QString &id, const QString &displayName, const QString &id, const QString &displayName,
const quint32 &engine, const QString &binary, int engine, const QString &binary,
const QStringList &abis, const KeyValuePairList &extra); const QStringList &abis, const KeyValuePairList &extra);
static QVariantMap initializeDebuggers(); static QVariantMap initializeDebuggers();
@@ -54,7 +54,7 @@ public:
private: private:
QString m_id; QString m_id;
QString m_displayName; QString m_displayName;
quint32 m_engine = 0; int m_engine = 0;
QString m_binary; QString m_binary;
QStringList m_abis; QStringList m_abis;
KeyValuePairList m_extra; KeyValuePairList m_extra;

View File

@@ -196,6 +196,8 @@
:Send to Codepaster_CodePaster::PasteView {name='CodePaster__Internal__ViewDialog' type='CodePaster::PasteView' visible='1' windowTitle='Send to Codepaster'} :Send to Codepaster_CodePaster::PasteView {name='CodePaster__Internal__ViewDialog' type='CodePaster::PasteView' visible='1' windowTitle='Send to Codepaster'}
:Session Manager_ProjectExplorer::Internal::SessionDialog {name='ProjectExplorer__Internal__SessionDialog' type='ProjectExplorer::Internal::SessionDialog' visible='1' windowTitle='Session Manager'} :Session Manager_ProjectExplorer::Internal::SessionDialog {name='ProjectExplorer__Internal__SessionDialog' type='ProjectExplorer::Internal::SessionDialog' visible='1' windowTitle='Session Manager'}
:Startup.contextHelpComboBox_QComboBox {container=':Form.Startup_QGroupBox' name='contextHelpComboBox' type='QComboBox' visible='1'} :Startup.contextHelpComboBox_QComboBox {container=':Form.Startup_QGroupBox' name='contextHelpComboBox' type='QComboBox' visible='1'}
:Take a UI Tour.Cancel_QPushButton {text='Cancel' type='QPushButton' unnamed='1' visible='1' window=':Take a UI Tour_Utils::CheckableMessageBox'}
:Take a UI Tour_Utils::CheckableMessageBox {type='Utils::CheckableMessageBox' unnamed='1' visible='1' windowTitle='Take a UI Tour'}
:User Interface.languageBox_QComboBox {container=':Core__Internal__GeneralSettings.User Interface_QGroupBox' name='languageBox' type='QComboBox' visible='1'} :User Interface.languageBox_QComboBox {container=':Core__Internal__GeneralSettings.User Interface_QGroupBox' name='languageBox' type='QComboBox' visible='1'}
:Widget Box_qdesigner_internal::WidgetBoxTreeWidget {container=':*Qt Creator.Widget Box_QDockWidget' type='qdesigner_internal::WidgetBoxTreeWidget' unnamed='1' visible='1'} :Widget Box_qdesigner_internal::WidgetBoxTreeWidget {container=':*Qt Creator.Widget Box_QDockWidget' type='qdesigner_internal::WidgetBoxTreeWidget' unnamed='1' visible='1'}
:Working Copy_Utils::BaseValidatingLineEdit {type='Utils::FancyLineEdit' unnamed='1' visible='1' window=':New Text File_ProjectExplorer::JsonWizard'} :Working Copy_Utils::BaseValidatingLineEdit {type='Utils::FancyLineEdit' unnamed='1' visible='1' window=':New Text File_ProjectExplorer::JsonWizard'}

View File

@@ -30,7 +30,7 @@ def switchViewTo(view):
waitFor("not QToolTip.isVisible()", 15000) waitFor("not QToolTip.isVisible()", 15000)
if view < ViewConstants.FIRST_AVAILABLE or view > ViewConstants.LAST_AVAILABLE: if view < ViewConstants.FIRST_AVAILABLE or view > ViewConstants.LAST_AVAILABLE:
return return
tabBar = waitForObject("{type='Core::Internal::FancyTabBar' unnamed='1' visible='1' " tabBar = waitForObject("{name='ModeSelector' type='Core::Internal::FancyTabBar' visible='1' "
"window=':Qt Creator_Core::Internal::MainWindow'}") "window=':Qt Creator_Core::Internal::MainWindow'}")
mouseMove(tabBar, 20, 20 + 52 * view) mouseMove(tabBar, 20, 20 + 52 * view)
if waitFor("QToolTip.isVisible()", 10000): if waitFor("QToolTip.isVisible()", 10000):
@@ -43,7 +43,7 @@ def switchViewTo(view):
test.passes("ToolTip verified") test.passes("ToolTip verified")
else: else:
test.warning("ToolTip does not match", "Expected pattern: %s\nGot: %s" % (pattern, text)) test.warning("ToolTip does not match", "Expected pattern: %s\nGot: %s" % (pattern, text))
mouseClick(waitForObject("{type='Core::Internal::FancyTabBar' unnamed='1' visible='1' " mouseClick(waitForObject("{name='ModeSelector' type='Core::Internal::FancyTabBar' visible='1' "
"window=':Qt Creator_Core::Internal::MainWindow'}"), 20, 20 + 52 * view, 0, Qt.LeftButton) "window=':Qt Creator_Core::Internal::MainWindow'}"), 20, 20 + 52 * view, 0, Qt.LeftButton)
def __kitIsActivated__(kit): def __kitIsActivated__(kit):

View File

@@ -55,7 +55,7 @@ source("../../shared/welcome.py")
source("../../shared/workarounds.py") # include this at last source("../../shared/workarounds.py") # include this at last
# additionalParameters must be a list or tuple of strings or None # additionalParameters must be a list or tuple of strings or None
def startQC(additionalParameters=None, withPreparedSettingsPath=True): def startQC(additionalParameters=None, withPreparedSettingsPath=True, cancelTour=True):
global SettingsPath global SettingsPath
appWithOptions = ['"Qt Creator"' if platform.system() == 'Darwin' else "qtcreator"] appWithOptions = ['"Qt Creator"' if platform.system() == 'Darwin' else "qtcreator"]
if withPreparedSettingsPath: if withPreparedSettingsPath:
@@ -65,7 +65,10 @@ def startQC(additionalParameters=None, withPreparedSettingsPath=True):
if platform.system() in ('Microsoft', 'Windows'): # for hooking into native file dialog if platform.system() in ('Microsoft', 'Windows'): # for hooking into native file dialog
appWithOptions.extend(('-platform', 'windows:dialogs=none')) appWithOptions.extend(('-platform', 'windows:dialogs=none'))
test.log("Starting now: %s" % ' '.join(appWithOptions)) test.log("Starting now: %s" % ' '.join(appWithOptions))
return startApplication(' '.join(appWithOptions)) appContext = startApplication(' '.join(appWithOptions))
if cancelTour:
clickButton(waitForObject(":Take a UI Tour.Cancel_QPushButton"))
return appContext;
def startedWithoutPluginError(): def startedWithoutPluginError():
try: try:

View File

@@ -46,7 +46,7 @@ def main():
invokeMenuItem("File", "Exit") invokeMenuItem("File", "Exit")
waitForCleanShutdown() waitForCleanShutdown()
snooze(4) # wait for complete unloading of Creator snooze(4) # wait for complete unloading of Creator
startQC() startQC(cancelTour=False)
try: try:
# Use Locator for menu items which wouldn't work on macOS # Use Locator for menu items which wouldn't work on macOS
exitCommand = testData.field(lang, "Exit") exitCommand = testData.field(lang, "Exit")