512 lines
20 KiB
JavaScript
512 lines
20 KiB
JavaScript
/*
|
||
* 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);
|
||
}
|