<commit_before>/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2013 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/>.
 */

var $ = require("jquery");
var cockpit = require("cockpit");

var Mustache = require("mustache");
var plot = require("plot");
var service = require("service");

/* These add themselves to jQuery so just including is enough */
require("patterns");
require("bootstrap-datepicker/dist/js/bootstrap-datepicker");
require("bootstrap-combobox/js/bootstrap-combobox");

var shutdown = require("./shutdown");

var host_keys_script = require("raw!./ssh-list-host-keys.sh");

var _ = cockpit.gettext;
var C_ = cockpit.gettext;

var permission = cockpit.permission({ admin: true });
$(permission).on("changed", update_hostname_privileged);

function update_hostname_privileged() {
    $(".hostname-privileged").update_privileged(
        permission, cockpit.format(
            _("The user <b>$0</b> is not permitted to modify hostnames"),
            permission.user ? permission.user.name : '')
    );
}

function debug() {
    if (window.debugging == "all" || window.debugging == "system")
        console.debug.apply(console, arguments);
}

/* machine_info(address).done(function (info) { })
 *
 * Get information about the machine at ADDRESS.  The returned object
 * has these fields:
 *
 * memory  -  amount of physical memory
 */

var machine_info_promises = { };

function machine_info(address) {
    var pr = machine_info_promises[address];
    var dfd;
    if (!pr) {
        dfd = $.Deferred();
        machine_info_promises[address] = pr = dfd.promise();

        cockpit.spawn(["cat", "/proc/meminfo", "/proc/cpuinfo"]).
            done(function(text) {
                var info = { };
                var match = text.match(/MemTotal:[^0-9]*([0-9]+) [kK]B/);
                var total_kb = match && parseInt(match[1], 10);
                if (total_kb)
                    info.memory = total_kb*1024;

                info.cpus = 0;
                var re = new RegExp("^processor", "gm");
                while (re.test(text))
                    info.cpus += 1;
                dfd.resolve(info);
            }).
            fail(function() {
                dfd.reject();
            });
    }
    return pr;
}

function ServerTime() {
    var self = this;

    var client = cockpit.dbus('org.freedesktop.timedate1');
    var timedate = client.proxy();

    var time_offset = null;
    var remote_offset = null;

    self.timedate = timedate;

    self.timedate1_service = service.proxy("dbus-org.freedesktop.timedate1.service");
    self.timesyncd_service = service.proxy("systemd-timesyncd.service");

    /*
     * The time we return from here as its UTC time set to the
     * server time. This is the only way to get predictable
     * behavior and formatting of a Date() object in the absence of
     * IntlDateFormat and  friends.
     */
    Object.defineProperty(self, 'utc_fake_now', {
        enumerable: true,
        get: function get() {
            var offset = time_offset + remote_offset;
            return new Date(offset + (new Date()).valueOf());
        }
    });

    Object.defineProperty(self, 'now', {
        enumerable: true,
        get: function get() {
            return new Date(time_offset + (new Date()).valueOf());
        }
    });

    self.format = function format(and_time) {
        var string = self.utc_fake_now.toISOString();
        if (!and_time)
            return string.split('T')[0];
        var pos = string.lastIndexOf(':');
        if (pos !== -1)
            string = string.substring(0, pos);
        return string.replace('T', ' ');
    };

    self.updateInterval = window.setInterval(function() {
        $(self).triggerHandler("changed");
    }, 30000);

    self.wait = function wait() {
        if (remote_offset === null)
            return self.update();
        return cockpit.resolve();
    };

    self.update = function update() {
        return cockpit.spawn(["date", "+%s:%:z"], { err: "message" })
            .done(function(data) {
                var parts = data.trim().split(":").map(function(x) {
                    return parseInt(x, 10);
                });
                if (parts[1] < 0)
                    parts[2] = -(parts[2]);
                var timems = parts[0] * 1000;
                var offsetms = (parts[1] * 3600000) + parts[2] * 60000;
                var now = new Date();
                time_offset = (timems - now.valueOf());
                remote_offset = offsetms;
                $(self).triggerHandler("changed");
            })
            .fail(function(ex) {
                console.log("Couldn't calculate server time offset: " + cockpit.message(ex));
            });
    };

    self.change_time = function change_time(datestr, hourstr, minstr) {
        var dfd = $.Deferred();

        /*
         * The browser is brain dead when it comes to dates. But even if
         * it wasn't, or we loaded a library like moment.js, there is no
         * way to make sense of this date without a round trip to the
         * server ... the timezone is really server specific.
         */
        cockpit.spawn(["date", "--date=" + datestr + " " + hourstr + ":" + minstr, "+%s"])
            .fail(function(ex) {
                dfd.reject(ex);
            })
            .done(function(data) {
                var seconds = parseInt(data.trim(), 10);
                timedate.call('SetTime', [seconds * 1000 * 1000, false, true])
                    .fail(function(ex) {
                        dfd.reject(ex);
                    })
                    .done(function() {
                        self.update();
                        dfd.resolve();
                    });
            });

        return dfd;
    };

    self.poll_ntp_synchronized = function poll_ntp_synchronized() {
        client.call(timedate.path,
                    "org.freedesktop.DBus.Properties", "Get", [ "org.freedesktop.timedate1", "NTPSynchronized" ]).
            fail(function (error) {
                if (error.name != "org.freedesktop.DBus.Error.UnknownProperty" &&
                    error.problem != "not-found")
                    console.log("can't get NTPSynchronized property", error);
            }).
            done(function (result) {
                var ifaces = { "org.freedesktop.timedate1": { NTPSynchronized: result[0].v } };
                var data = { };
                data[timedate.path] = ifaces;
                client.notify(data);
            });
    };

    self.close = function close() {
        client.close();
    };

    self.update();
}

var change_systime_dialog;

PageServer.prototype = {
    _init: function() {
        this.id = "server";
        this.server_time = null;
        this.client = null;
        this.hostname_proxy = null;
    },

    getTitle: function() {
        return null;
    },

    setup: function() {
        var self = this;
        update_hostname_privileged();

        $('#shutdown-group [data-action]').on("click", function() {
            self.shutdown($(this).attr('data-action'));
        });

        $('#system-ostree-version-link').on('click', function () {
            cockpit.jump("/updates", cockpit.transport.host);
        });

        $('#system_information_hostname_button').on('click', function () {
            PageSystemInformationChangeHostname.client = self.client;
            $('#system_information_change_hostname').modal('show');
        });

        $('#system_information_systime_button').on('click', function () {
            change_systime_dialog.display(self.server_time);
        });

        self.server_time = new ServerTime();
        $(self.server_time).on("changed", function() {
            $('#system_information_systime_button').text(self.server_time.format(true));
        });

        self.ntp_status_tmpl = $("#ntp-status-tmpl").html();
        Mustache.parse(this.ntp_status_tmpl);

        self.ntp_status_icon_tmpl = $("#ntp-status-icon-tmpl").html();
        Mustache.parse(this.ntp_status_icon_tmpl);

        self.ssh_host_keys_tmpl = $("#ssh-host-keys-tmpl").html();
        Mustache.parse(this.ssh_host_keys_tmpl);

        $("#system_information_ssh_keys").on("show.bs.modal", function() {
            self.host_keys_show();
        });

        function update_ntp_status() {
            var $elt = $('#system_information_systime_ntp_status');

            if (!self.server_time.timedate.NTP) {
                $elt.hide();
                $elt.popover('hide');
                return;
            }

            $elt.show();

            var model = {
                Synched: self.server_time.timedate.NTPSynchronized,
                service: null
            };

            var timesyncd_server_regex = /.*time server (.*)\./i;

            var timesyncd_status = (self.server_time.timesyncd_service.state == "running" &&
                                    self.server_time.timesyncd_service.service &&
                                    self.server_time.timesyncd_service.service.StatusText);

            if (self.server_time.timesyncd_service.state == "running")
                model.service = "systemd-timesyncd.service";

            if (timesyncd_status) {
                var match = timesyncd_status.match(timesyncd_server_regex);
                if (match)
                    model.Server = match[1];
                else if (timesyncd_status != "Idle." && timesyncd_status !== "")
                    model.SubStatus = timesyncd_status;
            }

            var popover_html = Mustache.render(self.ntp_status_tmpl, model);
            if (popover_html != $elt.attr('data-content')) {
                $elt.attr("data-content", popover_html);
                // Refresh the popover if it is open
                if ($elt.data('bs.popover').tip().hasClass('in'))
                    $elt.popover('show');
            }

            var icon_html = Mustache.render(self.ntp_status_icon_tmpl, model);
            $elt.html(icon_html);
        }

        $('#system_information_systime_ntp_status').popover();

        $(self.server_time.timesyncd_service).on("changed", update_ntp_status);
        $(self.server_time.timedate).on("changed", update_ntp_status);
        update_ntp_status();

        /* NTPSynchronized needs to be polled so we just do that
         * always.
         */
        window.setInterval(function () {
            self.server_time.poll_ntp_synchronized();
        }, 5000);

        $('#server').on('click', "[data-goto-service]", function () {
            var service = $(this).attr("data-goto-service");
            cockpit.jump("/system/services/#/" + window.encodeURIComponent(service));
        });

        self.plot_controls = plot.setup_plot_controls($('#server'), $('#server-graph-toolbar'));

        var pmcd_service = service.proxy("pmcd");
        var pmlogger_service = service.proxy("pmlogger");
        var pmlogger_promise;

        $("#server-pmlogger-switch").on("change", function(ev) {
            var val = $(this).onoff('value');
            if (pmlogger_service.exists) {
                if (val) {
                    pmlogger_promise = cockpit.all(pmcd_service.enable(),
                           pmcd_service.start(),
                           pmlogger_service.enable(),
                           pmlogger_service.start()).
                        fail(function (error) {
                            console.warn("Enabling pmlogger failed", error);
                        });
                } else {
                    pmlogger_promise = cockpit.all(pmlogger_service.disable(),
                           pmlogger_service.stop()).
                        fail(function (error) {
                            console.warn("Disabling pmlogger failed", error);
                        });
                }
                pmlogger_promise.always(function() {
                    pmlogger_promise = null;
                    refresh_pmlogger_state();
                });
            }
        });

        function refresh_pmlogger_state() {
            if (!pmlogger_service.exists)
                $('#server-pmlogger-onoff-row').hide();
            else if (!pmlogger_promise) {
                $("#server-pmlogger-switch").onoff('value', pmlogger_service.enabled);
                $('#server-pmlogger-onoff-row').show();
            }
        }

        $(pmlogger_service).on('changed', refresh_pmlogger_state);
        refresh_pmlogger_state();
    },

    enter: function() {
        var self = this;

        var machine_id = cockpit.file("/etc/machine-id");
        machine_id.read().done(function (content) {
            $("#system_machine_id").text(content);
            $("#system_machine_id").attr("title", content);
        }).fail(function (ex) {
            console.error("Error reading machine id", ex);
        }).always(function () {
            machine_id.close();
        });

        self.ostree_client = cockpit.dbus('org.projectatomic.rpmostree1',
                                          {"superuser" : true});
        $(self.ostree_client).on("close", function () {
            self.ostree_client = null;
        });

        self.sysroot = self.ostree_client.proxy('org.projectatomic.rpmostree1.Sysroot',
                                                '/org/projectatomic/rpmostree1/Sysroot');
        $(self.sysroot).on("changed", $.proxy(this, "sysroot_changed"));

        self.client = cockpit.dbus('org.freedesktop.hostname1');
        self.hostname_proxy = self.client.proxy('org.freedesktop.hostname1',
                                     '/org/freedesktop/hostname1');
        self.kernel_hostname = null;

        var series;

        /* CPU graph */

        var cpu_data = {
            direct: [ "kernel.all.cpu.nice", "kernel.all.cpu.user", "kernel.all.cpu.sys" ],
            internal: [ "cpu.basic.nice", "cpu.basic.user", "cpu.basic.system" ],
            units: "millisec",
            derive: "rate",
            factor: 0.1  // millisec / sec -> percent
        };

        var cpu_options = plot.plot_simple_template();
        $.extend(cpu_options.yaxis, { tickFormatter: function(v) { return v.toFixed(0); },
                                      max: 100
                                    });
        self.cpu_plot = plot.plot($("#server_cpu_graph"), 300);
        self.cpu_plot.set_options(cpu_options);
        series = self.cpu_plot.add_metrics_sum_series(cpu_data, { });

        /* Memory graph */

        var memory_data = {
            direct: [ "mem.util.used" ],
            internal: [ "memory.used" ],
            units: "bytes"
        };

        var memory_options = plot.plot_simple_template();
        $.extend(memory_options.yaxis, { ticks: plot.memory_ticks,
                                         tickFormatter: plot.format_bytes_tick_no_unit
                                       });
        memory_options.setup_hook = function memory_setup_hook(pl) {
            var axes = pl.getAxes();
            $('#server_memory_unit').text(plot.bytes_tick_unit(axes.yaxis));
        };

        self.memory_plot = plot.plot($("#server_memory_graph"), 300);
        self.memory_plot.set_options(memory_options);
        series = self.memory_plot.add_metrics_sum_series(memory_data, { });

        /* Network graph */

        var network_data = {
            direct: [ "network.interface.total.bytes" ],
            internal: [ "network.all.tx", "network.all.rx" ],
            units: "bytes",
            derive: "rate"
        };

        var network_options = plot.plot_simple_template();
        $.extend(network_options.yaxis, { tickFormatter: plot.format_bits_per_sec_tick_no_unit
                                        });
        network_options.setup_hook = function network_setup_hook(pl) {
            var axes = pl.getAxes();
            if (axes.yaxis.datamax < 100000)
                axes.yaxis.options.max = 100000;
            else
                axes.yaxis.options.max = null;
            axes.yaxis.options.min = 0;

            $('#server_network_traffic_unit').text(plot.bits_per_sec_tick_unit(axes.yaxis));
        };

        self.network_plot = plot.plot($("#server_network_traffic_graph"), 300);
        self.network_plot.set_options(network_options);
        series = self.network_plot.add_metrics_sum_series(network_data, { });

        /* Disk IO graph */

        var disk_data = {
            direct: [ "disk.all.total_bytes" ],
            internal: [ "disk.all.read", "disk.all.written" ],
            units: "bytes",
            derive: "rate"
        };

        var disk_options = plot.plot_simple_template();
        $.extend(disk_options.yaxis, { ticks: plot.memory_ticks,
                                       tickFormatter: plot.format_bytes_per_sec_tick_no_unit
                                     });
        disk_options.setup_hook = function disk_setup_hook(pl) {
            var axes = pl.getAxes();
            if (axes.yaxis.datamax < 100000)
                axes.yaxis.options.max = 100000;
            else
                axes.yaxis.options.max = null;
            axes.yaxis.options.min = 0;

            $('#server_disk_io_unit').text(plot.bytes_per_sec_tick_unit(axes.yaxis));
        };

        self.disk_plot = plot.plot($("#server_disk_io_graph"), 300);
        self.disk_plot.set_options(disk_options);
        series = self.disk_plot.add_metrics_sum_series(disk_data, { });

        machine_info().
            done(function (info) {
                cpu_options.yaxis.max = info.cpus * 100;
                self.cpu_plot.set_options(cpu_options);
                memory_options.yaxis.max = info.memory;
                self.memory_plot.set_options(memory_options);
            });

        self.plot_controls.reset([ self.cpu_plot, self.memory_plot, self.network_plot, self.disk_plot ]);

        $(window).on('resize.server', function () {
            self.cpu_plot.resize();
            self.memory_plot.resize();
            self.network_plot.resize();
            self.disk_plot.resize();
        });

        /*
         * Parses output like:
         *
         * bios_vendor:LENOVO
         * bios_version:8CET46WW
         */
        function parse_lines(output) {
            var ret = { };
            $.each(output.split("\n"), function(i, line) {
                var pos = line.indexOf(":");
                if (pos !== -1)
                    ret[line.substring(0, pos)] = line.substring(pos + 1);
            });
            return ret;
        }

        cockpit.spawn(["grep", "\\w", "sys_vendor", "product_name"],
                      { directory: "/sys/devices/virtual/dmi/id", err: "ignore" })
            .done(function(output) {
                var fields = parse_lines(output);
                $("#system_information_hardware_text").text(fields.sys_vendor + " " +
                                                            fields.product_name);
            })
            .fail(function(ex) {
                debug("couldn't read dmi info: " + ex);
            });

        cockpit.spawn(["grep", "\\w", "product_serial", "chassis_serial"],
                      { directory: "/sys/devices/virtual/dmi/id", superuser: "try", err: "ignore" })
            .done(function(output) {
                var fields = parse_lines(output);
                var present = !!(fields.product_serial || fields.chassis_serial);
                $("#system_information_asset_tag_text").text(fields.product_serial ||
                                                             fields.chassis_serial);
                $("#system-info-asset-row").toggle(present);
            })
            .fail(function(ex) {
                $("#system-info-asset-row").toggle(false);
                debug("couldn't read serial dmi info: " + ex);
            });

        function hostname_text() {
            if (!self.hostname_proxy)
                return;

            var pretty_hostname = self.hostname_proxy.PrettyHostname;
            var static_hostname = self.hostname_proxy.StaticHostname;

            var str = self.kernel_hostname;
            if (pretty_hostname && static_hostname && static_hostname != pretty_hostname)
                str = pretty_hostname + " (" + static_hostname + ")";
            else if (static_hostname)
                str = static_hostname;

            if (!str)
                str = _("Set Host name");
            $("#system_information_hostname_button").text(str);
            $("#system_information_os_text").text(self.hostname_proxy.OperatingSystemPrettyName || "");
        }

        cockpit.spawn(["hostname"], { err: "ignore" })
            .done(function(output) {
                self.kernel_hostname = $.trim(output);
                hostname_text();
            })
            .fail(function(ex) {
                hostname_text();
                debug("couldn't read kernel hostname: " + ex);
            });
        $(self.hostname_proxy).on("changed", hostname_text);
    },

    show: function() {
        this.cpu_plot.start_walking();
        this.memory_plot.start_walking();
        this.disk_plot.start_walking();
        this.network_plot.start_walking();
    },

    leave: function() {
        var self = this;

        self.cpu_plot.destroy();
        self.memory_plot.destroy();
        self.disk_plot.destroy();
        self.network_plot.destroy();

        $(self.hostname_proxy).off();
        self.hostname_proxy = null;

        self.client.close();
        self.client = null;

        $(cockpit).off('.server');

        $(self.sysroot).off();
        self.sysroot = null;
        if (self.ostree_client) {
            self.ostree_client.close();
            self.ostree_client = null;
        }
    },

    host_keys_show: function() {
        var self = this;
        var parenthesis = /^\((.*)\)$/;
        var spinner = $("#system_information_ssh_keys .spinner");
        var content = $("#system_information_ssh_keys .content");
        var error = $("#system_information_ssh_keys .alert");

        content.toggle(false);
        error.toggle(false);
        spinner.toggle(true);

        cockpit.script(host_keys_script, [],{ "superuser": "try",
                                              "err": "message" })
            .done(function(data) {
                var seen = {};
                var arr = [];
                var keys = {};

                var i, tmp, m;
                var full = data.trim().split("\n");
                for (i = 0; i < full.length; i++) {
                    var line = full[i];
                    if (!line)
                        continue;

                    var parts = line.trim().split(" ");
                    var title, fp = parts[1];
                    if (!seen[fp]) {
                        seen[fp] = fp;
                        title = parts[parts.length - 1];
                        if (title) {
                            m = title.match(parenthesis);
                            if (m && m[1])
                                title = m[1];
                        }
                        if (!keys[title])
                            keys[title] = [];
                        keys[title].push(fp);
                    }
                }

                arr = Object.keys(keys);
                arr.sort();
                arr = arr.map(function (k) {
                    return { title: k, fps: keys[k] };
                });

                tmp = Mustache.render(self.ssh_host_keys_tmpl, { keys: arr });
                content.html(tmp);
                spinner.toggle(false);
                error.toggle(false);
                content.toggle(true);
            })
            .fail(function(ex) {
                var msg = cockpit.format(_("failed to list ssh host keys: $0"), ex.message);
                content.toggle(false);
                spinner.toggle(false);
                $("#system_information_ssh_keys .alert strong").text(msg);
                error.toggle(true);
            });
    },

    sysroot_changed: function() {
        var self = this;

        if (self.sysroot.Booted && self.ostree_client) {
            var version = "";
            self.ostree_client.call(self.sysroot.Booted,
                                    "org.freedesktop.DBus.Properties", "Get",
                                    ['org.projectatomic.rpmostree1.OS',
                                     "BootedDeployment"])
                .done(function (result) {
                    if (result && result[0]) {
                        var deployment = result[0].v;
                        if (deployment && deployment.version)
                            version = deployment.version.v;
                    }
                })
                .fail(function (ex) {
                    console.log(ex);
                })
                .always(function () {
                    $("#system-ostree-version").toggleClass("hidden", !version);
                    $("#system-ostree-version-link").text(version);<commit_msg>systemd: Refresh host SSH key dialog every 10 seconds (#5996)<commit_after>/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2013 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/>.
 */

var $ = require("jquery");
var cockpit = require("cockpit");

var Mustache = require("mustache");
var plot = require("plot");
var service = require("service");

/* These add themselves to jQuery so just including is enough */
require("patterns");
require("bootstrap-datepicker/dist/js/bootstrap-datepicker");
require("bootstrap-combobox/js/bootstrap-combobox");

var shutdown = require("./shutdown");

var host_keys_script = require("raw!./ssh-list-host-keys.sh");

var _ = cockpit.gettext;
var C_ = cockpit.gettext;

var permission = cockpit.permission({ admin: true });
$(permission).on("changed", update_hostname_privileged);

function update_hostname_privileged() {
    $(".hostname-privileged").update_privileged(
        permission, cockpit.format(
            _("The user <b>$0</b> is not permitted to modify hostnames"),
            permission.user ? permission.user.name : '')
    );
}

function debug() {
    if (window.debugging == "all" || window.debugging == "system")
        console.debug.apply(console, arguments);
}

/* machine_info(address).done(function (info) { })
 *
 * Get information about the machine at ADDRESS.  The returned object
 * has these fields:
 *
 * memory  -  amount of physical memory
 */

var machine_info_promises = { };

function machine_info(address) {
    var pr = machine_info_promises[address];
    var dfd;
    if (!pr) {
        dfd = $.Deferred();
        machine_info_promises[address] = pr = dfd.promise();

        cockpit.spawn(["cat", "/proc/meminfo", "/proc/cpuinfo"]).
            done(function(text) {
                var info = { };
                var match = text.match(/MemTotal:[^0-9]*([0-9]+) [kK]B/);
                var total_kb = match && parseInt(match[1], 10);
                if (total_kb)
                    info.memory = total_kb*1024;

                info.cpus = 0;
                var re = new RegExp("^processor", "gm");
                while (re.test(text))
                    info.cpus += 1;
                dfd.resolve(info);
            }).
            fail(function() {
                dfd.reject();
            });
    }
    return pr;
}

function ServerTime() {
    var self = this;

    var client = cockpit.dbus('org.freedesktop.timedate1');
    var timedate = client.proxy();

    var time_offset = null;
    var remote_offset = null;

    self.timedate = timedate;

    self.timedate1_service = service.proxy("dbus-org.freedesktop.timedate1.service");
    self.timesyncd_service = service.proxy("systemd-timesyncd.service");

    /*
     * The time we return from here as its UTC time set to the
     * server time. This is the only way to get predictable
     * behavior and formatting of a Date() object in the absence of
     * IntlDateFormat and  friends.
     */
    Object.defineProperty(self, 'utc_fake_now', {
        enumerable: true,
        get: function get() {
            var offset = time_offset + remote_offset;
            return new Date(offset + (new Date()).valueOf());
        }
    });

    Object.defineProperty(self, 'now', {
        enumerable: true,
        get: function get() {
            return new Date(time_offset + (new Date()).valueOf());
        }
    });

    self.format = function format(and_time) {
        var string = self.utc_fake_now.toISOString();
        if (!and_time)
            return string.split('T')[0];
        var pos = string.lastIndexOf(':');
        if (pos !== -1)
            string = string.substring(0, pos);
        return string.replace('T', ' ');
    };

    self.updateInterval = window.setInterval(function() {
        $(self).triggerHandler("changed");
    }, 30000);

    self.wait = function wait() {
        if (remote_offset === null)
            return self.update();
        return cockpit.resolve();
    };

    self.update = function update() {
        return cockpit.spawn(["date", "+%s:%:z"], { err: "message" })
            .done(function(data) {
                var parts = data.trim().split(":").map(function(x) {
                    return parseInt(x, 10);
                });
                if (parts[1] < 0)
                    parts[2] = -(parts[2]);
                var timems = parts[0] * 1000;
                var offsetms = (parts[1] * 3600000) + parts[2] * 60000;
                var now = new Date();
                time_offset = (timems - now.valueOf());
                remote_offset = offsetms;
                $(self).triggerHandler("changed");
            })
            .fail(function(ex) {
                console.log("Couldn't calculate server time offset: " + cockpit.message(ex));
            });
    };

    self.change_time = function change_time(datestr, hourstr, minstr) {
        var dfd = $.Deferred();

        /*
         * The browser is brain dead when it comes to dates. But even if
         * it wasn't, or we loaded a library like moment.js, there is no
         * way to make sense of this date without a round trip to the
         * server ... the timezone is really server specific.
         */
        cockpit.spawn(["date", "--date=" + datestr + " " + hourstr + ":" + minstr, "+%s"])
            .fail(function(ex) {
                dfd.reject(ex);
            })
            .done(function(data) {
                var seconds = parseInt(data.trim(), 10);
                timedate.call('SetTime', [seconds * 1000 * 1000, false, true])
                    .fail(function(ex) {
                        dfd.reject(ex);
                    })
                    .done(function() {
                        self.update();
                        dfd.resolve();
                    });
            });

        return dfd;
    };

    self.poll_ntp_synchronized = function poll_ntp_synchronized() {
        client.call(timedate.path,
                    "org.freedesktop.DBus.Properties", "Get", [ "org.freedesktop.timedate1", "NTPSynchronized" ]).
            fail(function (error) {
                if (error.name != "org.freedesktop.DBus.Error.UnknownProperty" &&
                    error.problem != "not-found")
                    console.log("can't get NTPSynchronized property", error);
            }).
            done(function (result) {
                var ifaces = { "org.freedesktop.timedate1": { NTPSynchronized: result[0].v } };
                var data = { };
                data[timedate.path] = ifaces;
                client.notify(data);
            });
    };

    self.close = function close() {
        client.close();
    };

    self.update();
}

var change_systime_dialog;

PageServer.prototype = {
    _init: function() {
        this.id = "server";
        this.server_time = null;
        this.client = null;
        this.hostname_proxy = null;
    },

    getTitle: function() {
        return null;
    },

    setup: function() {
        var self = this;
        update_hostname_privileged();

        $('#shutdown-group [data-action]').on("click", function() {
            self.shutdown($(this).attr('data-action'));
        });

        $('#system-ostree-version-link').on('click', function () {
            cockpit.jump("/updates", cockpit.transport.host);
        });

        $('#system_information_hostname_button').on('click', function () {
            PageSystemInformationChangeHostname.client = self.client;
            $('#system_information_change_hostname').modal('show');
        });

        $('#system_information_systime_button').on('click', function () {
            change_systime_dialog.display(self.server_time);
        });

        self.server_time = new ServerTime();
        $(self.server_time).on("changed", function() {
            $('#system_information_systime_button').text(self.server_time.format(true));
        });

        self.ntp_status_tmpl = $("#ntp-status-tmpl").html();
        Mustache.parse(this.ntp_status_tmpl);

        self.ntp_status_icon_tmpl = $("#ntp-status-icon-tmpl").html();
        Mustache.parse(this.ntp_status_icon_tmpl);

        self.ssh_host_keys_tmpl = $("#ssh-host-keys-tmpl").html();
        Mustache.parse(this.ssh_host_keys_tmpl);

        $("#system_information_ssh_keys").on("show.bs.modal", function() {
            self.host_keys_show();
        });

        $("#system_information_ssh_keys").on("hide.bs.modal", function() {
            self.host_keys_hide();
        });

        function update_ntp_status() {
            var $elt = $('#system_information_systime_ntp_status');

            if (!self.server_time.timedate.NTP) {
                $elt.hide();
                $elt.popover('hide');
                return;
            }

            $elt.show();

            var model = {
                Synched: self.server_time.timedate.NTPSynchronized,
                service: null
            };

            var timesyncd_server_regex = /.*time server (.*)\./i;

            var timesyncd_status = (self.server_time.timesyncd_service.state == "running" &&
                                    self.server_time.timesyncd_service.service &&
                                    self.server_time.timesyncd_service.service.StatusText);

            if (self.server_time.timesyncd_service.state == "running")
                model.service = "systemd-timesyncd.service";

            if (timesyncd_status) {
                var match = timesyncd_status.match(timesyncd_server_regex);
                if (match)
                    model.Server = match[1];
                else if (timesyncd_status != "Idle." && timesyncd_status !== "")
                    model.SubStatus = timesyncd_status;
            }

            var popover_html = Mustache.render(self.ntp_status_tmpl, model);
            if (popover_html != $elt.attr('data-content')) {
                $elt.attr("data-content", popover_html);
                // Refresh the popover if it is open
                if ($elt.data('bs.popover').tip().hasClass('in'))
                    $elt.popover('show');
            }

            var icon_html = Mustache.render(self.ntp_status_icon_tmpl, model);
            $elt.html(icon_html);
        }

        $('#system_information_systime_ntp_status').popover();

        $(self.server_time.timesyncd_service).on("changed", update_ntp_status);
        $(self.server_time.timedate).on("changed", update_ntp_status);
        update_ntp_status();

        /* NTPSynchronized needs to be polled so we just do that
         * always.
         */
        window.setInterval(function () {
            self.server_time.poll_ntp_synchronized();
        }, 5000);

        $('#server').on('click', "[data-goto-service]", function () {
            var service = $(this).attr("data-goto-service");
            cockpit.jump("/system/services/#/" + window.encodeURIComponent(service));
        });

        self.plot_controls = plot.setup_plot_controls($('#server'), $('#server-graph-toolbar'));

        var pmcd_service = service.proxy("pmcd");
        var pmlogger_service = service.proxy("pmlogger");
        var pmlogger_promise;

        $("#server-pmlogger-switch").on("change", function(ev) {
            var val = $(this).onoff('value');
            if (pmlogger_service.exists) {
                if (val) {
                    pmlogger_promise = cockpit.all(pmcd_service.enable(),
                           pmcd_service.start(),
                           pmlogger_service.enable(),
                           pmlogger_service.start()).
                        fail(function (error) {
                            console.warn("Enabling pmlogger failed", error);
                        });
                } else {
                    pmlogger_promise = cockpit.all(pmlogger_service.disable(),
                           pmlogger_service.stop()).
                        fail(function (error) {
                            console.warn("Disabling pmlogger failed", error);
                        });
                }
                pmlogger_promise.always(function() {
                    pmlogger_promise = null;
                    refresh_pmlogger_state();
                });
            }
        });

        function refresh_pmlogger_state() {
            if (!pmlogger_service.exists)
                $('#server-pmlogger-onoff-row').hide();
            else if (!pmlogger_promise) {
                $("#server-pmlogger-switch").onoff('value', pmlogger_service.enabled);
                $('#server-pmlogger-onoff-row').show();
            }
        }

        $(pmlogger_service).on('changed', refresh_pmlogger_state);
        refresh_pmlogger_state();
    },

    enter: function() {
        var self = this;

        var machine_id = cockpit.file("/etc/machine-id");
        machine_id.read().done(function (content) {
            $("#system_machine_id").text(content);
            $("#system_machine_id").attr("title", content);
        }).fail(function (ex) {
            console.error("Error reading machine id", ex);
        }).always(function () {
            machine_id.close();
        });

        self.ostree_client = cockpit.dbus('org.projectatomic.rpmostree1',
                                          {"superuser" : true});
        $(self.ostree_client).on("close", function () {
            self.ostree_client = null;
        });

        self.sysroot = self.ostree_client.proxy('org.projectatomic.rpmostree1.Sysroot',
                                                '/org/projectatomic/rpmostree1/Sysroot');
        $(self.sysroot).on("changed", $.proxy(this, "sysroot_changed"));

        self.client = cockpit.dbus('org.freedesktop.hostname1');
        self.hostname_proxy = self.client.proxy('org.freedesktop.hostname1',
                                     '/org/freedesktop/hostname1');
        self.kernel_hostname = null;

        var series;

        /* CPU graph */

        var cpu_data = {
            direct: [ "kernel.all.cpu.nice", "kernel.all.cpu.user", "kernel.all.cpu.sys" ],
            internal: [ "cpu.basic.nice", "cpu.basic.user", "cpu.basic.system" ],
            units: "millisec",
            derive: "rate",
            factor: 0.1  // millisec / sec -> percent
        };

        var cpu_options = plot.plot_simple_template();
        $.extend(cpu_options.yaxis, { tickFormatter: function(v) { return v.toFixed(0); },
                                      max: 100
                                    });
        self.cpu_plot = plot.plot($("#server_cpu_graph"), 300);
        self.cpu_plot.set_options(cpu_options);
        series = self.cpu_plot.add_metrics_sum_series(cpu_data, { });

        /* Memory graph */

        var memory_data = {
            direct: [ "mem.util.used" ],
            internal: [ "memory.used" ],
            units: "bytes"
        };

        var memory_options = plot.plot_simple_template();
        $.extend(memory_options.yaxis, { ticks: plot.memory_ticks,
                                         tickFormatter: plot.format_bytes_tick_no_unit
                                       });
        memory_options.setup_hook = function memory_setup_hook(pl) {
            var axes = pl.getAxes();
            $('#server_memory_unit').text(plot.bytes_tick_unit(axes.yaxis));
        };

        self.memory_plot = plot.plot($("#server_memory_graph"), 300);
        self.memory_plot.set_options(memory_options);
        series = self.memory_plot.add_metrics_sum_series(memory_data, { });

        /* Network graph */

        var network_data = {
            direct: [ "network.interface.total.bytes" ],
            internal: [ "network.all.tx", "network.all.rx" ],
            units: "bytes",
            derive: "rate"
        };

        var network_options = plot.plot_simple_template();
        $.extend(network_options.yaxis, { tickFormatter: plot.format_bits_per_sec_tick_no_unit
                                        });
        network_options.setup_hook = function network_setup_hook(pl) {
            var axes = pl.getAxes();
            if (axes.yaxis.datamax < 100000)
                axes.yaxis.options.max = 100000;
            else
                axes.yaxis.options.max = null;
            axes.yaxis.options.min = 0;

            $('#server_network_traffic_unit').text(plot.bits_per_sec_tick_unit(axes.yaxis));
        };

        self.network_plot = plot.plot($("#server_network_traffic_graph"), 300);
        self.network_plot.set_options(network_options);
        series = self.network_plot.add_metrics_sum_series(network_data, { });

        /* Disk IO graph */

        var disk_data = {
            direct: [ "disk.all.total_bytes" ],
            internal: [ "disk.all.read", "disk.all.written" ],
            units: "bytes",
            derive: "rate"
        };

        var disk_options = plot.plot_simple_template();
        $.extend(disk_options.yaxis, { ticks: plot.memory_ticks,
                                       tickFormatter: plot.format_bytes_per_sec_tick_no_unit
                                     });
        disk_options.setup_hook = function disk_setup_hook(pl) {
            var axes = pl.getAxes();
            if (axes.yaxis.datamax < 100000)
                axes.yaxis.options.max = 100000;
            else
                axes.yaxis.options.max = null;
            axes.yaxis.options.min = 0;

            $('#server_disk_io_unit').text(plot.bytes_per_sec_tick_unit(axes.yaxis));
        };

        self.disk_plot = plot.plot($("#server_disk_io_graph"), 300);
        self.disk_plot.set_options(disk_options);
        series = self.disk_plot.add_metrics_sum_series(disk_data, { });

        machine_info().
            done(function (info) {
                cpu_options.yaxis.max = info.cpus * 100;
                self.cpu_plot.set_options(cpu_options);
                memory_options.yaxis.max = info.memory;
                self.memory_plot.set_options(memory_options);
            });

        self.plot_controls.reset([ self.cpu_plot, self.memory_plot, self.network_plot, self.disk_plot ]);

        $(window).on('resize.server', function () {
            self.cpu_plot.resize();
            self.memory_plot.resize();
            self.network_plot.resize();
            self.disk_plot.resize();
        });

        /*
         * Parses output like:
         *
         * bios_vendor:LENOVO
         * bios_version:8CET46WW
         */
        function parse_lines(output) {
            var ret = { };
            $.each(output.split("\n"), function(i, line) {
                var pos = line.indexOf(":");
                if (pos !== -1)
                    ret[line.substring(0, pos)] = line.substring(pos + 1);
            });
            return ret;
        }

        cockpit.spawn(["grep", "\\w", "sys_vendor", "product_name"],
                      { directory: "/sys/devices/virtual/dmi/id", err: "ignore" })
            .done(function(output) {
                var fields = parse_lines(output);
                $("#system_information_hardware_text").text(fields.sys_vendor + " " +
                                                            fields.product_name);
            })
            .fail(function(ex) {
                debug("couldn't read dmi info: " + ex);
            });

        cockpit.spawn(["grep", "\\w", "product_serial", "chassis_serial"],
                      { directory: "/sys/devices/virtual/dmi/id", superuser: "try", err: "ignore" })
            .done(function(output) {
                var fields = parse_lines(output);
                var present = !!(fields.product_serial || fields.chassis_serial);
                $("#system_information_asset_tag_text").text(fields.product_serial ||
                                                             fields.chassis_serial);
                $("#system-info-asset-row").toggle(present);
            })
            .fail(function(ex) {
                $("#system-info-asset-row").toggle(false);
                debug("couldn't read serial dmi info: " + ex);
            });

        function hostname_text() {
            if (!self.hostname_proxy)
                return;

            var pretty_hostname = self.hostname_proxy.PrettyHostname;
            var static_hostname = self.hostname_proxy.StaticHostname;

            var str = self.kernel_hostname;
            if (pretty_hostname && static_hostname && static_hostname != pretty_hostname)
                str = pretty_hostname + " (" + static_hostname + ")";
            else if (static_hostname)
                str = static_hostname;

            if (!str)
                str = _("Set Host name");
            $("#system_information_hostname_button").text(str);
            $("#system_information_os_text").text(self.hostname_proxy.OperatingSystemPrettyName || "");
        }

        cockpit.spawn(["hostname"], { err: "ignore" })
            .done(function(output) {
                self.kernel_hostname = $.trim(output);
                hostname_text();
            })
            .fail(function(ex) {
                hostname_text();
                debug("couldn't read kernel hostname: " + ex);
            });
        $(self.hostname_proxy).on("changed", hostname_text);
    },

    show: function() {
        this.cpu_plot.start_walking();
        this.memory_plot.start_walking();
        this.disk_plot.start_walking();
        this.network_plot.start_walking();
    },

    leave: function() {
        var self = this;

        self.cpu_plot.destroy();
        self.memory_plot.destroy();
        self.disk_plot.destroy();
        self.network_plot.destroy();

        $(self.hostname_proxy).off();
        self.hostname_proxy = null;

        self.client.close();
        self.client = null;

        $(cockpit).off('.server');

        $(self.sysroot).off();
        self.sysroot = null;
        if (self.ostree_client) {
            self.ostree_client.close();
            self.ostree_client = null;
        }
    },

    host_keys_show: function() {
        var self = this;
        $("#system_information_ssh_keys .spinner").toggle(true);
        $("#system_information_ssh_keys .content").toggle(false);
        $("#system_information_ssh_keys .alert").toggle(false);

        /*
         * Yes, we do refresh the keys while the dialog is open.
         * It may occur that sshd is not running at the point when
         * we try, or in rare cases the keys may change.
         */
        self.host_keys_interval = window.setInterval(function() {
            self.host_keys_update();
        }, 10 * 1000);
        self.host_keys_update();
    },

    host_keys_update: function() {
        var self = this;
        var parenthesis = /^\((.*)\)$/;
        var spinner = $("#system_information_ssh_keys .spinner");
        var content = $("#system_information_ssh_keys .content");
        var error = $("#system_information_ssh_keys .alert");

        cockpit.script(host_keys_script, [],{ "superuser": "try",
                                              "err": "message" })
            .done(function(data) {
                var seen = {};
                var arr = [];
                var keys = {};

                var i, tmp, m;
                var full = data.trim().split("\n");
                for (i = 0; i < full.length; i++) {
                    var line = full[i];
                    if (!line)
                        continue;

                    var parts = line.trim().split(" ");
                    var title, fp = parts[1];
                    if (!seen[fp]) {
                        seen[fp] = fp;
                        title = parts[parts.length - 1];
                        if (title) {
                            m = title.match(parenthesis);
                            if (m && m[1])
                                title = m[1];
                        }
                        if (!keys[title])
                            keys[title] = [];
                        keys[title].push(fp);
                    }
                }

                arr = Object.keys(keys);
                arr.sort();
                arr = arr.map(function (k) {
                    return { title: k, fps: keys[k] };
                });

                tmp = Mustache.render(self.ssh_host_keys_tmpl, { keys: arr });
                content.html(tmp);
                spinner.toggle(false);
                error.toggle(false);
                content.toggle(true);
            })
            .fail(function(ex) {
                var msg = cockpit.format(_("failed to list ssh host keys: $0"), ex.message);
                content.toggle(false);
                spinner.toggle(false);
                $("#system_information_ssh_keys .alert strong").text(msg);
                error.toggle(true);
            });
    },

    host_keys_hide: function() {
        var self = this;
        window.clearInterval(self.host_keys_interval);
        self.host_keys_interval = null;
    },

    sysroot_changed: function() {
        var self = this;

        if (self.sysroot.Booted && self.ostree_client) {
            var version = "";
            self.ostree_client.call(self.sysroot.Booted,
                                    "org.freedesktop.DBus.Properties", "Get",
                                    ['org.projectatomic.rpmostree1.OS',
                                     "BootedDeployment"])
                .done(function (result) {
                    if (result && result[0]) {
                        var deployment = result[0].v;
                        if (deployment && deployment.version)
                            version = deployment.version.v;
                    }
                })
                .fail(function (ex) {
                    console.log(ex);
                })
                .always(function () {
                    $("#system-ostree-version").toggleClass("hidden", !version);
                    $("#system-ostree-version-link").text(version);
{'fraction_non_alphanumeric': 0.1322734072340915, 'fraction_numerical': 0.00567680564612021, 'mean_word_length': 1.6766079770032338, 'pattern_counts': {'":': 12, '<': 17, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 48, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '191033', 'n_tokens_mistral': 14905, 'n_tokens_neox': 14225, 'n_words': 2630}