systemd: logs: Use reportd for reporting problems
Currently, crash reporting is done by running the command-line tool, which asks for user input on the standard streams. This completely breaks most workflows, as, for example, one needs to log in to their RH Bugzilla account to file a bug, but the prompt is shown on the server-side only. reportd was written for the express purpose of dealing with such cases. With version 0.5, it supports signaling clients about incoming prompts and provides an API to deal with them. Closes: #11150
This commit is contained in:
parent
6dc388a470
commit
d1b7154026
|
@ -434,3 +434,40 @@ body {
|
|||
.fa-exclamation-circle {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.reporting-table tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
table.reporting-table td:first-child {
|
||||
white-space: nowrap;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
table.reporting-table td:nth-child(2) {
|
||||
text-align: start;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
table.reporting-table td:nth-child(2) > .spinner {
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td.report-column {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
#journal-entry-heading {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#journal-entry-message {
|
||||
padding-left: 1.5rem;
|
||||
|
||||
}
|
||||
|
|
|
@ -81,17 +81,11 @@ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|||
<div id="journal-entry" class="container-fluid" hidden>
|
||||
<ol class="breadcrumb">
|
||||
<li><a tabindex="0" id="journal-navigate-home" translate="yes">Logs</a></li>
|
||||
<li class="active" translate="yes">Entry</li>
|
||||
<li id="journal-entry-crumb" class="active" translate="yes">Entry</li>
|
||||
</ol>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span id="journal-entry-id"></span>
|
||||
<span id="journal-entry-date" class="pull-right"></span>
|
||||
</div>
|
||||
<div id="journal-entry-message"></div>
|
||||
<table class="info-table-ct" id="journal-entry-fields">
|
||||
</table>
|
||||
</div>
|
||||
<h2 id="journal-entry-heading"></h2>
|
||||
<table class="info-table-ct panel panel-default" id="journal-entry-fields">
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="logs.js"></script>
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
import $ from "jquery";
|
||||
import cockpit from "cockpit";
|
||||
import { journal } from "journal";
|
||||
import moment from "moment";
|
||||
import { init_reporting } from "./reporting.jsx";
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
|
@ -426,12 +428,14 @@ $(function() {
|
|||
var cursor = cockpit.location.path[0];
|
||||
var out = $('#journal-entry-fields');
|
||||
|
||||
const reportingTable = document.getElementById("journal-entry-reporting-table");
|
||||
if (reportingTable != null) {
|
||||
reportingTable.remove();
|
||||
}
|
||||
|
||||
out.empty();
|
||||
|
||||
function show_entry(entry) {
|
||||
var d = new Date(entry.__REALTIME_TIMESTAMP / 1000);
|
||||
$('#journal-entry-date').text(d.toString());
|
||||
|
||||
var id;
|
||||
if (entry.SYSLOG_IDENTIFIER)
|
||||
id = entry.SYSLOG_IDENTIFIER;
|
||||
|
@ -446,13 +450,20 @@ $(function() {
|
|||
id = entry.PROBLEM_BINARY;
|
||||
}
|
||||
|
||||
$('#journal-entry-id').text(id);
|
||||
$('#journal-entry-heading').text(id);
|
||||
|
||||
const crumb = $("#journal-entry-crumb");
|
||||
const date = moment(new Date(entry.__REALTIME_TIMESTAMP / 1000));
|
||||
|
||||
if (is_problem) {
|
||||
crumb.text(cockpit.format(_("$0: crash at $1"), id, date.format("YYYY-MM-DD HH:mm:ss")));
|
||||
|
||||
find_problems().done(function() {
|
||||
create_problem(out, entry);
|
||||
});
|
||||
} else {
|
||||
crumb.text(cockpit.format(_("Entry at $0"), date.format("YYYY-MM-DD HH:mm:ss")));
|
||||
|
||||
create_entry(out, entry);
|
||||
}
|
||||
}
|
||||
|
@ -476,8 +487,21 @@ $(function() {
|
|||
});
|
||||
}
|
||||
|
||||
function create_message_row(entry) {
|
||||
const reasonColumn = document.createElement("th");
|
||||
reasonColumn.setAttribute("colspan", 2);
|
||||
reasonColumn.setAttribute("id", "journal-entry-message");
|
||||
reasonColumn.appendChild(document.createTextNode(journal.printable(entry.MESSAGE)));
|
||||
|
||||
const reason = document.createElement("tr");
|
||||
reason.appendChild(reasonColumn);
|
||||
|
||||
return reason;
|
||||
}
|
||||
|
||||
function create_entry(out, entry) {
|
||||
$('#journal-entry-message').text(journal.printable(entry.MESSAGE));
|
||||
out.append(create_message_row(entry));
|
||||
|
||||
var keys = Object.keys(entry).sort();
|
||||
$.each(keys, function (i, key) {
|
||||
if (key !== 'MESSAGE') {
|
||||
|
@ -515,7 +539,11 @@ $(function() {
|
|||
.replaceWith(new_content);
|
||||
}
|
||||
|
||||
$('#journal-entry-message').text('');
|
||||
const heading = document.createElement("h3");
|
||||
heading.appendChild(document.createTextNode(_("Extended Information")));
|
||||
|
||||
const caption = document.createElement("caption");
|
||||
caption.appendChild(heading);
|
||||
|
||||
var ge_t = $('<li class="active">').append($('<a tabindex="0">').append($('<span translate="yes">').text(_("General"))));
|
||||
var pi_t = $('<li>').append($('<a tabindex="0">').append($('<span translate="yes">').text(_("Problem info"))));
|
||||
|
@ -527,56 +555,18 @@ $(function() {
|
|||
.append(
|
||||
$('<tr>').append($('<div class="panel-group" id="accordion-markup">')));
|
||||
|
||||
var tab = $('<ul class="nav nav-tabs nav-tabs-pf">');
|
||||
var tab = $('<ul class="nav nav-tabs nav-tabs-pf">')
|
||||
.attr("id", "problem-navigation");
|
||||
|
||||
var d_btn = $('<button class="btn btn-danger problem-btn btn-delete pficon pficon-delete">');
|
||||
var r_btn = $();
|
||||
if (problem.IsReported) {
|
||||
for (var pid = 0; pid < problem.Reports.length; pid++) {
|
||||
if (problem.Reports[pid][0] === 'ABRT Server') {
|
||||
var url = problem.Reports[pid][1].URL.v.v;
|
||||
r_btn = $('<a class="problem-btn">')
|
||||
.attr('href', url)
|
||||
.attr("target", "_blank")
|
||||
.attr("rel", "noopener noreferrer")
|
||||
.text(_("Reported"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (problem.CanBeReported) {
|
||||
r_btn = $('<button class="btn btn-primary problem-btn">').text(_("Report"));
|
||||
|
||||
r_btn.click(function() {
|
||||
tab.children(':last-child').replaceWith($('<div class="spinner problem-btn">'));
|
||||
var proc = cockpit.spawn(['reporter-ureport', '-d', problem.ID], { superuser: 'true' });
|
||||
proc.done(function() {
|
||||
window.location.reload();
|
||||
});
|
||||
proc.fail(function(ex) {
|
||||
var message;
|
||||
// 70 is 'This problem has already been reported'
|
||||
if (ex.exit_status === 70) {
|
||||
window.location.reload();
|
||||
return;
|
||||
} else if (ex.problem === 'access-denied') {
|
||||
message = _("Not authorized to upload-report");
|
||||
} else if (ex.problem === "not-found") {
|
||||
message = _("Reporter 'reporter-ureport' not found.");
|
||||
} else {
|
||||
message = _("Reporting was unsucessful. Try running `reporter-ureport -d " + problem.ID + "`");
|
||||
}
|
||||
const reportingTable = document.createElement("div");
|
||||
reportingTable.setAttribute("id", "journal-entry-reporting-table");
|
||||
|
||||
$('<div class="pf-c-alert pf-m-danger pf-m-inline" aria-label="inline danger alert">')
|
||||
.append($('<div class="pf-c-alert__icon">' +
|
||||
'<span class="pficon pficon-error-circle-o">' +
|
||||
'</div>'),
|
||||
$('<h4 class="pf-c-alert__title">').text(message)
|
||||
)
|
||||
.insertAfter(".breadcrumb");
|
||||
tab.children(':last-child').replaceWith($('<span>'));
|
||||
});
|
||||
});
|
||||
}
|
||||
const journalTable = document.getElementById("journal-entry-fields");
|
||||
journalTable.insertAdjacentElement("beforebegin", reportingTable);
|
||||
|
||||
init_reporting(problem, reportingTable);
|
||||
|
||||
ge_t.click(function() {
|
||||
switch_tab(ge_t, ge);
|
||||
|
@ -615,13 +605,15 @@ $(function() {
|
|||
tab.append(pi_t);
|
||||
tab.append(pd_t);
|
||||
tab.append(d_btn);
|
||||
tab.append(r_btn);
|
||||
|
||||
var header = $('<tr>').append(
|
||||
$('<th colspan=2>').append(tab));
|
||||
|
||||
out.html(header).append(ge);
|
||||
out.html(header).append(create_message_row(entry));
|
||||
out.append(ge);
|
||||
out.prepend(caption);
|
||||
out.css("margin-bottom", "0px");
|
||||
|
||||
create_problem_details(problem, pi, pd);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,511 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2019 Red Hat, Inc.
|
||||
*
|
||||
* Cockpit is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Cockpit is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import cockpit from "cockpit";
|
||||
import React from "react";
|
||||
import ReactDOM from 'react-dom';
|
||||
import { show_modal_dialog } from "cockpit-components-dialog.jsx";
|
||||
|
||||
const _ = cockpit.gettext;
|
||||
|
||||
const TaskState = Object.freeze({
|
||||
READY: 0,
|
||||
RUNNING: 1,
|
||||
COMPLETED: 2,
|
||||
ERROR: 3,
|
||||
CANCELED: 4,
|
||||
});
|
||||
|
||||
const PromptType = Object.freeze({
|
||||
ASK: 0,
|
||||
ASK_YES_NO: 1,
|
||||
ASK_YES_NO_YESFOREVER: 2,
|
||||
ASK_YES_NO_SAVE: 3,
|
||||
ASK_PASSWORD: 4,
|
||||
});
|
||||
|
||||
const ProblemState = Object.freeze({
|
||||
REPORTABLE: 0,
|
||||
REPORTING: 1,
|
||||
REPORTED: 2,
|
||||
UNREPORTABLE: 3,
|
||||
});
|
||||
|
||||
const client = cockpit.dbus("org.freedesktop.problems", { superuser: "try" });
|
||||
var reportd_client;
|
||||
|
||||
// For one-off fetches of properties to avoid setting up a cache for everything.
|
||||
function get_problem_properties(problem) {
|
||||
function executor(resolve, reject) {
|
||||
client.wait().then(() => resolve(client));
|
||||
}
|
||||
|
||||
return new Promise(executor)
|
||||
.then(() => client.call(problem.path,
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"GetAll", ["org.freedesktop.Problems2.Entry"]));
|
||||
}
|
||||
|
||||
class FAFWorkflowRow extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
problemState: ProblemState.REPORTABLE,
|
||||
process: null,
|
||||
reportLinks: [],
|
||||
message: "",
|
||||
};
|
||||
|
||||
this._onCancelButtonClick = this._onCancelButtonClick.bind(this);
|
||||
this._onReportButtonClick = this._onReportButtonClick.bind(this);
|
||||
this.updateStatusFromBus = this.updateStatusFromBus.bind(this);
|
||||
|
||||
this.updateStatusFromBus();
|
||||
}
|
||||
|
||||
updateStatusFromBus() {
|
||||
get_problem_properties(this.props.problem)
|
||||
.catch(exception => {
|
||||
this.setState({ problemState: ProblemState.UNREPORTABLE });
|
||||
|
||||
console.error(cockpit.format("Getting properties for problem $0 failed: $1", this.props.problem.path, exception));
|
||||
})
|
||||
.then((properties) => {
|
||||
if (!properties) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!properties[0].CanBeReported.v) {
|
||||
this.setState({ problemState: ProblemState.UNREPORTABLE });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const reportLinks = [];
|
||||
let reported = false;
|
||||
|
||||
for (const report of properties[0].Reports.v) {
|
||||
if (report[0] === "ABRT Server") {
|
||||
if ("URL" in report[1]) {
|
||||
reportLinks.push(report[1].URL.v.v);
|
||||
}
|
||||
reported = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (reported) {
|
||||
this.setState({
|
||||
problemState: ProblemState.REPORTED,
|
||||
reportLinks: reportLinks,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onCancelButtonClick(event) {
|
||||
this.state.process.close("canceled");
|
||||
}
|
||||
|
||||
_onReportButtonClick(event) {
|
||||
this.setState({ problemState: ProblemState.UNREPORTABLE });
|
||||
|
||||
const process = cockpit.spawn(["reporter-ureport", "-d", this.props.problem.ID],
|
||||
{
|
||||
err: "out",
|
||||
superuser: "true",
|
||||
})
|
||||
.stream((data) => this.setState({ message: data, }))
|
||||
.then(() => this.setState({ problemState: ProblemState.REPORTED, }))
|
||||
.catch(exception => {
|
||||
this.setState({ problemState: ProblemState.REPORTABLE, });
|
||||
|
||||
if (exception.exit_signal != null) {
|
||||
console.error(cockpit.format("reporter-ureport was killed with signal $0", exception.exit_signal));
|
||||
}
|
||||
})
|
||||
.finally(() => this.updateStatusFromBus());
|
||||
|
||||
this.setState({
|
||||
problemState: ProblemState.REPORTING,
|
||||
process: process,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <WorkflowRow label={_("Report to ABRT Analytics")}
|
||||
message={this.state.message}
|
||||
onCancelButtonClick={this._onCancelButtonClick}
|
||||
onReportButtonClick={this._onReportButtonClick}
|
||||
problemState={this.state.problemState}
|
||||
reportLinks={this.state.reportLinks} />;
|
||||
}
|
||||
}
|
||||
|
||||
class BusWorkflowRow extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
label: this.props.workflow[1],
|
||||
message: "",
|
||||
problemState: ProblemState.REPORTABLE,
|
||||
reportLinks: [],
|
||||
task: null,
|
||||
};
|
||||
|
||||
this._createTask = this._createTask.bind(this);
|
||||
this._onCancelButtonClick = this._onCancelButtonClick.bind(this);
|
||||
this._onCreateTask = this._onCreateTask.bind(this);
|
||||
this._onReportButtonClick = this._onReportButtonClick.bind(this);
|
||||
this.updateStatusFromBus = this.updateStatusFromBus.bind(this);
|
||||
|
||||
this.updateStatusFromBus();
|
||||
}
|
||||
|
||||
_createTask(client) {
|
||||
return client.call("/org/freedesktop/reportd/Service",
|
||||
"org.freedesktop.reportd.Service", "CreateTask",
|
||||
[this.props.workflow[0], this.props.problem.path])
|
||||
.then(result => this._onCreateTask(result[0], client));
|
||||
}
|
||||
|
||||
_onCancelButtonClick(event) {
|
||||
this.state.task.Cancel();
|
||||
}
|
||||
|
||||
_onCreateTask(object_path, client) {
|
||||
const task_proxy = client.proxy("org.freedesktop.reportd.Task", object_path);
|
||||
|
||||
task_proxy
|
||||
.wait()
|
||||
.then((object_path) => {
|
||||
task_proxy.addEventListener("changed", (event, data) => {
|
||||
switch (data.Status) {
|
||||
case TaskState.RUNNING:
|
||||
// To avoid a needless D-Bus round trip.
|
||||
return;
|
||||
case TaskState.CANCELED:
|
||||
this.setState({ message: _("Reporting was canceled"), });
|
||||
// falls through
|
||||
case TaskState.ERROR:
|
||||
this.setState({ problemState: ProblemState.REPORTABLE, });
|
||||
break;
|
||||
case TaskState.COMPLETED:
|
||||
this.setState({ problemState: ProblemState.REPORTED, });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.updateStatusFromBus();
|
||||
});
|
||||
task_proxy.addEventListener("Prompt", (event, object_path, message, type) => {
|
||||
this.setState({ message: _("Waiting for input…") });
|
||||
const task_prompt = client.proxy("org.freedesktop.reportd.Task.Prompt", object_path);
|
||||
const props = {
|
||||
body: <p>{message}</p>,
|
||||
};
|
||||
const footerProps = {
|
||||
actions: [],
|
||||
cancel_clicked: () => {
|
||||
task_proxy.Cancel();
|
||||
},
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case PromptType.ASK:
|
||||
case PromptType.ASK_PASSWORD:
|
||||
props.body = (
|
||||
<div>
|
||||
<p>{message}</p>
|
||||
<input className="full-width"
|
||||
ref={(input) => { this.input = input }}
|
||||
type={type == PromptType.ASK_PASSWORD ? "password" : "text"} />
|
||||
</div>
|
||||
);
|
||||
footerProps.actions.push(
|
||||
{
|
||||
caption: _("Send"),
|
||||
clicked: () => {
|
||||
return task_prompt.wait().then(() => {
|
||||
task_prompt.Input = this.input.value;
|
||||
task_prompt.Commit();
|
||||
});
|
||||
},
|
||||
style: "primary",
|
||||
}
|
||||
);
|
||||
break;
|
||||
case PromptType.ASK_YES_NO_YESFOREVER:
|
||||
case PromptType.ASK_YES_NO:
|
||||
case PromptType.ASK_YES_NO_SAVE:
|
||||
footerProps.actions.push(
|
||||
{
|
||||
caption: _("Yes"),
|
||||
clicked: () => {
|
||||
return task_prompt.wait().then(() => {
|
||||
task_prompt.Response = true;
|
||||
task_prompt.Commit();
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
caption: _("No"),
|
||||
clicked: (callback) => {
|
||||
return task_prompt.wait().then(() => {
|
||||
task_prompt.Response = false;
|
||||
task_prompt.Commit();
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
show_modal_dialog(props, footerProps);
|
||||
});
|
||||
task_proxy.addEventListener("Progress", (event, message) => {
|
||||
if (/^\.+$/.exec(message) === null) {
|
||||
// abrt-retrace-client starts printing dots if the last message it receives is repeated
|
||||
this.setState({ message: message, });
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ task: task_proxy, });
|
||||
|
||||
task_proxy.Start().catch(ex => {
|
||||
/* GLib encodes errors for transport over the wire,
|
||||
* but we don’t have a good way of decoding them without calling into GIO.
|
||||
*
|
||||
* https://developer.gnome.org/gio/stable/gio-GDBusError.html#g-dbus-error-encode-gerror
|
||||
*
|
||||
* 19 is G_IO_ERROR_CANCELLED. No need to handle user cancellations.
|
||||
*/
|
||||
if (/Code19/.exec(ex.name) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(cockpit.format("reportd task for workflow $0 did not finish: $1", this.props.workflow[0], (ex.problem || ex.message)));
|
||||
this.setState({ message: _("Reporting failed") });
|
||||
});
|
||||
})
|
||||
.catch(ex => console.error(cockpit.format("Setting up a D-Bus proxy for $0 failed: $1", object_path, ex)));
|
||||
}
|
||||
|
||||
_onReportButtonClick(event) {
|
||||
this.setState({ problemState: ProblemState.UNREPORTABLE });
|
||||
|
||||
this.setState({
|
||||
message: _("Waiting to start…"),
|
||||
problemState: ProblemState.REPORTING,
|
||||
});
|
||||
|
||||
reportd_client
|
||||
.wait()
|
||||
.catch(exception => console.error(cockpit.format("Channel for reportd D-Bus client closed: $0", exception.problem || exception.message)))
|
||||
.then(() => this._createTask(reportd_client))
|
||||
.catch(exception => {
|
||||
const message = cockpit.format("reportd task could not be created: $0", (exception.problem || exception.message));
|
||||
|
||||
this.setState({
|
||||
message: message,
|
||||
problemState: ProblemState.REPORTABLE,
|
||||
});
|
||||
console.error(message);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <WorkflowRow label={this.state.label}
|
||||
message={this.state.message}
|
||||
onCancelButtonClick={this._onCancelButtonClick}
|
||||
onReportButtonClick={this._onReportButtonClick}
|
||||
problemState={this.state.problemState}
|
||||
reportLinks={this.state.reportLinks} />;
|
||||
}
|
||||
|
||||
updateStatusFromBus() {
|
||||
const on_get_properties = properties => {
|
||||
if (!properties[0].CanBeReported.v) {
|
||||
this.setState({ problemState: ProblemState.UNREPORTABLE });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const reportLinks = [];
|
||||
let reported = false;
|
||||
|
||||
for (const report of properties[0].Reports.v) {
|
||||
if (!("WORKFLOW" in report[1])) {
|
||||
continue;
|
||||
}
|
||||
if (this.props.workflow[0] !== report[1].WORKFLOW.v.v) {
|
||||
continue;
|
||||
}
|
||||
if (report[0] === "ABRT Server" || report[0] === "uReport") {
|
||||
continue;
|
||||
}
|
||||
if ("URL" in report[1]) {
|
||||
reportLinks.push(report[1].URL.v.v);
|
||||
}
|
||||
|
||||
reported = true;
|
||||
}
|
||||
|
||||
if (reported) {
|
||||
this.setState({
|
||||
problemState: ProblemState.REPORTED,
|
||||
reportLinks: reportLinks,
|
||||
});
|
||||
}
|
||||
};
|
||||
const on_get_properties_rejected = exception => {
|
||||
this.setState({ problemState: ProblemState.UNREPORTABLE });
|
||||
|
||||
console.error(cockpit.format("Getting properties for problem $0 failed: $1", this.props.problem.path, exception));
|
||||
};
|
||||
|
||||
get_problem_properties(this.props.problem).then(on_get_properties, on_get_properties_rejected);
|
||||
}
|
||||
}
|
||||
|
||||
function WorkflowRow(props) {
|
||||
let status = props.message;
|
||||
|
||||
if (props.problemState === ProblemState.REPORTED) {
|
||||
const icon = <i className="fa fa-external-link" aria-hidden="true" />;
|
||||
|
||||
if (props.reportLinks.length === 1) {
|
||||
status = (
|
||||
<a href={props.reportLinks[0]} rel="noopener noreferrer" target="_blank">
|
||||
{_("View report")} {icon}
|
||||
</a>
|
||||
);
|
||||
} else if (props.reportLinks.length > 1) {
|
||||
const reportLinks = props.reportLinks.map((reportLink, index) => [
|
||||
index > 0 && ", ",
|
||||
<a key={index.toString()} href={reportLink} rel="noopener noreferrer" target="_blank">
|
||||
{index + 1} {icon}
|
||||
</a>
|
||||
]);
|
||||
status = <p>{_("Reports:")} {reportLinks}</p>;
|
||||
} else {
|
||||
status = _("Reported; no links available");
|
||||
}
|
||||
}
|
||||
|
||||
let button = null;
|
||||
if (props.problemState === ProblemState.REPORTING) {
|
||||
button = (
|
||||
<button key={"cancel_" + props.label}
|
||||
className="btn btn-danger"
|
||||
onClick={props.onCancelButtonClick}>
|
||||
{_("Cancel")}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
<button key={"report_" + props.label}
|
||||
className="btn btn-primary"
|
||||
disabled={props.problemState !== ProblemState.REPORTABLE}
|
||||
onClick={props.problemState === ProblemState.REPORTABLE ? props.onReportButtonClick : undefined}>
|
||||
{_("Report")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{props.label}</td>
|
||||
<td>
|
||||
{props.problemState === ProblemState.REPORTING && <span className="spinner spinner-xs" />}
|
||||
{status}
|
||||
</td>
|
||||
<td>{button}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
class ReportingTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
workflows: [],
|
||||
};
|
||||
|
||||
this.getWorkflows = this.getWorkflows.bind(this);
|
||||
|
||||
reportd_client
|
||||
.wait()
|
||||
.then(() => this.getWorkflows(reportd_client))
|
||||
.catch(exception => console.error(cockpit.format("Channel for reportd D-Bus client closed: $0", exception.problem || exception.message)));
|
||||
}
|
||||
|
||||
getWorkflows(client) {
|
||||
client.call("/org/freedesktop/reportd/Service", "org.freedesktop.reportd.Service", "GetWorkflows", [this.props.problem.path])
|
||||
.then((args, options) => this.setState({ workflows: args[0], }))
|
||||
.catch(exception => console.error(cockpit.format("Failed to get workflows for problem $0: $1", this.props.problem.path, (exception.problem || exception.message))));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table className="panel panel-default reporting-table table">
|
||||
<caption>
|
||||
<h3>{_("Crash Reporting")}</h3>
|
||||
</caption>
|
||||
<tbody>
|
||||
<FAFWorkflowRow problem={this.props.problem} />
|
||||
{
|
||||
this.state.workflows.map((workflow, index) => [
|
||||
<BusWorkflowRow key={index.toString()}
|
||||
problem={this.props.problem}
|
||||
workflow={workflow} />
|
||||
])
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function init_reporting(problem, container) {
|
||||
const permission = cockpit.permission({ admin: true });
|
||||
const on_permission_changed = () => {
|
||||
// reportd may use ABRT API that requires authorization, but it cannot
|
||||
// be given using polkit, as the calling process will always be reportd
|
||||
// and not cockpit-bridge. However, UID 0 is always authorized, hence
|
||||
// the spawning of the system service and using the system bus here.
|
||||
//
|
||||
// TODO: Only use system bus when reportd is merged into ABRT
|
||||
// (https://github.com/abrt/reportd/issues/8).
|
||||
reportd_client = cockpit.dbus("org.freedesktop.reportd",
|
||||
{
|
||||
bus: permission.allowed ? "system" : "session",
|
||||
track: true,
|
||||
});
|
||||
|
||||
permission.close();
|
||||
permission.removeEventListener("changed", on_permission_changed);
|
||||
|
||||
ReactDOM.render(<ReportingTable problem={problem} />, container);
|
||||
};
|
||||
|
||||
permission.addEventListener("changed", on_permission_changed);
|
||||
}
|
|
@ -583,7 +583,7 @@ CMD ["/bin/sh"]
|
|||
b.enter_page("/system/logs")
|
||||
|
||||
# check for abrtd service in text
|
||||
b.wait_in_text("#journal-entry-id", "sleep")
|
||||
b.wait_in_text("#journal-entry-heading", "sleep")
|
||||
b.wait_in_text("#journal-entry-fields", "abrtd.service")
|
||||
|
||||
|
||||
|
|
|
@ -41,6 +41,13 @@ class TestJournal(MachineCase):
|
|||
else:
|
||||
browser.click(button_text_selector)
|
||||
|
||||
def crash(self):
|
||||
m = self.machine
|
||||
|
||||
m.execute("ulimit -c unlimited")
|
||||
sleep = m.spawn("sleep 1m", "sleep.log")
|
||||
m.execute("kill -SEGV %d" % (sleep))
|
||||
|
||||
def testBasic(self):
|
||||
b = self.browser
|
||||
b.wait_timeout(120)
|
||||
|
@ -380,12 +387,11 @@ ExecStart=/bin/sh -c 'for s in $(seq 10); do echo SLOW; sleep 0.1; done; sleep 1
|
|||
def testAbrtSegv(self):
|
||||
self.allow_core_dumps = True
|
||||
b = self.browser
|
||||
m = self.machine
|
||||
|
||||
self.crash()
|
||||
|
||||
self.login_and_go("/system/logs")
|
||||
|
||||
m.execute("ulimit -c unlimited; timeout --signal=SEGV 0.1s sleep 5s || true")
|
||||
|
||||
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in %s')" % self.crash_fn
|
||||
|
||||
self.select_from_dropdown(b, "#journal-prio-menu", "Critical and above")
|
||||
|
@ -420,30 +426,33 @@ ExecStart=/bin/sh -c 'for s in $(seq 10); do echo SLOW; sleep 0.1; done; sleep 1
|
|||
def testAbrtDelete(self):
|
||||
self.allow_core_dumps = True
|
||||
b = self.browser
|
||||
m = self.machine
|
||||
|
||||
# A bit of a race might happen if you delete the journal entry whilst
|
||||
# the reporting code is doing its thing.
|
||||
self.allow_browser_errors("Failed to get workflows for problem /org/freedesktop/Problems2/Entry/.*:.*")
|
||||
self.allow_browser_errors("Getting properties for problem /org/freedesktop/Problems2/Entry/.* failed:.*")
|
||||
|
||||
self.crash()
|
||||
|
||||
self.login_and_go("/system/logs")
|
||||
|
||||
m.execute("ulimit -c unlimited; timeout --signal=SEGV 0.1s sleep 5s || true")
|
||||
|
||||
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in %s')" % self.crash_fn
|
||||
b.click(sel)
|
||||
|
||||
b.wait_in_text("#journal-entry-id", "sleep")
|
||||
b.wait_in_text("#journal-entry-heading", "sleep")
|
||||
sel = "#journal-entry-fields .nav .btn-danger"
|
||||
b.click(sel)
|
||||
|
||||
b.wait_visible("#journal-box")
|
||||
b.wait_in_text('#journal-box', "crashed in " + self.crash_fn)
|
||||
|
||||
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in %s')" % self.crash_fn
|
||||
b.click(sel)
|
||||
|
||||
b.wait_in_text("#journal-entry-id", "sleep")
|
||||
b.wait_in_text("#journal-entry-heading", "sleep")
|
||||
# details view should hide log view
|
||||
b.wait_not_visible('.cockpit-log-panel')
|
||||
b.wait_present("#journal-entry-message:contains('crashed in %s')" % self.crash_fn)
|
||||
b.wait_not_present("#journal-entry-fields .nav")
|
||||
b.wait_not_present("#journal-entry-fields .nav .btn-danger")
|
||||
|
||||
@skipImage("ABRT does not work on i386", "fedora-i386")
|
||||
@skipImage("ABRT not available", "debian-stable", "debian-testing", "ubuntu-stable",
|
||||
|
@ -455,54 +464,139 @@ ExecStart=/bin/sh -c 'for s in $(seq 10); do echo SLOW; sleep 0.1; done; sleep 1
|
|||
b = self.browser
|
||||
m = self.machine
|
||||
|
||||
# We restart the reportd service,
|
||||
# which causes the D-Bus proxy to spit out an error when it tries to
|
||||
# query its managed objects.
|
||||
self.allow_journal_messages('.*Remote peer disconnected')
|
||||
|
||||
m.upload(["verify/files/mock-bugzilla-server.py"], "/tmp/")
|
||||
m.spawn("setsid /tmp/mock-bugzilla-server.py", "mock-bugzilla.log")
|
||||
m.execute("echo 'BugzillaURL=http://localhost:8080' >> /etc/libreport/plugins/bugzilla.conf")
|
||||
|
||||
# start mock FAF server
|
||||
m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
|
||||
m.spawn("setsid /tmp/mock-faf-server.py", "mock-faf.log")
|
||||
m.execute("echo 'URL=http://localhost:12345' >> /etc/libreport/plugins/ureport.conf")
|
||||
|
||||
self.crash()
|
||||
|
||||
self.login_and_go("/system/logs")
|
||||
|
||||
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in __nanosleep()')"
|
||||
b.click(sel)
|
||||
|
||||
sel = "table.reporting-table tr:first-child .btn:contains('Report')"
|
||||
b.click(sel)
|
||||
|
||||
sel = "table.reporting-table tr:first-child a[href$='/reports/bthash/123deadbeef'"
|
||||
b.wait_present(sel)
|
||||
|
||||
# "Unreport" the problem to test reporting unknown problem
|
||||
m.execute('find /var/spool/abrt -name "reported_to" -or -name "ureports_counter" | xargs rm')
|
||||
# The service also needs to be restarted, because the daemon maintains
|
||||
# its own cache that interferes with resetting the state.
|
||||
m.execute('systemctl restart reportd')
|
||||
# We have no network access.
|
||||
m.execute("sed -i 's|abrt-action-perform-ccpp-analysis|true|' /etc/libreport/events.d/ccpp_event.conf")
|
||||
# reporter-bugzilla will not be run without a duphash file present.
|
||||
m.execute("find /var/spool/abrt -depth -maxdepth 1 | head -1 | xargs -I {} cp '{}/uuid' '{}/duphash'")
|
||||
|
||||
b.reload()
|
||||
b.enter_page("/system/logs")
|
||||
|
||||
sel = "table.reporting-table tr:nth-child(2) button:contains('Report')"
|
||||
b.click(sel)
|
||||
|
||||
test_user = 'correcthorsebatterystaple'
|
||||
|
||||
for purpose in ['text', 'password']:
|
||||
b.wait_visible("#cockpit_modal_dialog .modal-content input[type='%s']" % (purpose))
|
||||
|
||||
sel = "#cockpit_modal_dialog .modal-content input"
|
||||
b.set_val(sel, test_user)
|
||||
|
||||
sel = "#cockpit_modal_dialog .modal-footer button:contains('Send')"
|
||||
b.click(sel)
|
||||
|
||||
sel = "#cockpit_modal_dialog .modal-content p:contains('Password')"
|
||||
b.wait_not_present(sel)
|
||||
|
||||
sel = "#cockpit_modal_dialog .modal-footer button:contains('No')"
|
||||
b.click(sel)
|
||||
|
||||
sel = "table.reporting-table tr:nth-child(2) a[href='https://bugzilla.example.com/show_bug.cgi?id=123456']"
|
||||
b.wait_present(sel)
|
||||
|
||||
@skipImage("ABRT does not work on i386", "fedora-i386")
|
||||
@skipImage("ABRT not available", "debian-stable", "debian-testing", "ubuntu-stable",
|
||||
"ubuntu-1804", "fedora-coreos", "rhel-8-1", "rhel-8-1-distropkg", "rhel-8-2")
|
||||
def testAbrtReportCancel(self):
|
||||
self.allow_core_dumps = True
|
||||
|
||||
b = self.browser
|
||||
m = self.machine
|
||||
|
||||
m.upload(["verify/files/mock-bugzilla-server.py"], "/tmp/")
|
||||
m.spawn("setsid /tmp/mock-bugzilla-server.py", "mock-bugzilla.log")
|
||||
m.execute("echo 'BugzillaURL=http://localhost:8080' >> /etc/libreport/plugins/bugzilla.conf")
|
||||
|
||||
# start mock FAF server
|
||||
m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
|
||||
m.spawn("setsid /tmp/mock-faf-server.py", "mock-faf.log")
|
||||
m.execute("echo 'URL=http://localhost:12345' >> /etc/libreport/plugins/ureport.conf")
|
||||
|
||||
self.crash()
|
||||
|
||||
self.login_and_go("/system/logs")
|
||||
|
||||
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in __nanosleep()')"
|
||||
b.click(sel)
|
||||
|
||||
# Something long-running to not pop up an unexpected dialog.
|
||||
m.execute("sed -i 's|abrt-action-perform-ccpp-analysis|echo Cancel me; sleep 5m|' /etc/libreport/events.d/ccpp_event.conf")
|
||||
|
||||
sel = "table.reporting-table tr:nth-child(2) button:contains('Report')"
|
||||
b.click(sel)
|
||||
|
||||
sel = "table.reporting-table tr:nth-child(2) td:contains('Cancel me')"
|
||||
b.wait_visible(sel)
|
||||
|
||||
sel = "table.reporting-table tr:nth-child(2) button:contains('Cancel')"
|
||||
b.click(sel)
|
||||
|
||||
sel = "table.reporting-table tr:nth-child(2) td:contains('Reporting was canceled')"
|
||||
b.wait_present(sel)
|
||||
|
||||
@skipImage("ABRT does not work on i386", "fedora-i386")
|
||||
@skipImage("ABRT not available", "debian-stable", "debian-testing", "ubuntu-stable",
|
||||
"ubuntu-1804", "fedora-coreos", "rhel-8-1", "rhel-8-1-distropkg", "rhel-8-2")
|
||||
def testAbrtReportNoReportd(self):
|
||||
self.allow_core_dumps = True
|
||||
|
||||
b = self.browser
|
||||
m = self.machine
|
||||
|
||||
self.allow_browser_errors("Channel for reportd D-Bus client closed: .*")
|
||||
|
||||
# start mock FAF server
|
||||
m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
|
||||
m.execute("setsid /tmp/mock-faf-server.py >/tmp/mock-faf.log 2>&1 &")
|
||||
m.execute("echo 'URL=http://localhost:12345' >> /etc/libreport/plugins/ureport.conf")
|
||||
|
||||
self.login_and_go("/system/logs")
|
||||
m.execute("systemctl mask --now reportd")
|
||||
|
||||
m.execute("ulimit -c unlimited; echo 'sleep 42m &' > slp; chmod u+x slp")
|
||||
m.execute("./slp; pkill -x -SEGV sleep")
|
||||
self.crash()
|
||||
|
||||
self.login_and_go("/system/logs")
|
||||
|
||||
sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('crashed in __nanosleep()')"
|
||||
b.click(sel)
|
||||
|
||||
# Wait until loaded (when delete button is loaded, all is loaded)
|
||||
sel = "#journal-entry-fields .nav .btn-danger"
|
||||
b.wait_visible(sel)
|
||||
|
||||
sel = "#journal-entry-fields .nav .btn-primary:contains('Report')"
|
||||
sel = "table.reporting-table tr:first-child .btn:contains('Report')"
|
||||
b.click(sel)
|
||||
|
||||
# jQuery magic on this page updates the whole frame for changing to "Reported" state
|
||||
b.expect_load_frame("cockpit1:localhost/system/logs")
|
||||
sel = "#journal-entry-fields .nav .problem-btn:contains('Reported')"
|
||||
b.wait_visible(sel)
|
||||
self.assertIn("/reports/bthash/123deadbeef", b.attr(sel, 'href'))
|
||||
|
||||
# "Unreport" the problem to test reporting unknown problem
|
||||
m.execute('find /var/spool/abrt -name "reported_to" | xargs rm')
|
||||
|
||||
b.reload()
|
||||
b.enter_page("/system/logs")
|
||||
|
||||
sel = "#journal-entry-fields"
|
||||
sel = "table.reporting-table tr:first-child a[href$='/reports/bthash/123deadbeef'"
|
||||
b.wait_present(sel)
|
||||
|
||||
self.allow_journal_messages('.*This problem has already been reported.')
|
||||
self.allow_journal_messages('.*http://localhost:12345/reports/42/')
|
||||
self.allow_journal_messages('.*https://bugzilla.example.com/show_bug.cgi\?id=123456')
|
||||
|
||||
sel = "#journal-entry-fields .nav .btn-primary:contains('Report')"
|
||||
b.click(sel)
|
||||
|
||||
# jQuery magic on this page updates the whole frame for changing to "Reported" state
|
||||
b.expect_load_frame("cockpit1:localhost/system/logs")
|
||||
sel = "#journal-entry-fields .nav .problem-btn:contains('Reported')"
|
||||
b.wait_visible(sel)
|
||||
self.assertIn("/reports/bthash/123deadbeef", b.attr(sel, 'href'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_main()
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
|
||||
|
||||
|
||||
class RequestHandler(SimpleXMLRPCRequestHandler):
|
||||
rpc_paths = ('/xmlrpc.cgi')
|
||||
|
||||
|
||||
with SimpleXMLRPCServer(('', 8080), requestHandler=RequestHandler) as server:
|
||||
class Bug:
|
||||
@server.register_function(name='Bug.add_attachment')
|
||||
def add_attachment(self):
|
||||
return {'ids': [42]}
|
||||
|
||||
@server.register_function(name='Bug.create')
|
||||
def create(self):
|
||||
return {'id': 42}
|
||||
|
||||
@server.register_function(name='Bug.search')
|
||||
def search(self):
|
||||
return {'bugs': []}
|
||||
|
||||
@server.register_function(name='Bug.update')
|
||||
def update(self):
|
||||
return {'bugs': []}
|
||||
|
||||
class Bugzilla:
|
||||
@server.register_function(name='Bugzilla.version')
|
||||
def version(self):
|
||||
return {'version': '42'}
|
||||
|
||||
class User:
|
||||
@server.register_function(name='User.login')
|
||||
def login(self):
|
||||
return {'id': 0, 'token': '70k3n'}
|
||||
|
||||
@server.register_function(name='User.logout')
|
||||
def logout(self):
|
||||
return {}
|
||||
|
||||
server.register_instance(Bugzilla())
|
||||
|
||||
server.serve_forever()
|
|
@ -9,6 +9,29 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
|||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST_attach(self):
|
||||
self.wfile.write(json.dumps({'result': True}).encode("UTF-8"))
|
||||
|
||||
def do_POST_new(self):
|
||||
response = {
|
||||
'bthash': '123deadbeef',
|
||||
'message': 'http://localhost:12345/reports/42/\nhttps://bugzilla.example.com/show_bug.cgi?id=123456',
|
||||
'reported_to': [
|
||||
{
|
||||
'type': 'url',
|
||||
'value': 'http://localhost:12345/reports/42/',
|
||||
'reporter': 'ABRT Server'
|
||||
},
|
||||
{
|
||||
'type': 'url',
|
||||
'value': 'https://bugzilla.example.com/show_bug.cgi?id=123456',
|
||||
'reporter': 'Bugzilla'
|
||||
}
|
||||
],
|
||||
'result': False
|
||||
}
|
||||
self.wfile.write(json.dumps(response, indent=2).encode('UTF-8'))
|
||||
|
||||
def do_POST(self):
|
||||
form = cgi.FieldStorage(
|
||||
fp=self.rfile,
|
||||
|
@ -32,27 +55,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||
sys.stderr.write('Received invalid JSON data:\n{0}\n'.format(json_str))
|
||||
return
|
||||
|
||||
response = {
|
||||
'bthash': '123deadbeef',
|
||||
'message': 'http://localhost:12345/reports/42/\nhttps://bugzilla.example.com/show_bug.cgi?id=123456',
|
||||
'reported_to': [
|
||||
{
|
||||
'type': 'url',
|
||||
'value': 'http://localhost:12345/reports/42/',
|
||||
'reporter': 'ABRT Server'
|
||||
},
|
||||
{
|
||||
'type': 'url',
|
||||
'value': 'https://bugzilla.example.com/show_bug.cgi?id=123456',
|
||||
'reporter': 'Bugzilla'
|
||||
}
|
||||
],
|
||||
'result': next(Handler.known)
|
||||
}
|
||||
self.wfile.write(json.dumps(response, indent=2).encode('UTF-8'))
|
||||
if self.path == '/reports/attach/':
|
||||
self.do_POST_attach()
|
||||
elif self.path == '/reports/new/':
|
||||
self.do_POST_new()
|
||||
|
||||
|
||||
PORT = 12345
|
||||
Handler.known = [True, False].__iter__()
|
||||
httpd = HTTPServer(("", PORT), Handler)
|
||||
httpd.serve_forever()
|
||||
|
|
|
@ -369,6 +369,11 @@ Recommends: setroubleshoot-server >= 3.3.3
|
|||
Provides: cockpit-selinux = %{version}-%{release}
|
||||
Provides: cockpit-sosreport = %{version}-%{release}
|
||||
%endif
|
||||
%if 0%{?fedora} >= 29
|
||||
# 0.7.0 (actually) supports task cancellation.
|
||||
# 0.7.1 fixes tasks never announcing completion.
|
||||
Recommends: (reportd >= 0.7.1 if abrt)
|
||||
%endif
|
||||
# NPM modules which are also available as packages
|
||||
Provides: bundled(js-jquery) = %{npm-version:jquery}
|
||||
Provides: bundled(js-moment) = %{npm-version:moment}
|
||||
|
|
Loading…
Reference in New Issue