Quote:
Originally Posted by systemlordanubis
Going on with your points, in Debian 6, what would be the preferred method of making a boot splash for both boot and init; a quick search reveals that this would require a kernel re-compilation; if so, what is the best package to achieve this?
|
I'd look at
plymouth (
Debian Wiki) first. I think you can create a text-only themes for it, but I'm not sure; I haven't played with it.
Alternatively, you could use a
quiet boot, i.e. no splash during kernel boot at all.
Quote:
Originally Posted by systemlordanubis
Ideally, I'd like to have some sort of a visual header (again ASCII or image) then some real-time status information followed by some pre-defined menu options.
|
For text one, a dash script should be sufficient.
For framebuffer console, you might be able to use
fim, although you could control it fully through a C program. If you convert your images to 24-bit PPM (PNM) format, it is very easy to "blit" images to the framebuffer. One of the problems you may encounter is that if your program is dependent on libraries, you cannot start it too early in the boot sequence. If you compile your program statically, then there is no problem, and you can start it as early as you wish.
Not all systems support a framebuffer console (I'm specifically thinking about some server hardware), so if you want maximum compatibility, use text mode only.
Quote:
Originally Posted by systemlordanubis
You wouldn't happen to be willing to share your interactive version or a stripped-down version of your console?
|
Perhaps we could develop something here together instead?
First, I use this trivial program I call
waitesc.c:
Code:
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <signal.h>
#include <errno.h>
/* Alarm signal handler (does nothing, just interrupts)
*/
static void alarm_handler(int signum)
{
return;
}
/* Cancel interrupts.
*/
static int cancel_interrupt(void)
{
struct itimerval timer;
timer.it_interval.tv_sec = 0L;
timer.it_interval.tv_usec = 0L;
timer.it_value.tv_sec = 0L;
timer.it_value.tv_usec = 0L;
if (setitimer(ITIMER_REAL, &timer, NULL) == -1)
return errno;
return 0;
}
/* Set interrupting timeout.
* Returns 0 if success, errno otherwise.
*/
static int set_interrupt(const struct timeval timeout)
{
struct sigaction act;
struct itimerval timer;
int saved_errno;
/* Save errno so we keep it unchanged if successful. */
saved_errno = errno;
/* Set alarm signal handlers. */
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = alarm_handler;
if (sigaction(SIGALRM, &act, NULL) == -1)
return errno;
/* This timer repeats a hundred times a second. */
timer.it_interval.tv_sec = 0L;
timer.it_interval.tv_usec = 10000L;
/* Set expiry time. */
timer.it_value.tv_sec = timeout.tv_sec;
timer.it_value.tv_usec = timeout.tv_usec;
/* Arm timer. */
if (setitimer(ITIMER_REAL, &timer, NULL) == -1)
return errno;
/* Done. */
errno = saved_errno;
return 0;
}
/* Helper function: Write to standard error.
*/
static int wrerr(const void *const message)
{
const char *head = (const char *)message;
const char *tail = (const char *)message;
ssize_t written;
int saved_errno;
/* If no message, return immediately. */
if (!head || !*head)
return 0;
/* Save errno. */
saved_errno = errno;
/* Find end of string. */
while (*tail)
tail++;
/* Write loop. */
while (head < tail) {
written = write(STDERR_FILENO, head, (size_t)(tail - head));
if (written > (ssize_t)0)
head += written;
else
if (written != (ssize_t)-1)
return errno = EIO;
else
if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK)
return errno;
}
/* Done. */
errno = saved_errno;
return 0;
}
static int parse_seconds(const char *s, struct timeval *t)
{
long sec, old, usec;
int negative = 0;
if (!s)
return ENOENT;
/* Skip leading whitespace and signs. */
while (*s == '\t' || *s == '\n' || *s == '\v' ||
*s == '\f' || *s == '\r' || *s == ' ' ||
*s == '+' || *s == '-')
if (*(s++) == '-')
negative = !negative;
/* Negative is unacceptable. */
if (negative)
return EDOM;
/* Full seconds? */
if (*s >= '0' && *s <= '9') {
sec = *(s++) - '0';
while (*s >= '0' && *s <= '9') {
old = sec;
sec = 10L * sec + (*(s++) - '0');
if (sec < old)
return EDOM;
}
} else
if (*s == '.' || *s == ',') {
sec = 0L;
} else
return ENOENT;
/* Fractional part? */
if (*s == '.' || *s == ',') {
s++;
usec = 0L;
if (*s >= '0' && *s <= '9') usec += 100000L * (*(s++) - '0');
if (*s >= '0' && *s <= '9') usec += 10000L * (*(s++) - '0');
if (*s >= '0' && *s <= '9') usec += 1000L * (*(s++) - '0');
if (*s >= '0' && *s <= '9') usec += 100L * (*(s++) - '0');
if (*s >= '0' && *s <= '9') usec += 10L * (*(s++) - '0');
if (*s >= '0' && *s <= '9') usec += 1L * (*(s++) - '0');
while (*s >= '0' && *s <= '9')
s++;
} else
usec = 0L;
/* Skip trailing whitespace. */
while (*s == '\t' || *s == '\n' || *s == '\v' ||
*s == '\f' || *s == '\r' || *s == ' ')
s++;
/* Trailing garbage? */
if (*s)
return EINVAL;
/* Save time. */
if (t) {
t->tv_sec = sec;
t->tv_usec = usec;
}
return 0;
}
int main(int argc, char *argv[])
{
struct timeval timeout;
char buffer[65536];
ssize_t bytes;
if (argc != 2) {
wrerr("\n"
"Usage: "); wrerr(argv[0]); wrerr(" seconds\n"
"\n"
"This program will read from standard input.\n"
"When input is available, it is returned immediately,\n"
"otherwise this waits for up to the specified time.\n"
"Fractional seconds like 0.001 work just fine.\n"
"\n"
"ASCII control characters are escaped using backslash\n"
"followed by three octal digits. Backslash itself is\n"
"escaped as \\134.\n"
"\n");
return (argc == 1) ? 0 : 1;
}
if (parse_seconds(argv[1], &timeout)) {
wrerr(argv[1]);
wrerr(": Invalid timeout (in seconds).\n");
return 1;
}
/* Clamp minimum timeout to ten milliseconds. */
if ((timeout.tv_sec < 0L) ||
(timeout.tv_sec == 0L && timeout.tv_usec < 10000L)) {
timeout.tv_sec = 0L;
timeout.tv_usec = 10000L;
}
if (set_interrupt(timeout)) {
wrerr(argv[1]);
wrerr(": Cannot set timeout.\n");
return 1;
}
/* Read. */
bytes = read(STDIN_FILENO, buffer, sizeof(buffer)/4);
if ((bytes == (ssize_t)-1) &&
(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN))
bytes = 0;
/* Cancel timeout. */
cancel_interrupt();
/* Anything to output? */
if (bytes > (ssize_t)0) {
unsigned char *in_head = (unsigned char *)buffer;
unsigned char *in_tail = (unsigned char *)buffer + bytes;
unsigned char *out_head = (unsigned char *)buffer + sizeof(buffer);
unsigned char *out_tail = (unsigned char *)buffer + sizeof(buffer);
while (in_tail-->in_head)
if (*in_tail < 32 || *in_tail == 92 || *in_tail == 127) {
*(--out_head) = '0' + ( *in_tail & 7U);
*(--out_head) = '0' + ((*in_tail >> 3U) & 7U);
*(--out_head) = '0' + ((*in_tail >> 6U) & 3U);
*(--out_head) = '\\';
} else
*(--out_head) = *in_tail;
while (out_head < out_tail) {
bytes = write(STDOUT_FILENO, out_head, (size_t)(out_tail - out_head));
if (bytes > (ssize_t)0)
out_head += bytes;
else
if (bytes != (ssize_t)-1)
return 2;
else
if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK)
return 2;
}
}
return 0;
}
Compile and install it as
/sbin/waitesc using
Code:
gcc waitesc.c -Wall -O3 -static -s -o waitesc
sudo install -m 0755 waitesc /sbin/waitesc
waitesc is a replacement for both reading input and sleeping between display updates. It takes one argument, the maximum sleep time in seconds (minimum is 0.01 or one hundredth of a second). If there is input, it outputs the input immediately, otherwise it waits for input, up to the specified wall clock time limit. It escapes all ASCII control characters using octal notation (
\nnn) and backslash as
\134.
(It works by setting an alarm, initially at the desired time, and every 0.01s afterwards. The alarm handler does nothing. The signal delivery will interrupt the read waiting for input. After the read is interrupted, the alarm is canceled. This makes sure the read will be interrupted, without using a pipe and select() to work around a race condition between the timeout and the read. Only a fourth of the output buffer is used for input, because any input byte may expand to four bytes in output. I scan the input buffer for bytes to escape in reverse order, filling the output buffer starting from the end of the buffer; this way the input and output contents will never overlap, and I only need one buffer.)
If you use
stty -icanon -echoto set the tty to raw mode, you can use
waitesc to read individual keypresses. To return the terminal to normal state, use
stty icanon echo .
Here is a table of special keys and what
waitesc outputs:
Code:
Backspace: \010 or \177
Tab: \011
Enter: \012
ESC: \033
Insert: \033[2~
Delete: \033[3~
PageUp: \033[5~
PageDown: \033[6~
Home: \033OH
End: \033OF
Up: \033[A
Down: \033[B
Left: \033[D
Right: \033[C
Note that the right side is exact, and will need to be escaped or quoted when written in a shell script. Non-ASCII characters and ASCII printable characters will be shown as-is in the output.
Let's start with this skeleton dash script. It does nothing but update the time every half second or so, showing the update counter, and let the user move an imaginary (not drawn) cursor around using the arrow keys. Only the coordinates are calculated for the imaginary cursor. It uses
/sbin/waitesc to get user input and the half second wait between updates, and
/bin/stty to control the terminal; otherwise only dash built-ins are used.
Code:
#!/bin/dash
# System console (tty1) LQ example script
# License: CC0 (Public Domain), see
# http://creativecommons.org/publicdomain/zero/1.0/
# You run this script on your own risk!
# Before anything else, ignore INT, QUIT, TSTP, and PIPE signals.
trap "" INT QUIT TSTP PIPE
# If terminated, reset the terminal to sane state.
trap "/bin/stty icanon echo ; printf '\033[?25h\033[0m\033[2J\033[1;1H' ; exit 0" TERM
# Make sure we are connected to the tty1,
# but redirect errors to /dev/null.
exec </dev/tty1 >/dev/tty1 2>/dev/null
# Comment out the above line, if you run in a virtual terminal (e.g. xterm).
# Set terminal to raw mode, so we get individual keypresses.
/bin/stty -icanon -echo
# ANSI: Disable line wrapping.
printf '\033[7l'
# ANSI: Clear all tab stops.
printf '\033[3g'
# ANSI: Hide cursor.
printf '\033[?25l'
# ANSI: Clear screen, and move cursor to upper left corner.
printf '\033[2J\033[1;1H'
# Find out the size of the screen.
COLS=$(stty size | (read rows cols ignore && printf '%d' $cols))
ROWS=$(stty size | (read rows cols ignore && printf '%d' $rows))
# Set safe defaults for screen size, just in case.
[ -n "$COLS" ] || COLS=80
[ -n "$ROWS" ] || ROWS=25
# No field separator.
unset IFS
# Flush input buffer.
/sbin/waitesc 0.01 >/dev/null
# ANSI sequence to clear the rest of the line, and move to next line.
# Use this instead of clearing the screen, to avoid flicker when updating.
END="\033[0m\033[K\n"
# Just some variables modified in the main loop.
counter=0
x=1
y=1
# Main loop.
input=""
while [ 1 ]; do
# Parse input.
while [ -n "$input" ]; do
case "$input" in
\\010*|\\177*) # Backspace
input="${input#????}"
# Just a placeholder, does nothing.
;;
\\033\[\[3\~*) # Delete
input="${input#????????}"
# Just a placeholder, does nothing.
;;
\\012*|\\015*) # Enter
input="${input#????}"
# Just a placeholder, does nothing.
;;
\\011*) # Tab
input="${input#????}"
# Just a placeholder, does nothing.
;;
\\033\[A*) # Up
input="${input#??????}"
y=$(( y - 1 ))
[ $y -lt 1 ] && y=1
;;
\\033\[B*) # Down
input="${input#??????}"
y=$(( y + 1 ))
[ $y -gt $ROWS ] && y=$ROWS
;;
\\033\[D*) # Left
input="${input#??????}"
x=$(( x - 1 ))
[ $x -lt 1 ] && x=1
;;
\\033\[C*) # Right
input="${input#??????}"
x=$(( x + 1 ))
[ $x -gt $COLS ] && x=$COLS
;;
\ *) # Space
input="${input#?}"
# Just a placeholder, does nothing.
;;
*) # Ignore all others.
input="${input#?}"
;;
esac
done
# Update counter.
counter=$(( counter + 1 ))
# Start updating by moving to upper left corner.
# Does NOT clear the screen.
printf '\033[1;1H'
# Output date (in brown).
printf '\033[0;33m%s'$END "$(date --rfc-3339=seconds)"
# Output an empty line.
printf $END
# Output counter using light and dark red:
printf '\033[1;31mCounter\033[0;31m = \033[1;31m%d'$END $counter
# Output X and Y coordinates using light and dark green:
printf '\033[1;32mX\033[0;32m = \033[1;32m%d\033[0;32m, \033[1;32mY\033[0;32m = \033[1;32m%d'$END $x $y
# Output an empty line.
printf $END
# Get new input, but don't spend more than half a second.
input="$input$(/sbin/waitesc .5)"
done
Note that this script cannot be killed from the keyboard. It does exit nicely (restoring the terminal to normal state) if you send a TERM signal to it, however. And you can use Alt+Fn, Alt+Left, and Alt+Right to switch to a getty for a login. (This type of TERM signal handling is important, as otherwise you'll have trouble shutting down the machine.)
If you are targeting a minimal, embedded system, then all you might need is an initrd. You'd need to copy both your script and the utility commands it uses to the ramdisk. (There is really no need to have a "real" root filesystem; the initrd works fine for that. The space is quite limited, though.)
For a Debian server, you should just remove the getty for tty1, and replace it with this script. Edit either
/etc/inittab or
/etc/init/tty1.conf (whichever you have); replace
/sbin/getty -8 38400 tty1 with
/path/to/the/above/dash/script .
I recommend using a virtual machine to test and develop this. VirtualBox is quite suitable. If you make a checkpoint immediately after installing Debian on it, developing the modifications to the init system is very easy, because you can always restore the machine state to any checkpoint you've made. Also, the virtual machine boots up much faster, so you'll save a lot of time, too.
Edit to add: For the ANSI escape codes used to change text and background colors, moving the cursor around, and so on, see
ANSI escape code Wikipedia page, and
ANSI escape sequences at ascii-table.com.