'));
- 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");
- $('
')
- .append($('
' +
- '' +
- '
'),
- $('
').text(message)
- )
- .insertAfter(".breadcrumb");
- tab.children(':last-child').replaceWith($(''));
- });
- });
- }
+ 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 = $('').append(
$('').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);
}
diff --git a/pkg/systemd/reporting.jsx b/pkg/systemd/reporting.jsx
new file mode 100644
index 000000000..2497c79fb
--- /dev/null
+++ b/pkg/systemd/reporting.jsx
@@ -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 .
+ */
+
+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 ;
+ }
+}
+
+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: {message} ,
+ };
+ const footerProps = {
+ actions: [],
+ cancel_clicked: () => {
+ task_proxy.Cancel();
+ },
+ };
+
+ switch (type) {
+ case PromptType.ASK:
+ case PromptType.ASK_PASSWORD:
+ props.body = (
+
+ {message}
+ { this.input = input }}
+ type={type == PromptType.ASK_PASSWORD ? "password" : "text"} />
+
+ );
+ 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 ;
+ }
+
+ 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 = ;
+
+ if (props.reportLinks.length === 1) {
+ status = (
+
+ {_("View report")} {icon}
+
+ );
+ } else if (props.reportLinks.length > 1) {
+ const reportLinks = props.reportLinks.map((reportLink, index) => [
+ index > 0 && ", ",
+
+ {index + 1} {icon}
+
+ ]);
+ status = {_("Reports:")} {reportLinks} ;
+ } else {
+ status = _("Reported; no links available");
+ }
+ }
+
+ let button = null;
+ if (props.problemState === ProblemState.REPORTING) {
+ button = (
+
+ );
+ } else {
+ button = (
+
+ );
+ }
+
+ return (
+ |
+ {props.label} |
+
+ {props.problemState === ProblemState.REPORTING && }
+ {status}
+ |
+ {button} |
+
+ );
+}
+
+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 (
+
+
+ {_("Crash Reporting")}
+
+
+
+ {
+ this.state.workflows.map((workflow, index) => [
+
+ ])
+ }
+
+
+ );
+ }
+}
+
+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(, container);
+ };
+
+ permission.addEventListener("changed", on_permission_changed);
+}
diff --git a/test/verify/check-docker b/test/verify/check-docker
index e599a2cd2..9365dfdaa 100755
--- a/test/verify/check-docker
+++ b/test/verify/check-docker
@@ -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")
diff --git a/test/verify/check-journal b/test/verify/check-journal
index fa4fb488b..0a24cc43d 100755
--- a/test/verify/check-journal
+++ b/test/verify/check-journal
@@ -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()
diff --git a/test/verify/files/mock-bugzilla-server.py b/test/verify/files/mock-bugzilla-server.py
new file mode 100755
index 000000000..2cbec976f
--- /dev/null
+++ b/test/verify/files/mock-bugzilla-server.py
@@ -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()
diff --git a/test/verify/files/mock-faf-server.py b/test/verify/files/mock-faf-server.py
index 99e65cdab..af4cf86fa 100755
--- a/test/verify/files/mock-faf-server.py
+++ b/test/verify/files/mock-faf-server.py
@@ -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()
diff --git a/tools/cockpit.spec b/tools/cockpit.spec
index 5d3ff13fa..67e03b7b1 100644
--- a/tools/cockpit.spec
+++ b/tools/cockpit.spec
@@ -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}