Skip to content

Commit

Permalink
Installer configuration console
Browse files Browse the repository at this point in the history
  • Loading branch information
lslezak committed Mar 4, 2021
1 parent ff95270 commit 57bec1d
Show file tree
Hide file tree
Showing 13 changed files with 763 additions and 6 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
steps:

- name: Git Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2

- name: Install Dependencies
run: rake build_dependencies:install
Expand Down Expand Up @@ -41,7 +41,7 @@ jobs:
steps:

- name: Git Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2

- name: Rubocop
run: rake check:rubocop
Expand All @@ -53,7 +53,7 @@ jobs:
steps:

- name: Git Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2

- name: Install Dependencies
run: rake build_dependencies:install
Expand All @@ -68,7 +68,7 @@ jobs:
steps:

- name: Git Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2

- name: Yardoc
run: rake check:doc
Expand All @@ -82,7 +82,7 @@ jobs:
steps:

- name: Git Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2

- name: Perl Syntax
run: yast-ci-ruby -o perl_syntax
Expand Down
87 changes: 87 additions & 0 deletions src/lib/installation/console.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# ------------------------------------------------------------------------------
# Copyright (c) 2021 SUSE LLC, 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.
#
# ------------------------------------------------------------------------------

require "yast"
require "installation/console/commands"
require "installation/console/gui"
require "installation/console/tui"

Yast.import "UI"

# Override the IRB to log all executed commands into the y2log so we know
# what exactly happened there (in case user did something wrong or strange...)
module IrbLogger
# wrap the original "evaluate" method, do some logging around
def evaluate(*args)
statements = args[1]
# do not log the internal IRB command for setting the last value variable
if statements.is_a?(::String) && statements.start_with?("_ = ")
super
else
Yast::Y2Logger.instance.info "Executing console command: #{statements.inspect}"
ret = super
Yast::Y2Logger.instance.info "Console command result: #{ret.inspect}"
ret
end
end
end

# inject the code
require "irb/workspace"
module IRB # :nodoc:
class WorkSpace
prepend IrbLogger
end
end

module Installation
module Console
class << self
# open a console and run an interactive IRB session in it
# testing in installed system:
# ruby -I src/lib -r installation/console.rb -e ::Installation::Console.run
def run
console = Yast::UI.TextMode ? Console::Tui.new : Console::Gui.new
console.run do
commands = Console::Commands.new(console)
# print the basic help text
commands.welcome

# start an IRB session in the context of the "commands" object
irb(commands)
end
end

private

# configure IRB and start an interactive session
# @param context [Object] context in which the IRB session runs
def irb(context)
# lazy loading
require "irb"
# enable TAB completion
require "irb/completion"

# see the Binding::irb method in irb.rb in the Ruby stdlib
IRB.setup(eval("__FILE__"), argv: [])
# use a simple prompt with some customizations
IRB.conf[:PROMPT][:YAST] = IRB.conf[:PROMPT][:SIMPLE].dup
IRB.conf[:PROMPT][:YAST][:RETURN] = ""
IRB.conf[:PROMPT][:YAST][:PROMPT_I] = "YaST >> "
IRB.conf[:PROMPT_MODE] = :YAST
workspace = IRB::WorkSpace.new(context)
IRB::Irb.new(workspace).run(IRB.conf)
end
end
end
end
96 changes: 96 additions & 0 deletions src/lib/installation/console/commands.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# ------------------------------------------------------------------------------
# Copyright (c) 2021 SUSE LLC, 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.
#
# ------------------------------------------------------------------------------

require "pp"
require "shellwords"

require "yast"
require "installation/console/plugins"

module Installation
module Console
class Commands
include Yast::I18n

# This class implements the commands in the installer console,
# the actual commands are implemented as plugins loaded from
# lib/installation/console/plugins/*.rb files
#
# All public methods in this class are the commands available in the console,
# that means we cannot include Yast::Logger here because it would define
# "log" command. For logging we have to use the full form here, e.g.
# Yast::Y2Logger.instance.info
def initialize(console)
textdomain "installation"

@console = console
Plugins.load_plugins
end

# print the "welcome" message with a basic help text
def welcome
puts "---- This is the YaST Installation Console ----"
puts
# this is the most important message so make it translatable,
# the console is for experts so it is OK have the rest untranslated
# TRANSLATORS: help text displayed in the installer command line console
puts _("Type 'quit' or press Ctrl+D to close the console and go back to the installer")
puts
puts "Type 'commands' to see the available special commands"
puts
puts "This is a Ruby shell, you can also type any Ruby command here"
puts "and inspect or change the YaST installer"
puts
puts "Hints: <Tab> completion is enabled, the command history is kept,"
puts "you can use the usual \"readline\" features..."
puts
end

# print the available commands
def commands
puts "Available commands:"
puts
print_command("quit", "Close the console and return back to the installer")

private_methods.grep(/_description$/).sort.each do |method|
print_command(method.to_s.sub(/_description$/, ""), send(method))
end

puts
end

# all unknown commands are handled via this "method_missing" callback
def method_missing(method_name, *_args)
Yast::Y2Logger.instance.info "Entered unknown command: #{method_name.inspect}"
puts "Error: Unknown command \"#{method_name}\""
puts
commands
end

# helper for running an YaST module
def run_yast_module(*args)
@console.run_yast_module(*args)
end

private

# print help text for a command
def print_command(cmd, descr)
# indent multiline descriptions
description = descr.gsub("\n", "\n ")
puts " #{cmd} - #{description}"
puts
end
end
end
end
136 changes: 136 additions & 0 deletions src/lib/installation/console/gui.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# ------------------------------------------------------------------------------
# Copyright (c) 2021 SUSE LLC, 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.
#
# ------------------------------------------------------------------------------

require "yast"

module Installation
module Console
# the installer console implementation for the graphical (Qt) UI
class Gui
Yast.import "Wizard"

# open a console and run a block in it
def run(&block)
start
redirect(&block)
ensure
stop
end

# helper for running an YaST module in console
def run_yast_module(*args)
# we cannot run any YaST module if there is a popup displayed, the module
# would be displayed *below* the popup making it inaccessible :-(
# make sure a wizard dialog is at the top
return unless wizard_dialog?

begin
# get the window ID of the currently active window (the xterm window)
window = `#{SWITCHER}`.chomp
rescue Errno::ENOENT
# if the switcher is missing display a short help
puts "Starting an YaST configuration module..."
puts
puts "After it is finished (by pressing [Next]/[Back]/[Abort])"
puts "press Alt+Tab to get back to this console."

# wait a bit so the user can read the message above
sleep(5)
end

Yast::WFM.call(*args)

# automatically switch the window focus from YaST back to the xterm window
system("#{SWITCHER} #{Shellwords.escape(window)}") if window
end

private

# path to the window switching helper tool (from yast2-x11)
SWITCHER = "/usr/lib/YaST2/bin/active_window".freeze

# is the toplevel dialog a wizard dialog? We cannot run an YaST module
# if a popup is currently displayed...
def wizard_dialog?
return true if Yast::Wizard.IsWizardDialog

puts "Error: YaST modules cannot be started if there is a popup dialog"
puts "displayed. First close this console then close the popup in the installer"
puts "and then start the console again."

false
end

# start the console, open a new xterm window
def start
# create a pipe for communication with the shell running in the xterm
@read, @write = IO.pipe
# get the /proc path for the reading end of the pipe
read_path = fd_path(Process.pid, @read.fileno)

# start a new xterm window, run a shell command which:
# 1. opens a watching FD for signaling exit
# 2. "echo" command prints the PID, the watching FD and the terminal
# device to the Ruby pipe above
# 3. "read" command keeps the xterm window open until any input is sent
# to the watching FD
command = "xterm -title \"YaST Installation Console\" -e bash -c \"exec {CLOSEFD}<> <(:);
echo \\$\\$ \\$CLOSEFD \\$(tty) > #{read_path};
read -u \\$CLOSEFD\" &"

system(command)

# read the values printed by the "echo" command above
@pid, @close_fd, @tty = @read.readline.split
end

# stop the console, close the xterm window
def stop
# send an empty string to the waiting "read" process
File.write(fd_path(@pid, @close_fd), "\n") if @pid && @close_fd
# close the pipes
@read.close if @read
@write.close if @write
end

# run a block with redirected IO (redirect to the started xterm console)
def redirect(&block)
# remember the initial IO channels
stdout_orig = $stdout.dup
stderr_orig = $stderr.dup
stdin_orig = $stdin.dup

# redirect all IO to the xterm window (its tty device)
$stdout.reopen(@tty)
$stderr.reopen(@tty)
$stdin.reopen(@tty)

begin
block.call if block_given?
ensure
# restore the original IO channels
$stdout.reopen(stdout_orig)
$stderr.reopen(stderr_orig)
$stdin.reopen(stdin_orig)
end
end

# get /proc path for a file descriptor
# @param pid [String, Integer] PID of the process
# @param fd [String, Integer] file descriptor number
def fd_path(pid, fd)
"/proc/#{pid}/fd/#{fd}"
end
end
end
end
Loading

0 comments on commit 57bec1d

Please sign in to comment.