-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathmain.cpp
More file actions
536 lines (483 loc) · 21.8 KB
/
Copy pathmain.cpp
File metadata and controls
536 lines (483 loc) · 21.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImage>
#include <QLocalServer>
#include <QLocalSocket>
#include <QPageSize>
#include <QPainter>
#include <QPdfWriter>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QObject>
#include <QQuickStyle>
#include <QQuickWindow>
#include <QTemporaryDir>
#include <QUrl>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <unistd.h>
#include "backend/StartupTrace.h"
// Selfcheck reporter: DETERMINISTIC output to stdout (does not depend on the
// QML logging rules, which in release may silence console.log).
// Exposed to QML as the context property `SelfCheckOut`.
class SelfCheckReporter : public QObject {
Q_OBJECT
public:
using QObject::QObject;
Q_INVOKABLE void line(const QString &text) {
printf("%s\n", text.toLocal8Bit().constData());
fflush(stdout);
}
};
// V1.2 startup audit: mirrors SelfCheckReporter's pattern (a deterministic,
// QML-invokable sink) but routes through startupTrace() so QML-side marks
// get the SAME elapsed-since-process-start timestamp as the C++ marks
// scattered through main.cpp/DirectoryModel.cpp. Only constructed/exposed
// when OMAFILES_STARTUP_TRACE=1 (see runNormal below).
class StartupTraceReporter : public QObject {
Q_OBJECT
public:
using QObject::QObject;
Q_INVOKABLE void line(const QString &text) {
startupTrace(text.toLocal8Bit().constData());
}
};
// Qt6 bootstrap of the standalone frontend. In normal mode it
// loads app/Main.qml, which instantiates the SAME
// core/OmafilesContent.qml as the frontend, inside a real
// ApplicationWindow.
//
// Phase 12 (josema): adds `--selfcheck`. In that mode the executable stops
// being an alternative host and becomes the OFFICIAL environment for automatic
// validation: it starts headless (offscreen), sets up fixtures in a
// self-cleaning temporary directory and loads
// app/SelfCheck.qml, which exercises the
// backend/frontend/integration subsystems and ends with Qt.exit(number of
// failures). See AUDIT-V2.md and SelfCheck.qml.
//
// OMAFILES_SOURCE_DIR / OMAFILES_QML_IMPORT_DIR are defined by CMakeLists.txt.
namespace {
// Phase 29 (josema): resolves the RESOURCE root (.sh scripts + QML tree).
// Omafiles no longer depends on the repository: if it is installed in
// $XDG_DATA_HOME/omafiles (or wherever CMake put it), it loads from there; if
// not, from the development tree. The first candidate whose Main.qml exists
// wins, so the binary works both after `cmake --install` (even if the repo is
// deleted) and running from the development checkout.
QString resolveResourceDir() {
QStringList candidates;
// 1. Development tree (the repo). It goes FIRST on purpose: if the repo
// exists, it runs from it (live QML editing, as always). If
// the repo is deleted, this path stops existing and it falls back to the
// installation.
candidates << QStringLiteral(OMAFILES_SOURCE_DIR);
// 2. Where CMake installed the resources (respects a configure -D).
candidates << QStringLiteral(OMAFILES_DATA_INSTALL_DIR) + "/omafiles";
// 3. $XDG_DATA_HOME/omafiles at runtime (in case it differs from configure's).
const QByteArray xdg = qgetenv("XDG_DATA_HOME");
const QString dataHome = (!xdg.isEmpty() && xdg.startsWith('/'))
? QString::fromLocal8Bit(xdg)
: QDir::homePath() + "/.local/share";
candidates << dataHome + "/omafiles";
for (const QString &c : candidates) {
if (QFileInfo::exists(c + "/app/Main.qml"))
return c;
}
// Fallback: the standard installation (clear error message if it is not there either).
return QDir::homePath() + "/.local/share/omafiles";
}
// Adds to the engine the import paths the core needs: the
// qs.Commons/qs.Ui adapters (under the resource root) and the C++ plugin
// Omafiles.Backend. For the backend the TWO possible locations are added -- the
// development build/ and the stable installation in ~/.local/lib/qt6/qml -- and
// Qt ignores the one that does not exist, so it works in both modes.
void addImportPaths(QQmlApplicationEngine &engine, const QString &resourceDir) {
// QQmlEngine::addImportPath() PREPENDS each call to the front of the
// search list, so the call order here has to be the REVERSE of the
// intended priority -- confirmed the hard way (2026-08-18): with
// IMPORT_DIR added before INSTALL_DIR, INSTALL_DIR ends up searched
// FIRST, so a freshly-built dev omafiles-standalone silently loaded the
// stale installed Omafiles.Backend plugin instead of its own sibling
// build/qml one -- symbols present in one .so but not the other were the
// giveaway. Same dev-tree-wins-when-present philosophy as
// resolveResourceDir() above; INSTALL_DIR added first so it's the
// fallback once IMPORT_DIR is prepended in front of it.
engine.addImportPath(resourceDir + "/app/qml_modules");
engine.addImportPath(QStringLiteral(OMAFILES_QML_INSTALL_DIR)); // installed (fallback)
engine.addImportPath(QStringLiteral(OMAFILES_QML_IMPORT_DIR)); // dev build/qml (wins if present)
}
// Name of the single-instance socket, per user (to not clash between
// users nor with a headless selfcheck session). It lives under
// XDG_RUNTIME_DIR (QLocalServer resolves it by name).
QString instanceSocketName() {
return QStringLiteral("omafiles-instance-%1").arg(static_cast<uint>(getuid()));
}
// Normalizes the command-line argument to the "payload" that
// core/OmafilesContent.open() understands: an absolute folder path, optionally
// followed by "\n" and names to select (separated by \x1f). Rules:
// · empty -> "" (open() restores the previous session).
// · starts with "/" -> forwarded AS IS. Covers both an absolute path
// and a payload already built by dbus-filemanager1.py
// ("/folder\nname") -- that is why it is not touched.
// · file:// URI -> decoded; if it is a file, "dir\nname".
// · relative path -> made absolute; if it is a file, "dir\nname".
// The file->folder+selection logic only applies to the last two
// (direct use `omafiles ~/x` or xdg-open); the scripts already send something clean.
QString normalizePayload(const QString &arg) {
if (arg.isEmpty()) return QString();
if (arg.startsWith(QLatin1Char('/'))) return arg;
QString path;
if (arg.startsWith(QLatin1String("file://"))) {
const QUrl u(arg);
path = u.isLocalFile() ? u.toLocalFile() : QString();
} else {
path = arg; // relative path
}
if (path.isEmpty()) return QString();
const QFileInfo fi(path);
if (!fi.exists()) return QString();
if (fi.isFile())
return fi.absolutePath() + QLatin1Char('\n') + fi.fileName();
return fi.absoluteFilePath();
}
// Single instance. The FIRST instance listens on a QLocalServer and exposes
// received(payload) to QML (Main.qml connects it to content.open + raise). A
// SECOND invocation (e.g. opening a folder from another app) connects to the
// socket, sends its payload and exits, instead of opening another window.
class SingleInstance : public QObject {
Q_OBJECT
public:
using QObject::QObject;
// Tries to deliver `payload` to an already-running instance. Returns true if
// it succeeded (this invocation must exit without opening a window).
static bool deliverToRunning(const QString &payload) {
QLocalSocket sock;
sock.connectToServer(instanceSocketName());
if (!sock.waitForConnected(300)) return false;
sock.write(payload.toUtf8());
sock.flush();
sock.waitForBytesWritten(300);
sock.disconnectFromServer();
if (sock.state() != QLocalSocket::UnconnectedState)
sock.waitForDisconnected(300);
return true;
}
// Starts listening. removeServer() cleans up an orphan socket from a
// previous instance that died without closing it.
bool listen() {
QLocalServer::removeServer(instanceSocketName());
connect(&m_server, &QLocalServer::newConnection, this,
&SingleInstance::onConnection);
return m_server.listen(instanceSocketName());
}
signals:
void received(const QString &payload);
private slots:
void onConnection() {
QLocalSocket *c = m_server.nextPendingConnection();
if (!c) return;
connect(c, &QLocalSocket::readyRead, this, [this, c]() {
emit received(QString::fromUtf8(c->readAll()));
});
connect(c, &QLocalSocket::disconnected, c, &QObject::deleteLater);
}
private:
QLocalServer m_server;
};
// Generates the deterministic test files the selfcheck needs.
// Everything hangs off `base` (a QTemporaryDir), so it is deleted on its own on
// exit. Requires a live QGuiApplication (QImage/QPdfWriter/QPainter).
bool writeSelfCheckFixtures(const QString &base) {
QDir d(base);
if (!d.mkpath("list/sub") || !d.mkpath("watch") || !d.mkpath("ops") ||
!d.mkpath("json")) {
return false;
}
// Directory with known content for DirectoryModel (natural order:
// the subfolder first, then the .txt).
for (const QString &name : {QStringLiteral("alpha.txt"),
QStringLiteral("beta.txt"),
QStringLiteral("gamma.txt")}) {
QFile f(base + "/list/" + name);
if (!f.open(QIODevice::WriteOnly)) return false;
f.write("x");
f.close();
}
// Text file for PreviewProvider.
QFile note(base + "/note.txt");
if (!note.open(QIODevice::WriteOnly)) return false;
note.write("hello selfcheck\nsecond line\n");
note.close();
// Symlink to validate that copy/move preserve links as links.
QFile::remove(base + "/link.txt");
QFile::link(base + "/note.txt", base + "/link.txt");
// Large file (32 MiB) to test cooperative cancellation mid-copy:
// large enough that the copy spans many chunks and
// the cancel() lands in the middle.
{
QFile big(base + "/big.bin");
if (!big.open(QIODevice::WriteOnly)) return false;
const QByteArray chunk(1 << 20, '\0'); // 1 MiB
for (int i = 0; i < 32; ++i) big.write(chunk);
big.close();
}
// Folder with many files: for the cancellation of a recursive delete
// (removeTree walks entry by entry checking the flag, so with
// enough entries the synchronous cancel aborts mid-way
// deterministically).
if (!d.mkpath("bigdir")) return false;
for (int i = 0; i < 500; ++i) {
QFile f(base + QStringLiteral("/bigdir/f%1").arg(i));
if (!f.open(QIODevice::WriteOnly)) return false;
f.write("x");
f.close();
}
// Read-only folder with a file inside: deleting the child fails
// (EACCES, write permission on the parent is needed). It is restored to
// writable after the run (see runSelfCheck) so that QTemporaryDir
// can clean it up.
if (!d.mkpath("readonly")) return false;
{
QFile f(base + "/readonly/locked.txt");
if (!f.open(QIODevice::WriteOnly)) return false;
f.write("x");
f.close();
}
QFile::setPermissions(base + "/readonly",
QFileDevice::ReadOwner | QFileDevice::ExeOwner);
// Real PNG for ThumbnailProvider (QImageReader path).
QImage img(16, 16, QImage::Format_RGB32);
img.fill(Qt::red);
if (!img.save(base + "/img.png", "PNG")) return false;
// Real one-page PDF for ThumbnailProvider (QPdfDocument/qpdf path).
{
QPdfWriter pdf(base + "/doc.pdf");
pdf.setPageSize(QPageSize(QPageSize::A4));
QPainter p(&pdf);
if (!p.isActive()) return false;
p.drawText(200, 200, QStringLiteral("selfcheck"));
p.end();
}
// Real synthetic WAV for MediaInfo / PreviewProvider audio metadata.
{
QFile wav(base + "/audio.wav");
if (wav.open(QIODevice::WriteOnly)) {
QByteArray w;
w.append("RIFF", 4);
quint32 riffSize = 36 + 44100 * 4;
w.append(reinterpret_cast<const char *>(&riffSize), 4);
w.append("WAVEfmt ", 8);
quint32 fmtSize = 16;
quint16 audioFmt = 1; // PCM
quint16 numCh = 2; // Stereo
quint32 sampleRate = 44100;
quint32 byteRate = 44100 * 4;
quint16 blockAlign = 4;
quint16 bitsPerSample = 16;
w.append(reinterpret_cast<const char *>(&fmtSize), 4);
w.append(reinterpret_cast<const char *>(&audioFmt), 2);
w.append(reinterpret_cast<const char *>(&numCh), 2);
w.append(reinterpret_cast<const char *>(&sampleRate), 4);
w.append(reinterpret_cast<const char *>(&byteRate), 4);
w.append(reinterpret_cast<const char *>(&blockAlign), 2);
w.append(reinterpret_cast<const char *>(&bitsPerSample), 2);
w.append("data", 4);
quint32 dataSize = 44100 * 4;
w.append(reinterpret_cast<const char *>(&dataSize), 4);
w.append(QByteArray(1024, '\0'));
wav.write(w);
wav.close();
}
}
return QFile::exists(base + "/img.png") && QFile::exists(base + "/doc.pdf");
}
int runSelfCheck(int argc, char *argv[]) {
// Headless by default (CI): if the user did not force a platform, offscreen.
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
qputenv("QT_QPA_PLATFORM", "offscreen");
}
// The core reads this in AppBindings to NOT self-register as the file
// manager during validation (no side effects on the system).
qputenv("OMAFILES_SELFCHECK", "1");
QGuiApplication app(argc, argv);
QQuickStyle::setStyle("Basic");
// Phase 29: same resource resolution as normal mode, so the
// selfcheck works both from the development tree and from the
// installation (without the repo).
const QString resourceDir = resolveResourceDir();
qputenv("OMAFILES_RESOURCE_DIR", resourceDir.toLocal8Bit());
// Under $HOME/.cache (not /tmp): this way the fixtures live on the SAME mount
// as the user's XDG trash, and trash/restore round-trip as in
// real use (in /tmp, a separate tmpfs, moveToTrash and restore use
// different trashes). It is still self-cleaning (QTemporaryDir).
QDir().mkpath(QDir::homePath() + "/.cache");
QTemporaryDir tmp(QDir::homePath() + "/.cache/omafiles-selfcheck-XXXXXX");
if (!tmp.isValid() || !writeSelfCheckFixtures(tmp.path())) {
fprintf(stderr, "[selfcheck] could not create the fixtures\n");
return 2;
}
QQmlApplicationEngine engine;
addImportPaths(engine, resourceDir);
SelfCheckReporter reporter;
engine.rootContext()->setContextProperty("selfCheckTmpDir", tmp.path());
engine.rootContext()->setContextProperty("SelfCheckOut", &reporter);
bool failedToCreate = false;
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
[&failedToCreate]() {
failedToCreate = true;
QCoreApplication::exit(2);
},
Qt::QueuedConnection);
engine.load(QUrl::fromLocalFile(resourceDir +
"/app/SelfCheck.qml"));
if (failedToCreate || engine.rootObjects().isEmpty()) {
fprintf(stderr, "[selfcheck] could not load SelfCheck.qml\n");
return 2;
}
// SelfCheck.qml calls Qt.exit(number of failures) on finishing; that code
// exits through app.exec().
const int rc = app.exec();
// Restore permissions of the readonly folder (permission-failure fixture)
// so QTemporaryDir can delete it when destroying tmp.
QFile::setPermissions(tmp.path() + "/readonly",
QFileDevice::ReadOwner | QFileDevice::WriteOwner |
QFileDevice::ExeOwner);
return rc; // tmp is destroyed (and cleaned) on returning from main.
}
int runNormal(int argc, char *argv[]) {
startupTrace("main() entry");
QGuiApplication app(argc, argv);
startupTrace("QGuiApplication constructed");
app.setApplicationName(QStringLiteral("omafiles"));
app.setApplicationVersion(QStringLiteral(APP_VERSION));
// Wayland app_id = "omafiles" (kept on purpose: any
// Hyprland windowrule with class:omafiles keeps working). The installed
// .desktop has a different basename (io.github.percius04.omafiles, mandatory
// for D-Bus activation), so it is matched to this window via
// StartupWMClass=omafiles in the .desktop itself -> the dock/taskbar resolves
// Icon=omafiles without changing the app_id.
// First positional argument = path/URI/payload to open (empty = normal
// start, which restores the previous session ).
// Flags: --new-window / -new-window (a second/Nth window for e.g. another
// Hyprland workspace) skip the single-instance delivery -- this process
// opens its own window instead of handing the invocation to the running
// primary. Everything else starting with '-' is ignored as a flag.
bool newWindow = false;
QString payload;
for (int i = 1; i < argc; ++i) {
const QString a = QString::fromLocal8Bit(argv[i]);
if (a.startsWith(QLatin1Char('-'))) {
if (a == QLatin1String("--new-window") || a == QLatin1String("-new-window"))
newWindow = true;
continue;
}
payload = normalizePayload(a);
break;
}
// Check if this is a file-chooser picker payload
const bool isPicker = payload.contains(QLatin1String("picker:"));
if (isPicker) {
app.setDesktopFileName(QStringLiteral("omafiles-picker"));
} else {
app.setDesktopFileName(QStringLiteral("omafiles"));
}
// Single instance: if there is already an Omafiles open, deliver it the payload
// (it will navigate/select and bring itself to the front) and exit without
// opening another window. If not, this invocation becomes the server instance.
// --new-window skips this entirely (see the flag parsing above).
// Picker instances run independently as separate transient windows.
if (!isPicker && !newWindow && SingleInstance::deliverToRunning(payload)) return 0;
startupTrace("single-instance check done (this is the primary instance)");
// qs.Ui uses QtQuick.Controls (Button/TextField) -- Basic is the style that
// does not depend on any extra native backend, the safest.
QQuickStyle::setStyle("Basic");
QQmlApplicationEngine engine;
startupTrace("QQmlApplicationEngine constructed");
// Phase 29: resolved resource root (installed vs dev). It is published by env
// so state/Paths.qml (resourceDir) locates the .sh scripts without knowing
// the repo.
const QString resourceDir = resolveResourceDir();
qputenv("OMAFILES_RESOURCE_DIR", resourceDir.toLocal8Bit());
addImportPaths(engine, resourceDir);
startupTrace("resourceDir resolved + import paths added");
SingleInstance instance;
// Only the FIRST instance owns the summon socket. --new-window instances
// skip it so the socket (and thus which window receives "open folder"
// requests) stays with the primary; if listen() fails (name taken by a
// race) it simply does not receive summons; the window opens anyway.
if (!newWindow) instance.listen();
engine.rootContext()->setContextProperty("SingleInstance", &instance);
engine.rootContext()->setContextProperty("omafilesInitialPayload", payload);
// V1.2 startup audit: opt-in QML-side trace point, only registered when
// OMAFILES_STARTUP_TRACE=1 -- see backend/StartupTrace.h. QML call sites
// guard with `typeof StartupTrace !== "undefined"` so they're inert
// (a single cheap typeof check) on every normal launch.
StartupTraceReporter startupTraceReporter;
if (startupTraceEnabled())
engine.rootContext()->setContextProperty("StartupTrace", &startupTraceReporter);
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
[]() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
startupTrace("engine.load(Main.qml) starting");
engine.load(
QUrl::fromLocalFile(resourceDir + "/app/Main.qml"));
if (engine.rootObjects().isEmpty()) return -1;
startupTrace("engine.load(Main.qml) returned (QML tree built, all Component.onCompleted ran)");
if (auto *win = qobject_cast<QQuickWindow *>(engine.rootObjects().constFirst())) {
// One-shot: the first real frameSwapped is the first moment actual
// pixels reached the compositor -- what the user perceives as "the
// window appeared", as opposed to `visible: true` merely being set.
// Qt::SingleShotConnection cleanly detaches after the first emission
// instead of freeing memory mid-dispatch (UAF).
QObject::connect(win, &QQuickWindow::frameSwapped, win, []() {
startupTrace("first frame swapped (window actually visible on screen)");
}, Qt::SingleShotConnection);
}
startupTrace("entering app.exec()");
return app.exec();
}
} // namespace
int main(int argc, char *argv[]) {
// V1.2 startup audit: the very first thing main() does, before touching
// Qt at all -- OMAFILES_T0_NS is the reference every startupTrace() call
// (in this binary AND in the backend .so, across the process boundary,
// see backend/StartupTrace.h) measures itself against. Cost when
// OMAFILES_STARTUP_TRACE isn't set: one clock_gettime() + one setenv(),
// ~microseconds.
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
const long long t0Ns = static_cast<long long>(ts.tv_sec) * 1000000000LL + ts.tv_nsec;
char buf[32];
std::snprintf(buf, sizeof(buf), "%lld", t0Ns);
setenv("OMAFILES_T0_NS", buf, 1);
// Raw value (not a startupTrace()-style elapsed-ms line) so an external
// bench harness -- which reads its OWN CLOCK_MONOTONIC just before
// spawning this process -- can correlate the two clocks (CLOCK_MONOTONIC
// is the same clock domain machine-wide on Linux) and derive
// "process launch -> main() reached" i.e. exec+dynamic-linker overhead,
// which no timestamp taken from inside main() can ever see on its own.
if (startupTraceEnabled()) {
std::fprintf(stderr, "[startup-t0] %lld\n", t0Ns);
std::fflush(stderr);
}
}
// Qt6 blocks, for security, file:// reads via XMLHttpRequest unless
// they are explicitly enabled. The standalone's qs.Commons/ThemeSource
// adapter reads Omarchy's live theme files this way
// (colors.toml/shell.toml). Without this, ThemeSource falls back to grey.
// It must go before
// creating the QML engine.
qputenv("QML_XHR_ALLOW_FILE_READ", "1");
for (int i = 1; i < argc; ++i) {
if (QString::fromLocal8Bit(argv[i]) == QLatin1String("--selfcheck")) {
return runSelfCheck(argc, argv);
}
}
return runNormal(argc, argv);
}
#include "main.moc"