646 lines
23 KiB
QML
646 lines
23 KiB
QML
/*
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
|
|
Copyright (c) 2022,2023 Peter G. (nephros)
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
|
import QtQuick 2.6
|
|
import Sailfish.Silica 1.0
|
|
import Nemo.DBus 2.0
|
|
import Nemo.Configuration 1.0
|
|
import Nemo.Notifications 1.0
|
|
import "pages"
|
|
import "cover"
|
|
import "components"
|
|
|
|
ApplicationWindow {
|
|
id: app
|
|
|
|
allowedOrientations: Orientation.All
|
|
|
|
property ListModel ignore: ListModel{} // global ignore list: all that are ignored
|
|
property ListModel pignore: ListModel{} // persistent ignore list: things to be saved are added to this
|
|
|
|
property var watchedJobs: new Object() // record jobs we launched, used as key-valuse store
|
|
|
|
signal refreshed()
|
|
|
|
onIgnoreChanged: refreshed()
|
|
|
|
// app / filter constants
|
|
// order is important here, because of the weird way we construct the page carousel in UnitPage.qml.
|
|
readonly property var supportedUnits: [ "service", "timer", "path", "socket", "target", "mount", ]
|
|
readonly property var ignoreMounts: [
|
|
"apex",
|
|
"odm",
|
|
"vendor",
|
|
"mnt\/vendor",
|
|
"linkerconfig",
|
|
"metadata",
|
|
"system",
|
|
"config",
|
|
];
|
|
readonly property var ignoreStatus: [
|
|
"plugged",
|
|
"tentative",
|
|
];
|
|
|
|
property string osVersion: ""
|
|
property int osVersionI: 0;
|
|
|
|
// systemd dbus
|
|
property alias nfailed: dbus.nFailedUnits
|
|
readonly property bool isSessionBus: dbus.bus == DBus.SessionBus
|
|
readonly property bool isSystemBus: !isSessionBus // convenience
|
|
|
|
// user settings
|
|
property alias notify: config.notify
|
|
property alias notifySticky: config.notifySticky
|
|
property alias showSuccess: config.showSuccess
|
|
property alias checkInterval: config.checkInterval
|
|
property alias ignoreAll: config.ignoreAll
|
|
property alias hybris: config.hybris
|
|
property alias boring: config.boring
|
|
property alias systemctl: config.systemctl
|
|
property alias systembus: config.systembus
|
|
|
|
// update application state
|
|
function refresh() {
|
|
console.debug("update...");
|
|
dbus.getUnits();
|
|
}
|
|
// add something to ignore to the model
|
|
function addIgnore(o) {
|
|
ignore.append(o);
|
|
}
|
|
// add something to ignore to the persistent model
|
|
function addIgnorePerm(o) {
|
|
ignore.append(o);
|
|
pignore.append(o);
|
|
}
|
|
// save model to persistent key
|
|
function saveIgnore() {
|
|
var ds = [];
|
|
// something has a bug causing duplication, lets see if this is better:
|
|
//for (var i=0; i<pignore.count;++i) ds.push(pignore.get(i))
|
|
for (var i=0; i<pignore.count;++i) ds.push({ "unit": pignore.get(i)["unit"]})
|
|
config.ignoreList = JSON.stringify(ds);
|
|
console.debug("saved:", config.ignoreList);
|
|
}
|
|
// load model from persistent key
|
|
function loadIgnore() {
|
|
try {
|
|
var l = JSON.parse(config.ignoreList);
|
|
if ( l.length > 0 ) { pignore.append(l) ; ignore.append(l) }
|
|
} catch (error) {
|
|
console.warn("ignore list load failed:", error )
|
|
}
|
|
}
|
|
// clear all settings
|
|
function resetSettings() {
|
|
resetIgnorePerm();
|
|
ignore.clear();
|
|
config.clear();
|
|
config.sync();
|
|
}
|
|
// clear persistent key
|
|
function resetIgnorePerm() {
|
|
config.setValue('ignoreList', '[]')
|
|
pignore.clear();
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
// for sailjail
|
|
Qt.application.domain = "sailfish.nephros.org";
|
|
Qt.application.version = "unreleased";
|
|
console.info("Intialized", Qt.application.name, "version", Qt.application.version, "by", Qt.application.organization );
|
|
console.debug("Parameters: " + Qt.application.arguments.join(" "))
|
|
// correct landscape for Gemini, set once on start
|
|
allowedOrientations = (devicemodel === 'planetgemini')
|
|
? Orientation.LandscapeInverted
|
|
: defaultAllowedOrientations
|
|
|
|
sfosVersion();
|
|
// reload saved list
|
|
loadIgnore();
|
|
// refresh(); // dbus will refresh itself...
|
|
}
|
|
|
|
// correct landscape for Gemini
|
|
ConfigurationValue {
|
|
id: devicemodel
|
|
key: "/desktop/lipstick-jolla-home/model"
|
|
}
|
|
// application settings:
|
|
ConfigurationGroup {
|
|
id: settings
|
|
path: "/org/nephros/" + Qt.application.name
|
|
}
|
|
ConfigurationGroup {
|
|
id: config
|
|
scope: settings
|
|
path: "app"
|
|
property bool notify: true
|
|
property bool notifySticky: false
|
|
property bool showSuccess: true
|
|
property bool ignoreAll: false
|
|
property int checkInterval: 120
|
|
property bool hybris: false
|
|
property bool boring: false
|
|
property bool systemctl: false
|
|
property bool systembus: false
|
|
property string ignoreList: '[]'
|
|
}
|
|
|
|
// pointless assignment for debugging with qmlscene, which calls itself "QtQmlViewer":
|
|
readonly property string busname: (Qt.application.name === "QtQmlViewer") ? "SailorD" : Qt.application.name
|
|
DBusAdaptor { id: listener
|
|
bus: DBus.SessionBus
|
|
service: "org.nephros.sailfish." + busname
|
|
iface: "org.nephros.sailfish." + busname
|
|
path: "/org/nephros/sailfish/" + busname
|
|
xml: '<interface name="' + iface + '">\n'
|
|
+ '<method name="open" />\n'
|
|
+ '<method name="openPage">\n'
|
|
+ ' <arg name="page" type="s" direction="in">'
|
|
+ ' <doc:doc>'
|
|
+ ' <doc:summary>'
|
|
+ ' Name of the page to open'
|
|
+ ' </doc:summary>'
|
|
+ ' </doc:doc>'
|
|
+ ' </arg>'
|
|
+ '</method>\n'
|
|
+ '<method name="refresh" />\n'
|
|
+ '</interface>\n'
|
|
|
|
function open() {
|
|
console.info("App opened via DBus call.")
|
|
__silica_applicationwindow_instance.activate()
|
|
}
|
|
function openPage(which) {
|
|
open()
|
|
console.warn("Page opening not implemented, just triggering app.")
|
|
return
|
|
switch (which) {
|
|
default:
|
|
pageStack.push(Qt.resolvedUrl("pages/%1.qml".arg(which)))
|
|
}
|
|
return
|
|
}
|
|
function refresh() {
|
|
console.info("DBus triggered refresh.")
|
|
app.refresh()
|
|
}
|
|
Component.onCompleted: console.debug(qsTr("DBus service %1 ready").arg(service))
|
|
}
|
|
|
|
|
|
DBusInterface {
|
|
id: dbus
|
|
bus: DBus.SessionBus
|
|
service: "org.freedesktop.systemd1"
|
|
path: "/org/freedesktop/systemd1"
|
|
iface: "org.freedesktop.systemd1.Manager"
|
|
propertiesEnabled: true
|
|
|
|
/* start handling signals only after the first run: */
|
|
signalsEnabled: false
|
|
//Component.onCompleted: call('Subscribe'); // get signals
|
|
|
|
// handle signal
|
|
function unitFilesChanged() {
|
|
console.debug("dbus UnitFiles changed.");
|
|
app.refresh();
|
|
}
|
|
|
|
// just so we get signals for them:
|
|
property int nNames
|
|
property int nFailedUnits
|
|
onNNamesChanged: {
|
|
console.debug("dbus NNames changed (" + nNames + ")");
|
|
app.refresh();
|
|
}
|
|
onNFailedUnitsChanged: {
|
|
console.debug("dbus NFailedUnits changed (" + nFailedUnits + ")");
|
|
app.refresh();
|
|
}
|
|
function switchBus() {
|
|
bus = (bus == DBus.SessionBus) ? DBus.SystemBus : DBus.SessionBus
|
|
}
|
|
|
|
function enable(u) {
|
|
//console.debug("dbus enable unit", u );
|
|
typedCall('EnableUnitFiles', [
|
|
{ "type": "as", "value": [u] },
|
|
{ "type": "b", "value": false },
|
|
{ "type": "b", "value": false },
|
|
],
|
|
function(result) { successMsg(qsTr("Enable %1").arg(u), result[1]); app.refresh() },
|
|
function(result) { failMsg(qsTr("Enable %1").arg(u), result[1]) }
|
|
);
|
|
}
|
|
function disable(u) {
|
|
//console.debug("dbus disable unit", u );
|
|
typedCall('DisableUnitFiles', [
|
|
{ "type": "as", "value": [u] },
|
|
{ "type": "b", "value": false },
|
|
],
|
|
function(result) { successMsg(qsTr("Disable %1").arg(u), result); app.refresh() },
|
|
function(result) { failMsg(qsTr("Disable %1").arg(u), result) }
|
|
);
|
|
}
|
|
function start(u) {
|
|
//console.debug("dbus start unit", u );
|
|
call('StartUnit',
|
|
[u, "replace",],
|
|
function(result) { console.debug("Job:", JSON.stringify(result)); watchJob(result, qsTr("Start")); },
|
|
function(result) { failMsg(qsTr("Start %1").arg(u), result) }
|
|
);
|
|
}
|
|
function stop(u) {
|
|
//console.debug("dbus stop unit", u );
|
|
call('StopUnit',
|
|
[u, "replace",],
|
|
function(result) { console.debug("Job:", JSON.stringify(result)); watchJob(result, qsTr("Stop")); },
|
|
function(result) { failMsg(qsTr("Stop %1").arg(u), result) }
|
|
);
|
|
}
|
|
function restart(u) {
|
|
//console.debug("dbus restart unit", u );
|
|
call('RestartUnit',
|
|
[u, "replace",],
|
|
function(result) { console.debug("Job:", JSON.stringify(result)); watchJob(result, qsTr("Restart")); },
|
|
function(result) { failMsg(qsTr("Restart %1").arg(u), result) }
|
|
);
|
|
}
|
|
function preset(u) {
|
|
//console.debug("dbus restart unit", u );
|
|
call('PresetUnitFiles',
|
|
[u, false, false,],
|
|
function(result) { successMsg(qsTr("Preset %1").arg(u), result) },
|
|
function(result) { failMsg(qsTr("Preset %1").arg(u), result) }
|
|
);
|
|
}
|
|
function daemon_reload() {
|
|
//console.debug("daemon reload ");
|
|
call('Reload',
|
|
[],
|
|
function(result) { successMsg(qsTr("Preset %1").arg(u), result) },
|
|
function(result) { failMsg(qsTr("Preset %1").arg(u), result) }
|
|
);
|
|
}
|
|
|
|
//signal handler:
|
|
//function jobNew(id, job, unit) {
|
|
// console.debug("Job added: ", id, "/", nJobs)
|
|
//}
|
|
//signal handler for finished jobs:
|
|
function jobRemoved(id, job, unit, result) {
|
|
// TODO: maybe we want notifications about all jobs:
|
|
if (!watchedJobs.hasOwnProperty(job) ) { return; }
|
|
console.debug("Job removed: ", id, "/", result, unit)
|
|
if (result == "done") {
|
|
successMsg(watchedJobs[job], qsTr("%2 for unit %3", "successful message, e.g. 'start for unit foo'").arg(result).arg(unit))
|
|
} else {
|
|
failMsg(watchedJobs[job], qsTr("%1 %2 for unit %3","failure message, e.g. 'starting failed for unit foo'").arg(watchedJobs[job]).arg(result).arg(unit))
|
|
}
|
|
unwatchJob(job);
|
|
app.refresh();
|
|
}
|
|
// handle the internal job queue, we only report jobs we triggered:
|
|
// TODO: maybe we want notifications about all jobs:
|
|
function watchJob(job, action) {
|
|
console.debug("watching:",job);
|
|
watchedJobs[job] = action;
|
|
}
|
|
function unwatchJob(job) {
|
|
console.debug("unwatching:",job);
|
|
if (watchedJobs.hasOwnProperty(job)) {
|
|
delete watchedJobs[job];
|
|
} else {
|
|
console.warn("Trying to remove job not in list");
|
|
}
|
|
}
|
|
|
|
// parameters: unit, callback cb
|
|
function getUnitState(u, cb) {
|
|
call('GetUnitFileState',
|
|
[u],
|
|
function(result) { cb(result) },
|
|
function(result) { console.debug("failure: ", u, result) }
|
|
);
|
|
}
|
|
function getUnits() {
|
|
|
|
/*
|
|
Available unit load states:
|
|
stub loaded not-found error merged masked
|
|
|
|
Available unit active states:
|
|
active reloading inactive failed activating deactivating
|
|
|
|
Available automount unit substates:
|
|
dead waiting running failed
|
|
|
|
Available mount unit substates:
|
|
dead mounting mounting-done mounted remounting unmounting
|
|
remounting-sigterm remounting-sigkill unmounting-sigterm
|
|
unmounting-sigkill failed
|
|
|
|
Available path unit substates:
|
|
dead waiting running failed
|
|
|
|
Available service unit substates:
|
|
dead start-pre start start-post running exited reload stop
|
|
stop-sigabrt stop-sigterm stop-sigkill stop-post final-sigterm
|
|
final-sigkill failed auto-restart
|
|
|
|
Available timer unit substates:
|
|
dead waiting running elapsed failed
|
|
*/
|
|
const filterstates = [
|
|
"loaded", "masked", // unit load state
|
|
"inactive", "active", "failed", // unit active state
|
|
"dead", "waiting", "running", // common substates
|
|
"mounted", "mounting-done", // mount substate
|
|
"exited", "auto-restart", // service substates
|
|
"start-pre", "start-post", // service substates
|
|
"static", // ???
|
|
"elapsed", // timer substate
|
|
"listening", // socket substate
|
|
];
|
|
//call('ListUnits',undefined,
|
|
typedCall('ListUnitsFiltered', { "type": "as", "value": filterstates },
|
|
function(result) { fillModels(result); },
|
|
function(result) { console.critical("Failed to get list of Units: ", result) }
|
|
);
|
|
/* start handling signals only after the first run: */
|
|
if (!signalsEnabled) {
|
|
console.debug("Enabling signals from systemd");
|
|
signalsEnabled = true;
|
|
call('Subscribe'); // get signals
|
|
}
|
|
}
|
|
/*
|
|
* unused. see commit e66ca7a076 for experimental use
|
|
*
|
|
function getUnitFiles(callback) {
|
|
console.debug("calling...");
|
|
const filterpatterns = app.supportedUnits.map(function(e) { return "*." + e }); // need to match unitnnames
|
|
typedCall('ListUnitFilesByPatterns',
|
|
[
|
|
{ "type": "as", "value": [ "static", "loaded", "disabled" ] },
|
|
{ "type": "as", "value": filterpatterns },
|
|
],
|
|
function(result) { console.debug(result); },
|
|
function(result) { console.critical("Failed to get list of Units: ", result) }
|
|
);
|
|
}
|
|
*/
|
|
}
|
|
|
|
/*
|
|
***************************
|
|
* Popups and Notifications
|
|
***************************
|
|
*/
|
|
Notification { id: smessage; isTransient: true; }
|
|
Notification { id: message; isTransient: true; appName: Qt.application.name; appIcon: "harbour-sailord";
|
|
property string pageName: "MainPage" // FIXME
|
|
remoteActions: [ {
|
|
"name": "default",
|
|
"displayName": "Open",
|
|
"service": listener.service,
|
|
"path": listener.path,
|
|
"iface": listener.iface,
|
|
"method": "openPage",
|
|
"arguments": [ pageName ]
|
|
} ]
|
|
}
|
|
|
|
// notification(message)
|
|
function popup(s) {
|
|
smessage.previewSummary = s
|
|
//m ? message.previewBody = m : true
|
|
smessage.urgency = 0;
|
|
smessage.publish();
|
|
}
|
|
// notification (action, message)
|
|
function successMsg(a, m) {
|
|
if (!config.showSuccess) return
|
|
console.debug("success:", m)
|
|
message.previewSummary = qsTr("%1 successful.", "arg is an operation, such as 'restarting service X'").arg(a);
|
|
message.summary = qsTr("Success:");
|
|
message.body = a + ": " + m;
|
|
message.category = "device"
|
|
message.urgency = 0;
|
|
message.publish();
|
|
|
|
}
|
|
// notification(action, message)
|
|
function failMsg(a, m) {
|
|
console.warn("failed:",m)
|
|
message.previewSummary = qsTr("%1 failed.", "arg is an operation, such as 'restarting service X'").arg(a);
|
|
message.summary = qsTr("Failure:");
|
|
message.body = a + ": " + m;
|
|
message.category = "device.error";
|
|
message.urgency = 2;
|
|
message.publish();
|
|
}
|
|
|
|
/*
|
|
****************************
|
|
* Models and model handlers
|
|
****************************
|
|
*/
|
|
ListModel { id: serviceModel }
|
|
ListModel { id: timerModel }
|
|
ListModel { id: pathModel }
|
|
ListModel { id: mountModel }
|
|
ListModel { id: socketModel }
|
|
ListModel { id: targetModel }
|
|
|
|
/*
|
|
* Popups and Notifications
|
|
*/
|
|
|
|
function fillModels(data) {
|
|
console.time("fillModel took: ");
|
|
serviceModel.clear();
|
|
timerModel.clear();
|
|
pathModel.clear();
|
|
mountModel.clear();
|
|
socketModel.clear();
|
|
targetModel.clear();
|
|
|
|
//console.debug("fill model got: " + data.length + " entries.");
|
|
const keys = [
|
|
"unit", "desc", "loaded", "status", "sub", "followed",
|
|
"service", "numjobs", "jobs", "jobpath",
|
|
"custom",
|
|
];
|
|
const o = {};
|
|
const c = {};
|
|
// set up regular expressions outside the loop:
|
|
const typStr = app.supportedUnits.join("|");
|
|
const typre = new RegExp('\.(' + typStr + ')$');
|
|
const intStr = ignoreStatus.join("|");
|
|
const intre = new RegExp('(' + intStr + ')');
|
|
const mntStr = ignoreMounts.join("|");
|
|
const mntre = new RegExp('^\/(' + mntStr + ').*');
|
|
|
|
// magically sort data model by "substatus" and "unit":
|
|
// see https://medium.com/developer-rants/sorting-json-structures-by-multiple-fields-in-javascript-60ed96704df7
|
|
//
|
|
console.time("sorting took: ");
|
|
data = data.sort(function(a, b) {
|
|
// substate = data[N][4], unit = data[N][0]
|
|
const i = 4; const j = 0;
|
|
const ret = 0;
|
|
if (a[i] < b[i]) ret = -1;
|
|
if (a[i] > b[i]) ret = 1;
|
|
if (ret === 0) ret = a[j] < b[j] ? -1 : 1;
|
|
return ret;
|
|
});
|
|
console.timeEnd("sorting took: ");
|
|
|
|
data.forEach(function(e) {
|
|
|
|
//console.debug("looking at:", e);
|
|
|
|
// create object with all values except custom:
|
|
for (var k=0; k<keys.length-1; k++) { o[keys[k]] = e[k]; }
|
|
// last element is custom;
|
|
// record unit type
|
|
c = { "unitType": "unknown", }
|
|
c.unitType = e[0].split(".").pop();
|
|
o.custom = c;
|
|
|
|
/*
|
|
* FILTER STUFF
|
|
* we could use filters on the data array, but doing it in forEach
|
|
* is probably more efficient
|
|
*/
|
|
//console.debug("before filter: " , JSON.stringify(o));
|
|
if (!typre.test(o.unit)) return; // skip uninteresting units (devices, slices...)
|
|
if (app.contains(ignore, "unit", o.unit)) return;
|
|
if (!config.boring) {
|
|
if (intre.test(o.sub)) return; // skip uninteresting substatus
|
|
}
|
|
if ( (!config.hybris) && (o.custom.unitType == "mount") ) { // skip firmware mounts
|
|
if (mntre.test(o.desc)) return;
|
|
if (/^Droid mount/.test(o.desc)) return;
|
|
}
|
|
// weird non-existing services.
|
|
// often they appear because a user-scope service depends on them,
|
|
// but they are system-scope units. So they show up in the list,
|
|
// but details can not be determined.
|
|
//
|
|
// see also: https://serverfault.com/a/836992
|
|
//
|
|
// TODO: maybe switch to system bus on demand and get their properties
|
|
if (o.loaded === "not-found") return;
|
|
|
|
// special case debugging:
|
|
if (o.followed.length > 0) {
|
|
console.debug("INFO:" + o.unit + "is followed by: " + o.followed);
|
|
}
|
|
|
|
/* POPULATE MODELS */
|
|
//console.debug("adding unit of type " + o.custom.unitType );
|
|
switch (o.custom.unitType) {
|
|
case "service":
|
|
serviceModel.append(o);
|
|
// bubble failed ones to the top:
|
|
if (o.sub === "failed") { serviceModel.move(serviceModel.count-1,0,1); return; }
|
|
break;
|
|
case "timer":
|
|
timerModel.append(o);
|
|
break;
|
|
case "mount":
|
|
mountModel.append(o);
|
|
// bubble failed ones to the top:
|
|
if (o.sub === "failed") { mountModel.move(mountModel.count-1,0,1); return; }
|
|
break;
|
|
case "socket":
|
|
socketModel.append(o);
|
|
// bubble failed ones to the top:
|
|
if (o.sub === "failed") { socketModel.move(socketModel.count-1,0,1); return; }
|
|
break;
|
|
case "target":
|
|
targetModel.append(o);
|
|
// bubble failed ones to the top:
|
|
if (o.sub === "dead") { targetModel.move(targetModel.count-1,0,1); return; }
|
|
break;
|
|
case "path":
|
|
pathModel.append(o);
|
|
// bubble failed ones to the top:
|
|
if (o.sub === "failed") { pathModel.move(pathModel.count-1,0,1); return; }
|
|
break;
|
|
default:
|
|
console.debug("ignoring unsupported unit type " + o.custom.unitType );
|
|
return;
|
|
}
|
|
});
|
|
|
|
console.debug("models have now: "
|
|
+ serviceModel.count + " "
|
|
+ timerModel.count + " "
|
|
+ pathModel.count + " "
|
|
+ mountModel.count + " "
|
|
+ socketModel.count + " "
|
|
+ targetModel.count + " "
|
|
+ " (service,timer,path,mount,socket,target) entries."
|
|
);
|
|
app.refreshed();
|
|
console.timeEnd("fillModel took: ");
|
|
}
|
|
|
|
// function contains(model, key, value) {
|
|
function contains(m, k, v) {
|
|
for (var i=0; i<m.count; i++) {
|
|
if (m.get(i)[k] == v) return true
|
|
}
|
|
return false
|
|
}
|
|
function sfosVersion() {
|
|
var fileUrl = "file:///etc/sailfish-release"
|
|
|
|
var r = new XMLHttpRequest()
|
|
r.open('GET', fileUrl);
|
|
r.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
r.send();
|
|
|
|
r.onreadystatechange = function(event) {
|
|
if (r.readyState == XMLHttpRequest.DONE) {
|
|
const vre = /^VERSION_ID=([0-9.]*)$/gm;
|
|
const v = vre.exec(r.response)[1];
|
|
const vn = v.replace(/\./g,"");
|
|
app.osVersion = v;
|
|
app.osVersionI = vn;
|
|
}
|
|
}
|
|
}
|
|
|
|
initialPage: Component { UnitPage{} }
|
|
cover: CoverPage{}
|
|
}
|
|
|
|
// vim: ft=javascript expandtab ts=4 sw=4 st=4
|
|
|