yast/yast-yast2

View on GitHub
library/system/src/lib/yast2/clients/view_anymsg.rb

Summary

Maintainability
B
6 hrs
Test Coverage
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************

require "yast/core_ext"

require "shellwords"

require "yast2/popup"

Yast.import "UI"
Yast.import "CommandLine"
Yast.import "Directory"
Yast.import "FileUtils"
Yast.import "Label"
Yast.import "Package"

module Yast
  # Reads a \n separated list of filenames from
  # /var/lib/YaST2/filenames
  # Lines starting with "#" are ignored (comments)
  # A line starting with "*" is taken as the default filename, the "*" is stripped
  #
  # All files are listed in an editable combo box, where the user can
  # easily switch between files and even add a new file
  #
  # At finish, the list of filenames is written back to
  # /var/lib/YaST2/filenames
  # adapting the default line (starting with "*") accordingly.
  #
  # The default is either given as WFM::Args(0) or is the file last viewed.
  class ViewAnymsgClient < Client
    using Yast::CoreExt::AnsiString

    # [String] Default list of log files
    DEFAULT_FILENAMES = [
      "/var/log/boot.log",
      "/var/log/messages",
      "/var/log/YaST2/y2log"
    ].freeze

    def main
      textdomain "base"

      # the command line description map
      return CommandLine.Run("id" => "view_anymsg") if WFM.Args.first == "help"

      # set up dialogue
      UI.OpenDialog(
        Opt(:decorated, :defaultsize),
        dialog_content
      )

      # wait until user clicks "OK"
      # check if ComboBox selected and change view accordingly
      res = nil

      loop do
        # Fill the LogView with file content
        UI.ChangeWidget(Id(:log), :Value, file_content(selected_filename))

        heading = Builtins.sformat(_("System Log (%1)"), selected_filename)
        UI.ChangeWidget(Id(:log), :Label, heading)

        if start_journal?
          res = :journal
          break
        end

        # wait for user input
        res = UI.UserInput

        case res
        when :ok, :cancel then break
        when :custom_file
          # adapt to combo box settings
          new_file = UI.QueryWidget(Id(:custom_file), :Value)
          self.selected_filename = new_file if !new_file.nil?
        else
          Builtins.y2milestone("bad UserInput (%1)", res)
        end
      end

      write_new_filenames if res == :ok
      UI.CloseDialog

      Yast::WFM.CallFunction("journal") if res == :journal

      true
    end

  private

    # Helper method to assess file status.
    #
    # Return one of :ok, :empty, :missing, :no_file, :no_access.
    #
    def file_state(file)
      begin
        File.stat(file)
      rescue Errno::EACCES
        return :no_access
      rescue Errno::ENOENT
        return :missing
      rescue StandardError
        nil
      end
      return :no_access if !File.readable?(file)
      return :no_file if !File.file?(file)
      return :empty if !File.size?(file)

      :ok
    end

    # Decide whether to read the log file or to start the 'journal' module instead.
    #
    # If the log can't be read, show some popups indicating the cause.
    #
    # Return true if the 'journal' module should be started.
    #
    def start_journal?
      case file_state(selected_filename)
      when :ok
        false
      when :empty
        Yast2::Popup.show(_("The selected log file is empty."))
        false
      when :no_file
        Yast2::Popup.show(_("The selected item is not a file."))
        false
      when :no_access
        Yast2::Popup.show(
          _(
            "You do not have permission to read the selected log file.\n\n" \
            "Run this YaST module as user 'root'."
          )
        )
        false
      when :missing
        res = Yast2::Popup.show(
          _(
            "The selected log file does not exist.\n\n" \
            "Many system components log into the systemd journal.\n" \
            "Do you want to start the YaST module for reading the systemd journal?"
          ),
          buttons: :yes_no,
          focus:   :no
        ) == :yes

        res && journal_client?
      end
    end

    # Tries to ensure the 'journal' client is available, even installing additional
    # packages if needed
    def journal_client?
      WFM.ClientExists("journal") || Package.Install("yast2-journal")
    end

    def dialog_content
      VBox(
        HSpacing(70), # force width
        HBox(
          HSpacing(1.0),
          ComboBox(
            Id(:custom_file),
            Opt(:editable, :notify, :hstretch),
            "",
            combobox_items
          ),
          HStretch()
        ),
        VSpacing(0.3),
        VWeight(
          1,
          HBox(
            VSpacing(18), # force height
            HSpacing(0.7),
            LogView(
              Id(:log),
              "",
              3, # height
              0
            ), # number of lines to show
            HSpacing(0.7)
          )
        ),
        VSpacing(0.3),
        PushButton(Id(:ok), Label.OKButton),
        VSpacing(0.3)
      )
    end

    def write_new_filenames
      result = []

      to_write = (available_filenames + [selected_filename]).uniq

      # re-build list to get new default correct
      filenames_content.lines.each do |line|
        line.strip!
        result << line if line.empty? || line.start_with?("#")

        line = line[1..-1] if line.start_with?("*")
        to_write.delete(line) # remember that we already write it
        line = "*" + line if selected_filename == line
        result << line
      end
      to_write.each do |line|
        line = "*" + line if selected_filename == line
        result << line
      end

      SCR.Write(
        path(".target.string"),
        filenames_path,
        result.join("\n")
      )
    end

    def filenames_path
      @filenames_path ||= ::File.join(Directory.vardir, "filenames")
    end

    def ensure_filenames_exist
      # Check if the filename list is present
      return if FileUtils.Exists(filenames_path)

      SCR.Execute(
        path(".target.bash"),
        "/bin/cp #{::File.join(Directory.ydatadir, "filenames").shellescape} #{filenames_path.shellescape}"
      )
    end

    attr_writer :selected_filename

    def selected_filename
      return @selected_filename if @selected_filename

      @selected_filename = default_filename
    end

    def file_content(filename)
      # read file content
      result = SCR.Read(path(".target.string"), filename)

      if result
        # replace invalid byte sequences with Unicode "replacement character"
        result.scrub!("�")
        # remove ANSI color escape sequences
        result.remove_ansi_sequences
        # remove remaining ASCII control characters (ASCII 0-31 and 127 (DEL))
        # except new line (LF = 0xa) and carriage return (CR = 0xd)
        result.tr!("\u0000-\u0009\u000b\u000c\u000e-\u001f\u007f", "")
      else
        result = _("File not found.")
      end

      result
    end

    def filenames_list
      @filenames_list ||= filenames_content.lines.each_with_object([]) do |line, result|
        line.strip!
        next if line.empty?
        next if line.start_with?("#")

        line = line[1..-1] if line.start_with?("*")
        result << line
      end
    end

    def available_filenames
      return @available_filenames if @available_filenames

      result = filenames_list + DEFAULT_FILENAMES + [arg_filename]
      @available_filenames = result.uniq.compact
    end

    def arg_filename
      arg = WFM.Args.first
      return arg if arg.is_a?(::String) && !arg.empty?
    end

    def filenames_content
      return @filenames_content if @filenames_content

      ensure_filenames_exist

      # get filename list
      @filenames_content = Convert.to_string(
        SCR.Read(path(".target.string"), filenames_path)
      )

      @filenames_content ||= ""
    end

    def default_filename
      return @default_filename if @default_filename

      return @default_filename = arg_filename if arg_filename

      default_line = filenames_content.lines.find { |l| l.start_with?("*") }

      return @default_filename = available_filenames.first unless default_line

      @default_filename = default_line[1..-1].strip
    end

    def combobox_items
      available_filenames.map do |filename|
        Item(Id(filename), filename, filename == default_filename)
      end
    end
  end
end