You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
systemd-watcher/qml/harbour-sailord.qml

586 lines
21 KiB

/*
Apache License 2.0
Copyright (c) 2022 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
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 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 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: '[]'
}
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: message; isTransient: true; appName: Qt.application.name; appIcon: "harbour-sailord"; }
Notification { id: smessage; isTransient: true; }
// 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) {
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