cockpit/pkg/systemd/reporting.jsx

512 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 dont 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);
}