2015-07-21 15:55:02 +00:00
|
|
|
# Copyright 2015 Canonical, Ltd.
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# 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 Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2017-01-13 01:42:31 +00:00
|
|
|
from concurrent import futures
|
2017-09-08 03:59:30 +00:00
|
|
|
import fcntl
|
2019-03-06 02:52:13 +00:00
|
|
|
import json
|
2015-07-21 15:55:02 +00:00
|
|
|
import logging
|
2018-02-07 21:37:22 +00:00
|
|
|
import os
|
|
|
|
import struct
|
2019-08-22 23:17:02 +00:00
|
|
|
import subprocess
|
2017-09-08 03:59:30 +00:00
|
|
|
import sys
|
2018-02-07 21:37:22 +00:00
|
|
|
import tty
|
2017-01-17 01:26:07 +00:00
|
|
|
|
2015-07-21 15:55:02 +00:00
|
|
|
import urwid
|
2017-11-16 00:29:22 +00:00
|
|
|
import yaml
|
2017-01-13 01:42:31 +00:00
|
|
|
|
2019-03-06 23:03:54 +00:00
|
|
|
from subiquitycore.controller import RepeatedController
|
2016-06-30 18:17:01 +00:00
|
|
|
from subiquitycore.signals import Signal
|
2019-09-09 02:28:48 +00:00
|
|
|
from subiquitycore.prober import Prober
|
2019-09-04 23:58:20 +00:00
|
|
|
from subiquitycore.ui.frame import SubiquityCoreUI
|
2015-07-21 15:55:02 +00:00
|
|
|
|
2016-06-30 18:17:01 +00:00
|
|
|
log = logging.getLogger('subiquitycore.core')
|
2015-07-21 15:55:02 +00:00
|
|
|
|
|
|
|
|
2019-03-06 02:32:31 +00:00
|
|
|
class Skip(Exception):
|
|
|
|
"""Raise this from a controller's default method to skip a screen."""
|
|
|
|
|
|
|
|
|
2017-09-08 03:59:30 +00:00
|
|
|
# From uapi/linux/kd.h:
|
|
|
|
KDGKBTYPE = 0x4B33 # get keyboard type
|
2018-02-07 21:37:22 +00:00
|
|
|
|
2018-05-22 17:46:00 +00:00
|
|
|
GIO_CMAP = 0x4B70 # gets colour palette on VGA+
|
|
|
|
PIO_CMAP = 0x4B71 # sets colour palette on VGA+
|
2017-09-12 13:28:08 +00:00
|
|
|
UO_R, UO_G, UO_B = 0xe9, 0x54, 0x20
|
2017-09-08 03:59:30 +00:00
|
|
|
|
2018-02-07 21:37:22 +00:00
|
|
|
# /usr/include/linux/kd.h
|
2018-05-22 17:46:00 +00:00
|
|
|
K_RAW = 0x00
|
|
|
|
K_XLATE = 0x01
|
2018-02-07 21:37:22 +00:00
|
|
|
K_MEDIUMRAW = 0x02
|
2018-05-22 17:46:00 +00:00
|
|
|
K_UNICODE = 0x03
|
|
|
|
K_OFF = 0x04
|
2018-02-07 21:37:22 +00:00
|
|
|
|
2018-05-22 17:46:00 +00:00
|
|
|
KDGKBMODE = 0x4B44 # gets current keyboard mode
|
|
|
|
KDSKBMODE = 0x4B45 # sets current keyboard mode
|
2018-02-07 21:37:22 +00:00
|
|
|
|
2017-09-08 03:59:30 +00:00
|
|
|
|
2017-11-21 22:26:47 +00:00
|
|
|
class ISO_8613_3_Screen(urwid.raw_display.Screen):
|
|
|
|
|
2017-11-21 22:37:49 +00:00
|
|
|
def __init__(self, _urwid_name_to_rgb):
|
2019-10-04 00:05:44 +00:00
|
|
|
self._urwid_name_to_rgb = _urwid_name_to_rgb
|
2017-11-21 22:26:47 +00:00
|
|
|
super().__init__()
|
|
|
|
|
2019-10-04 00:05:44 +00:00
|
|
|
def _cc(self, color):
|
|
|
|
"""Return the "SGR" parameter for selecting color.
|
|
|
|
|
|
|
|
See https://en.wikipedia.org/wiki/ANSI_escape_code#SGR for an
|
|
|
|
explanation. We use the basic codes for black/white/default for
|
|
|
|
maximum compatibility; they are the only colors used when the
|
|
|
|
mono palette is selected.
|
|
|
|
"""
|
|
|
|
if color == 'white':
|
|
|
|
return '7'
|
|
|
|
elif color == 'black':
|
|
|
|
return '0'
|
|
|
|
elif color == 'default':
|
|
|
|
return '9'
|
|
|
|
else:
|
|
|
|
# This is not, pedantically, a ISO 8613-3 code -- that would
|
|
|
|
# use colons to separate the rgb values instead. But it's
|
|
|
|
# what xterm, and hence everything else, supports.
|
|
|
|
return '8;2;{};{};{}'.format(*self._urwid_name_to_rgb[color])
|
|
|
|
|
2017-11-21 22:26:47 +00:00
|
|
|
def _attrspec_to_escape(self, a):
|
2019-10-04 00:05:44 +00:00
|
|
|
return '\x1b[0;3{};4{}m'.format(
|
|
|
|
self._cc(a.foreground),
|
|
|
|
self._cc(a.background))
|
2017-09-12 13:28:08 +00:00
|
|
|
|
2017-09-08 03:59:30 +00:00
|
|
|
|
|
|
|
def is_linux_tty():
|
|
|
|
try:
|
|
|
|
r = fcntl.ioctl(sys.stdout.fileno(), KDGKBTYPE, ' ')
|
|
|
|
except IOError as e:
|
|
|
|
log.debug("KDGKBTYPE failed %r", e)
|
|
|
|
return False
|
2019-09-09 02:37:48 +00:00
|
|
|
log.debug("KDGKBTYPE returned %r, is_linux_tty %s", r, r == b'\x02')
|
2017-09-08 03:59:30 +00:00
|
|
|
return r == b'\x02'
|
|
|
|
|
2017-11-21 22:37:49 +00:00
|
|
|
|
2019-10-03 23:38:26 +00:00
|
|
|
urwid_8_names = (
|
|
|
|
'black',
|
|
|
|
'dark red',
|
|
|
|
'dark green',
|
|
|
|
'brown',
|
|
|
|
'dark blue',
|
|
|
|
'dark magenta',
|
|
|
|
'dark cyan',
|
|
|
|
'light gray',
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def setup_palette(colors, styles):
|
|
|
|
"""Return a palette to be passed to MainLoop.
|
2017-11-21 22:49:20 +00:00
|
|
|
|
|
|
|
colors is a list of exactly 8 tuples (name, (r, g, b))
|
|
|
|
|
|
|
|
styles is a list of tuples (stylename, fg_color, bg_color) where
|
|
|
|
fg_color and bg_color are defined in 'colors'
|
|
|
|
"""
|
|
|
|
# The part that makes this "fun" is that urwid insists on referring
|
|
|
|
# to the basic colors by their "standard" names but we overwrite
|
|
|
|
# these colors to mean different things. So we convert styles into
|
|
|
|
# an urwid palette by mapping the names in colors to the standard
|
2019-10-03 23:38:26 +00:00
|
|
|
# name.
|
2017-11-21 22:49:20 +00:00
|
|
|
if len(colors) != 8:
|
2018-05-22 17:46:00 +00:00
|
|
|
raise Exception(
|
2019-10-03 23:38:26 +00:00
|
|
|
"setup_palette must be passed a list of exactly 8 colors")
|
2017-11-21 23:16:09 +00:00
|
|
|
urwid_name = dict(zip([c[0] for c in colors], urwid_8_names))
|
2017-11-21 22:49:20 +00:00
|
|
|
|
2017-11-21 23:16:09 +00:00
|
|
|
urwid_palette = []
|
2017-11-21 22:37:49 +00:00
|
|
|
for name, fg, bg in styles:
|
2017-11-21 23:16:09 +00:00
|
|
|
urwid_palette.append((name, urwid_name[fg], urwid_name[bg]))
|
2015-07-21 15:55:02 +00:00
|
|
|
|
2019-10-03 23:38:26 +00:00
|
|
|
return urwid_palette
|
|
|
|
|
|
|
|
|
|
|
|
def setup_screen(colors, is_linux_tty):
|
|
|
|
"""Return a screen to be passed to MainLoop.
|
|
|
|
|
|
|
|
colors is a list of exactly 8 tuples (name, (r, g, b)), the same as
|
|
|
|
passed to setup_palette.
|
|
|
|
"""
|
|
|
|
# On the linux console, we overwrite the first 8 colors to be those
|
|
|
|
# defined by colors. Otherwise, we return a screen that uses ISO
|
|
|
|
# 8613-3 codes to display the colors.
|
|
|
|
if len(colors) != 8:
|
|
|
|
raise Exception(
|
|
|
|
"setup_screen must be passed a list of exactly 8 colors")
|
2019-09-09 02:33:44 +00:00
|
|
|
if is_linux_tty:
|
2017-11-21 22:26:47 +00:00
|
|
|
curpal = bytearray(16*3)
|
|
|
|
fcntl.ioctl(sys.stdout.fileno(), GIO_CMAP, curpal)
|
|
|
|
for i in range(8):
|
|
|
|
for j in range(3):
|
2017-11-21 22:49:20 +00:00
|
|
|
curpal[i*3+j] = colors[i][1][j]
|
2017-11-21 22:26:47 +00:00
|
|
|
fcntl.ioctl(sys.stdout.fileno(), PIO_CMAP, curpal)
|
2019-10-03 23:38:26 +00:00
|
|
|
return urwid.raw_display.Screen()
|
2017-11-21 22:26:47 +00:00
|
|
|
else:
|
2017-11-21 22:49:20 +00:00
|
|
|
_urwid_name_to_rgb = {}
|
|
|
|
for i, n in enumerate(urwid_8_names):
|
|
|
|
_urwid_name_to_rgb[n] = colors[i][1]
|
2019-10-03 23:38:26 +00:00
|
|
|
return ISO_8613_3_Screen(_urwid_name_to_rgb)
|
2017-11-21 22:26:47 +00:00
|
|
|
|
|
|
|
|
2018-02-07 21:37:22 +00:00
|
|
|
class KeyCodesFilter:
|
|
|
|
"""input_filter that can pass (medium) raw keycodes to the application
|
|
|
|
|
|
|
|
See http://lct.sourceforge.net/lct/x60.html for terminology.
|
|
|
|
|
|
|
|
Call enter_keycodes_mode()/exit_keycodes_mode() to switch into and
|
|
|
|
out of keycodes mode. In keycodes mode, the only events passed to
|
|
|
|
the application are "press $N" / "release $N" where $N is the
|
|
|
|
keycode the user pressed or released.
|
|
|
|
|
|
|
|
Much of this is cribbed from the source of the "showkeys" utility.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self._fd = os.open("/proc/self/fd/0", os.O_RDWR)
|
|
|
|
self.filtering = False
|
|
|
|
|
|
|
|
def enter_keycodes_mode(self):
|
|
|
|
log.debug("enter_keycodes_mode")
|
|
|
|
self.filtering = True
|
2019-08-15 01:50:23 +00:00
|
|
|
# Read the old keyboard mode (it will proably always be K_UNICODE but
|
|
|
|
# well).
|
2018-02-07 21:37:22 +00:00
|
|
|
o = bytearray(4)
|
|
|
|
fcntl.ioctl(self._fd, KDGKBMODE, o)
|
|
|
|
self._old_mode = struct.unpack('i', o)[0]
|
2019-08-15 01:50:23 +00:00
|
|
|
# Set the keyboard mode to K_MEDIUMRAW, which causes the keyboard
|
|
|
|
# driver in the kernel to pass us keycodes.
|
2018-02-07 21:37:22 +00:00
|
|
|
fcntl.ioctl(self._fd, KDSKBMODE, K_MEDIUMRAW)
|
|
|
|
|
|
|
|
def exit_keycodes_mode(self):
|
|
|
|
log.debug("exit_keycodes_mode")
|
|
|
|
self.filtering = False
|
|
|
|
fcntl.ioctl(self._fd, KDSKBMODE, self._old_mode)
|
|
|
|
|
|
|
|
def filter(self, keys, codes):
|
|
|
|
# Luckily urwid passes us the raw results from read() we can
|
|
|
|
# turn into keycodes.
|
|
|
|
if self.filtering:
|
|
|
|
i = 0
|
|
|
|
r = []
|
|
|
|
n = len(codes)
|
|
|
|
while i < len(codes):
|
|
|
|
# This is straight from showkeys.c.
|
|
|
|
if codes[i] & 0x80:
|
|
|
|
p = 'release '
|
|
|
|
else:
|
|
|
|
p = 'press '
|
2018-05-22 17:46:00 +00:00
|
|
|
if i + 2 < n and (codes[i] & 0x7f) == 0:
|
|
|
|
if (codes[i + 1] & 0x80) != 0:
|
|
|
|
if (codes[i + 2] & 0x80) != 0:
|
|
|
|
kc = (((codes[i + 1] & 0x7f) << 7) |
|
|
|
|
(codes[i + 2] & 0x7f))
|
|
|
|
i += 3
|
2018-02-07 21:37:22 +00:00
|
|
|
else:
|
|
|
|
kc = codes[i] & 0x7f
|
|
|
|
i += 1
|
|
|
|
r.append(p + str(kc))
|
|
|
|
return r
|
|
|
|
else:
|
|
|
|
return keys
|
|
|
|
|
|
|
|
|
|
|
|
class DummyKeycodesFilter:
|
|
|
|
# A dummy implementation of the same interface as KeyCodesFilter
|
|
|
|
# we can use when not running in a linux tty.
|
|
|
|
|
|
|
|
def enter_keycodes_mode(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def exit_keycodes_mode(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def filter(self, keys, codes):
|
|
|
|
return keys
|
|
|
|
|
|
|
|
|
2016-07-25 00:38:19 +00:00
|
|
|
class Application:
|
|
|
|
|
2016-07-25 00:51:39 +00:00
|
|
|
# A concrete subclass must set project and controllers attributes, e.g.:
|
|
|
|
#
|
|
|
|
# project = "subiquity"
|
2016-09-27 02:33:54 +00:00
|
|
|
# controllers = [
|
|
|
|
# "Welcome",
|
|
|
|
# "Network",
|
|
|
|
# "Filesystem",
|
|
|
|
# "Identity",
|
|
|
|
# "InstallProgress",
|
|
|
|
# ]
|
2016-09-29 01:06:09 +00:00
|
|
|
# The 'next-screen' and 'prev-screen' signals move through the list of
|
|
|
|
# controllers in order, calling the default method on the controller
|
|
|
|
# instance.
|
2016-06-30 18:50:21 +00:00
|
|
|
|
2019-09-04 23:58:20 +00:00
|
|
|
make_ui = SubiquityCoreUI
|
|
|
|
|
|
|
|
def __init__(self, opts):
|
2019-09-09 02:28:48 +00:00
|
|
|
prober = Prober(opts)
|
2015-10-23 15:03:04 +00:00
|
|
|
|
2019-09-04 23:58:20 +00:00
|
|
|
self.ui = self.make_ui()
|
2019-08-06 02:11:57 +00:00
|
|
|
self.opts = opts
|
2017-03-16 09:52:05 +00:00
|
|
|
opts.project = self.project
|
|
|
|
|
2019-03-07 02:05:13 +00:00
|
|
|
self.root = '/'
|
2019-03-06 02:52:13 +00:00
|
|
|
if opts.dry_run:
|
2019-03-07 02:05:13 +00:00
|
|
|
self.root = '.subiquity'
|
|
|
|
self.state_dir = os.path.join(self.root, 'run', self.project)
|
2019-03-06 02:52:13 +00:00
|
|
|
os.makedirs(os.path.join(self.state_dir, 'states'), exist_ok=True)
|
|
|
|
|
2019-08-06 02:11:57 +00:00
|
|
|
self.answers = {}
|
2017-11-16 00:29:22 +00:00
|
|
|
if opts.answers is not None:
|
2019-08-06 02:11:57 +00:00
|
|
|
self.answers = yaml.safe_load(opts.answers.read())
|
|
|
|
log.debug("Loaded answers %s", self.answers)
|
2017-12-13 03:10:36 +00:00
|
|
|
if not opts.dry_run:
|
|
|
|
open('/run/casper-no-prompt', 'w').close()
|
2017-11-16 00:29:22 +00:00
|
|
|
|
2019-10-03 23:38:26 +00:00
|
|
|
self.is_color = False
|
|
|
|
self.color_palette = setup_palette(self.COLORS, self.STYLES)
|
|
|
|
|
2019-09-09 02:33:44 +00:00
|
|
|
self.is_linux_tty = is_linux_tty()
|
|
|
|
|
|
|
|
if self.is_linux_tty:
|
2019-08-06 02:11:57 +00:00
|
|
|
self.input_filter = KeyCodesFilter()
|
2018-02-07 21:37:22 +00:00
|
|
|
else:
|
2019-08-06 02:11:57 +00:00
|
|
|
self.input_filter = DummyKeycodesFilter()
|
|
|
|
|
|
|
|
self.scale_factor = float(
|
|
|
|
os.environ.get('SUBIQUITY_REPLAY_TIMESCALE', "1"))
|
|
|
|
self.updated = os.path.exists(os.path.join(self.state_dir, 'updating'))
|
|
|
|
self.signal = Signal()
|
|
|
|
self.prober = prober
|
|
|
|
self.loop = None
|
|
|
|
self.pool = futures.ThreadPoolExecutor(10)
|
2016-11-22 23:08:36 +00:00
|
|
|
if opts.screens:
|
2018-05-22 17:46:00 +00:00
|
|
|
self.controllers = [c for c in self.controllers
|
|
|
|
if c in opts.screens]
|
2019-03-06 23:03:54 +00:00
|
|
|
else:
|
|
|
|
self.controllers = self.controllers[:]
|
2019-09-04 23:58:20 +00:00
|
|
|
self.ui.progress_completion = len(self.controllers)
|
2019-08-06 02:11:57 +00:00
|
|
|
self.controller_instances = dict.fromkeys(self.controllers)
|
2016-09-27 02:33:54 +00:00
|
|
|
self.controller_index = -1
|
2015-07-21 20:34:46 +00:00
|
|
|
|
2019-09-03 01:04:47 +00:00
|
|
|
@property
|
|
|
|
def cur_controller(self):
|
|
|
|
if self.controller_index < 0:
|
|
|
|
return None
|
|
|
|
controller_name = self.controllers[self.controller_index]
|
|
|
|
return self.controller_instances[controller_name]
|
|
|
|
|
2019-03-07 02:05:13 +00:00
|
|
|
def run_in_bg(self, func, callback):
|
|
|
|
"""Run func() in a thread and call callback on UI thread.
|
|
|
|
|
|
|
|
callback will be passed a concurrent.futures.Future containing
|
|
|
|
the result of func(). The result of callback is discarded. An
|
|
|
|
exception will crash the process so be careful!
|
|
|
|
"""
|
2019-08-06 02:11:57 +00:00
|
|
|
fut = self.pool.submit(func)
|
2019-03-07 02:05:13 +00:00
|
|
|
|
|
|
|
def in_main_thread(ignored):
|
2019-08-19 22:34:31 +00:00
|
|
|
self.loop.remove_watch_pipe(pipe)
|
2019-08-14 22:07:14 +00:00
|
|
|
os.close(pipe)
|
2019-03-07 02:05:13 +00:00
|
|
|
callback(fut)
|
|
|
|
|
2019-08-06 02:11:57 +00:00
|
|
|
pipe = self.loop.watch_pipe(in_main_thread)
|
2019-03-07 02:05:13 +00:00
|
|
|
|
|
|
|
def in_random_thread(ignored):
|
|
|
|
os.write(pipe, b'x')
|
|
|
|
fut.add_done_callback(in_random_thread)
|
|
|
|
|
2019-08-22 23:17:02 +00:00
|
|
|
def run_command_in_foreground(self, cmd, **kw):
|
|
|
|
screen = self.loop.screen
|
|
|
|
|
2019-09-30 03:11:31 +00:00
|
|
|
# Calling screen.stop() sends the INPUT_DESCRIPTORS_CHANGED
|
|
|
|
# signal. This calls _reset_input_descriptors() which calls
|
|
|
|
# unhook_event_loop / hook_event_loop on the screen. But this all
|
|
|
|
# happens before _started is set to False on the screen and so this
|
|
|
|
# does not actually do anything -- we end up attempting to read from
|
|
|
|
# stdin while in a background process group, something that gets the
|
|
|
|
# kernel upset at us.
|
|
|
|
#
|
|
|
|
# The cleanest fix seems to be to just send the signal again once
|
|
|
|
# stop() has returned which, now that screen._started is False,
|
|
|
|
# correctly stops listening from stdin.
|
|
|
|
#
|
|
|
|
# There is an exactly analagous problem with screen.start() except
|
|
|
|
# there the symptom is that we are running in the foreground but not
|
|
|
|
# listening to stdin! The fix is the same.
|
|
|
|
|
2019-08-22 23:17:02 +00:00
|
|
|
def run():
|
|
|
|
subprocess.run(cmd, **kw)
|
|
|
|
|
|
|
|
def restore(fut):
|
|
|
|
screen.start()
|
|
|
|
urwid.emit_signal(
|
|
|
|
screen, urwid.display_common.INPUT_DESCRIPTORS_CHANGED)
|
|
|
|
tty.setraw(0)
|
|
|
|
|
|
|
|
screen.stop()
|
2019-09-30 03:11:31 +00:00
|
|
|
urwid.emit_signal(
|
|
|
|
screen, urwid.display_common.INPUT_DESCRIPTORS_CHANGED)
|
2019-08-22 23:17:02 +00:00
|
|
|
self.run_in_bg(run, restore)
|
|
|
|
|
2015-08-18 16:29:56 +00:00
|
|
|
def _connect_base_signals(self):
|
2019-09-09 02:52:36 +00:00
|
|
|
"""Connect signals used in the core controller."""
|
|
|
|
signals = [
|
|
|
|
('quit', self.exit),
|
|
|
|
('next-screen', self.next_screen),
|
|
|
|
('prev-screen', self.prev_screen),
|
|
|
|
]
|
2019-08-06 02:11:57 +00:00
|
|
|
if self.opts.dry_run:
|
2016-08-30 22:55:42 +00:00
|
|
|
signals.append(('control-x-quit', self.exit))
|
2019-08-06 02:11:57 +00:00
|
|
|
self.signal.connect_signals(signals)
|
2015-08-18 16:29:56 +00:00
|
|
|
|
|
|
|
# Registers signals from each controller
|
2019-08-06 02:11:57 +00:00
|
|
|
for controller_class in self.controller_instances.values():
|
2015-08-18 16:29:56 +00:00
|
|
|
controller_class.register_signals()
|
2019-09-09 02:52:36 +00:00
|
|
|
log.debug("known signals: %s", self.signal.known_signals)
|
2015-07-21 20:34:46 +00:00
|
|
|
|
2019-03-06 02:52:13 +00:00
|
|
|
def save_state(self):
|
2019-09-03 01:04:47 +00:00
|
|
|
cur_controller = self.cur_controller
|
|
|
|
if cur_controller is None:
|
2019-03-06 02:52:13 +00:00
|
|
|
return
|
|
|
|
state_path = os.path.join(
|
2019-09-03 01:04:47 +00:00
|
|
|
self.state_dir, 'states', cur_controller._controller_name())
|
2019-03-06 02:52:13 +00:00
|
|
|
with open(state_path, 'w') as fp:
|
|
|
|
json.dump(cur_controller.serialize(), fp)
|
|
|
|
|
2019-03-06 02:32:31 +00:00
|
|
|
def select_screen(self, index):
|
2019-09-03 01:04:47 +00:00
|
|
|
if self.cur_controller is not None:
|
|
|
|
self.cur_controller.end_ui()
|
2019-03-06 02:32:31 +00:00
|
|
|
self.controller_index = index
|
2019-08-06 02:11:57 +00:00
|
|
|
self.ui.progress_current = index
|
2019-09-03 01:04:47 +00:00
|
|
|
log.debug(
|
|
|
|
"moving to screen %s", self.cur_controller._controller_name())
|
|
|
|
self.cur_controller.start_ui()
|
2019-03-06 02:52:13 +00:00
|
|
|
state_path = os.path.join(self.state_dir, 'last-screen')
|
|
|
|
with open(state_path, 'w') as fp:
|
2019-09-03 01:04:47 +00:00
|
|
|
fp.write(self.cur_controller._controller_name())
|
2019-03-06 02:32:31 +00:00
|
|
|
|
|
|
|
def next_screen(self, *args):
|
2019-03-06 02:52:13 +00:00
|
|
|
self.save_state()
|
2019-03-06 02:32:31 +00:00
|
|
|
while True:
|
|
|
|
if self.controller_index == len(self.controllers) - 1:
|
|
|
|
self.exit()
|
|
|
|
try:
|
|
|
|
self.select_screen(self.controller_index + 1)
|
|
|
|
except Skip:
|
2019-04-17 22:22:05 +00:00
|
|
|
controller_name = self.controllers[self.controller_index]
|
|
|
|
log.debug("skipping screen %s", controller_name)
|
2019-03-06 02:32:31 +00:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
return
|
2016-09-27 02:33:54 +00:00
|
|
|
|
|
|
|
def prev_screen(self, *args):
|
2019-03-06 02:52:13 +00:00
|
|
|
self.save_state()
|
2019-03-06 02:32:31 +00:00
|
|
|
while True:
|
|
|
|
if self.controller_index == 0:
|
|
|
|
self.exit()
|
|
|
|
try:
|
|
|
|
self.select_screen(self.controller_index - 1)
|
|
|
|
except Skip:
|
2019-04-03 23:00:49 +00:00
|
|
|
controller_name = self.controllers[self.controller_index]
|
|
|
|
log.debug("skipping screen %s", controller_name)
|
2019-03-06 02:32:31 +00:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
return
|
2016-09-27 02:33:54 +00:00
|
|
|
|
2015-07-21 15:55:02 +00:00
|
|
|
# EventLoop -------------------------------------------------------------------
|
|
|
|
|
|
|
|
def exit(self):
|
|
|
|
raise urwid.ExitMainLoop()
|
|
|
|
|
2018-05-01 00:49:39 +00:00
|
|
|
def run_scripts(self, scripts):
|
2018-05-01 01:00:27 +00:00
|
|
|
# run_scripts runs (or rather arranges to run, it's all async)
|
|
|
|
# a series of python snippets in a helpful namespace. This is
|
|
|
|
# all in aid of being able to test some part of the UI without
|
|
|
|
# having to click the same buttons over and over again to get
|
|
|
|
# the UI to the part you are working on.
|
|
|
|
#
|
|
|
|
# In the namespace are:
|
|
|
|
# * everything from view_helpers
|
|
|
|
# * wait, delay execution of subsequent scripts for a while
|
|
|
|
# * c, a function that finds a button and clicks it. uses
|
|
|
|
# wait, above to wait for the button to appear in case it
|
|
|
|
# takes a while.
|
2018-05-01 00:49:39 +00:00
|
|
|
from subiquitycore.testing import view_helpers
|
2018-05-01 00:57:03 +00:00
|
|
|
|
|
|
|
class ScriptState:
|
|
|
|
def __init__(self):
|
|
|
|
self.ns = view_helpers.__dict__.copy()
|
|
|
|
self.waiting = False
|
|
|
|
self.wait_count = 0
|
|
|
|
self.scripts = scripts
|
|
|
|
|
|
|
|
ss = ScriptState()
|
|
|
|
|
2018-05-01 00:49:39 +00:00
|
|
|
def _run_script(*args):
|
2018-05-01 00:57:03 +00:00
|
|
|
log.debug("running %s", ss.scripts[0])
|
|
|
|
exec(ss.scripts[0], ss.ns)
|
|
|
|
if ss.waiting:
|
2018-05-01 00:49:39 +00:00
|
|
|
return
|
2018-05-01 00:57:03 +00:00
|
|
|
ss.scripts = ss.scripts[1:]
|
|
|
|
if ss.scripts:
|
2019-08-06 02:11:57 +00:00
|
|
|
self.loop.set_alarm_in(0.01, _run_script)
|
2018-05-01 00:57:03 +00:00
|
|
|
|
2018-05-01 00:49:39 +00:00
|
|
|
def c(pat):
|
2019-08-06 02:11:57 +00:00
|
|
|
but = view_helpers.find_button_matching(self.ui, '.*' + pat + '.*')
|
2018-05-01 00:49:39 +00:00
|
|
|
if not but:
|
2018-05-01 00:57:03 +00:00
|
|
|
ss.wait_count += 1
|
|
|
|
if ss.wait_count > 10:
|
2018-05-22 17:46:00 +00:00
|
|
|
raise Exception("no button found matching %r after"
|
|
|
|
"waiting for 10 secs" % pat)
|
|
|
|
wait(1, func=lambda: c(pat))
|
2018-05-01 00:49:39 +00:00
|
|
|
return
|
2018-05-01 00:57:03 +00:00
|
|
|
ss.wait_count = 0
|
2018-05-01 00:49:39 +00:00
|
|
|
view_helpers.click(but)
|
2018-05-01 00:57:03 +00:00
|
|
|
|
2018-05-01 00:49:39 +00:00
|
|
|
def wait(delay, func=None):
|
2018-05-01 00:57:03 +00:00
|
|
|
ss.waiting = True
|
2018-05-22 17:46:00 +00:00
|
|
|
|
2018-05-01 00:49:39 +00:00
|
|
|
def next(loop, user_data):
|
2018-05-01 00:57:03 +00:00
|
|
|
ss.waiting = False
|
2018-05-01 00:49:39 +00:00
|
|
|
if func is not None:
|
|
|
|
func()
|
2018-05-01 00:57:03 +00:00
|
|
|
if not ss.waiting:
|
|
|
|
ss.scripts = ss.scripts[1:]
|
|
|
|
if ss.scripts:
|
2018-05-01 00:49:39 +00:00
|
|
|
_run_script()
|
2019-08-06 02:11:57 +00:00
|
|
|
self.loop.set_alarm_in(delay, next)
|
2018-05-01 00:57:03 +00:00
|
|
|
|
|
|
|
ss.ns['c'] = c
|
|
|
|
ss.ns['wait'] = wait
|
2019-08-06 02:11:57 +00:00
|
|
|
ss.ns['ui'] = self.ui
|
2018-05-01 00:57:03 +00:00
|
|
|
|
2019-08-06 02:11:57 +00:00
|
|
|
self.loop.set_alarm_in(0.06, _run_script)
|
2018-05-01 00:29:29 +00:00
|
|
|
|
2019-10-03 09:47:14 +00:00
|
|
|
def toggle_color(self):
|
|
|
|
if self.is_color:
|
|
|
|
new_palette = self.STYLES_MONO
|
|
|
|
self.is_color = False
|
|
|
|
else:
|
|
|
|
new_palette = self.color_palette
|
|
|
|
self.is_color = True
|
|
|
|
self.loop.screen.register_palette(new_palette)
|
|
|
|
self.loop.screen.clear()
|
|
|
|
|
2019-08-07 03:56:23 +00:00
|
|
|
def unhandled_input(self, key):
|
|
|
|
if key == 'ctrl x':
|
|
|
|
self.signal.emit_signal('control-x-quit')
|
2019-10-03 09:47:14 +00:00
|
|
|
elif key == 'f3':
|
|
|
|
self.loop.screen.clear()
|
|
|
|
elif key in ['ctrl t', 'f4']:
|
|
|
|
self.toggle_color()
|
2019-08-07 03:56:23 +00:00
|
|
|
|
2019-09-03 00:58:57 +00:00
|
|
|
def load_controllers(self):
|
2019-09-09 02:52:36 +00:00
|
|
|
log.debug("load_controllers")
|
2019-09-03 00:58:57 +00:00
|
|
|
controllers_mod = __import__(
|
|
|
|
'{}.controllers'.format(self.project), None, None, [''])
|
|
|
|
for i, k in enumerate(self.controllers):
|
|
|
|
if self.controller_instances[k] is None:
|
|
|
|
log.debug("Importing controller: {}".format(k))
|
|
|
|
klass = getattr(controllers_mod, k+"Controller")
|
|
|
|
self.controller_instances[k] = klass(self)
|
|
|
|
else:
|
|
|
|
count = 1
|
|
|
|
for k2 in self.controllers[:i]:
|
|
|
|
if k2 == k or k2.startswith(k + '-'):
|
|
|
|
count += 1
|
|
|
|
orig = self.controller_instances[k]
|
|
|
|
k += '-' + str(count)
|
|
|
|
self.controllers[i] = k
|
|
|
|
self.controller_instances[k] = RepeatedController(
|
|
|
|
orig, count)
|
2019-09-09 02:52:36 +00:00
|
|
|
log.debug("load_controllers done")
|
2019-09-03 00:58:57 +00:00
|
|
|
|
|
|
|
def load_serialized_state(self):
|
|
|
|
for k in self.controllers:
|
|
|
|
state_path = os.path.join(self.state_dir, 'states', k)
|
|
|
|
if not os.path.exists(state_path):
|
|
|
|
continue
|
|
|
|
with open(state_path) as fp:
|
|
|
|
self.controller_instances[k].deserialize(
|
|
|
|
json.load(fp))
|
|
|
|
|
|
|
|
last_screen = None
|
|
|
|
state_path = os.path.join(self.state_dir, 'last-screen')
|
|
|
|
if os.path.exists(state_path):
|
|
|
|
with open(state_path) as fp:
|
|
|
|
last_screen = fp.read().strip()
|
|
|
|
|
|
|
|
if last_screen in self.controllers:
|
|
|
|
return self.controllers.index(last_screen)
|
|
|
|
else:
|
|
|
|
return 0
|
|
|
|
|
2015-07-21 15:55:02 +00:00
|
|
|
def run(self):
|
2019-09-09 02:52:36 +00:00
|
|
|
log.debug("Application.run")
|
2019-10-03 23:38:26 +00:00
|
|
|
screen = setup_screen(self.COLORS, self.is_linux_tty)
|
2015-07-21 15:55:02 +00:00
|
|
|
|
2019-08-06 02:11:57 +00:00
|
|
|
self.loop = urwid.MainLoop(
|
2019-10-03 23:38:26 +00:00
|
|
|
self.ui, palette=self.color_palette, screen=screen,
|
2019-08-06 02:11:57 +00:00
|
|
|
handle_mouse=False, pop_ups=True,
|
2019-08-07 03:56:23 +00:00
|
|
|
input_filter=self.input_filter.filter,
|
|
|
|
unhandled_input=self.unhandled_input)
|
2018-02-07 21:37:22 +00:00
|
|
|
|
2019-10-03 23:38:26 +00:00
|
|
|
self.toggle_color()
|
|
|
|
|
2019-08-06 02:11:57 +00:00
|
|
|
self.base_model = self.make_model()
|
2015-07-21 15:55:02 +00:00
|
|
|
try:
|
2019-08-06 02:11:57 +00:00
|
|
|
if self.opts.scripts:
|
|
|
|
self.run_scripts(self.opts.scripts)
|
2019-09-03 00:58:57 +00:00
|
|
|
|
|
|
|
self.load_controllers()
|
2019-03-06 02:52:13 +00:00
|
|
|
|
|
|
|
initial_controller_index = 0
|
|
|
|
|
2019-08-06 02:11:57 +00:00
|
|
|
if self.updated:
|
2019-09-03 00:58:57 +00:00
|
|
|
initial_controller_index = self.load_serialized_state()
|
2019-03-06 02:52:13 +00:00
|
|
|
|
|
|
|
def select_initial_screen(loop, index):
|
|
|
|
try:
|
|
|
|
self.select_screen(index)
|
|
|
|
except Skip:
|
|
|
|
self.next_screen()
|
|
|
|
|
2019-08-16 02:24:56 +00:00
|
|
|
self.loop.set_alarm_in(
|
2019-08-15 01:50:23 +00:00
|
|
|
0.00, lambda loop, ud: tty.setraw(0))
|
2019-08-06 02:11:57 +00:00
|
|
|
self.loop.set_alarm_in(
|
2019-03-06 02:52:13 +00:00
|
|
|
0.05, select_initial_screen, initial_controller_index)
|
2015-08-31 15:55:46 +00:00
|
|
|
self._connect_base_signals()
|
2019-03-07 02:31:39 +00:00
|
|
|
|
2019-09-09 02:37:48 +00:00
|
|
|
log.debug("starting controllers")
|
2019-03-07 02:31:39 +00:00
|
|
|
for k in self.controllers:
|
2019-08-06 02:11:57 +00:00
|
|
|
self.controller_instances[k].start()
|
2019-09-09 02:37:48 +00:00
|
|
|
log.debug("controllers started")
|
2019-03-07 02:31:39 +00:00
|
|
|
|
2019-08-06 02:11:57 +00:00
|
|
|
self.loop.run()
|
2018-05-24 18:12:55 +00:00
|
|
|
except Exception:
|
2015-07-21 15:55:02 +00:00
|
|
|
log.exception("Exception in controller.run():")
|
|
|
|
raise
|
2018-05-18 02:33:07 +00:00
|
|
|
finally:
|
|
|
|
# concurrent.futures.ThreadPoolExecutor tries to join all
|
|
|
|
# threads before exiting. We don't want that and this
|
|
|
|
# ghastly hack prevents it.
|
|
|
|
from concurrent.futures import thread
|
|
|
|
thread._threads_queues = {}
|