Add support for up to 35 boot menu entries (2 pages if >18). To solve the
">10" problem currently experienced by SeaBIOS users (there are no 11, 12, etc.
keys on a keyboard - so impossible to choose the last menu entries if you got
>10 entries because of multiple hard drives / secondary payloads / floppies)
- the boot menu has been extended to the letter keys. NOTE: TPM menu has been
moved from T to M letter: it is at the end of keyboard's 3rd row of letters and
"Trusted" is adjective …
[View More]while "Module" is a noun; alternatively could press '-'.
Also, add support for a numpad. Small USB numpad could be really convenient for
choosing the boot entries at coreboot boards used as (maybe headless) servers.
'/' char on numpad could be used to open the boot menu or to exit it. If there
are >10 boot menu entries - the numpad console interface will be enabled: press
one or two digit keys and then ENTER to confirm your choice, or remove a digit
by pressing the '.Del' key. Also you could call TPM with '-' key at any moment,
or boot with a single key press of your fullsize keyboard.
Signed-off-by: Mike Banon <mikebdp2 at gmail.com>
(patch body is after the testing instructions below)
This "advanced_bootmenu" patch is much more useful when used together
with my "multiple_floppies" patch:
[SeaBIOS] [PATCH v2] ramdisk: search for all available floppy images
instead of one
https://mail.coreboot.org/pipermail/seabios/2018-December/012670.html
Sadly I haven't done the suggestions by Kevin (I have so many unfinished tasks)
so the "multiple_floppies" patch is also not merged yet. But you could install
both patches to your coreboot by executing this script while at ./coreboot dir:
https://pastebin.com/raw/hv9sSuMU
And here is a coreboot image for QEMU with these two patches applied and my
collection of wonderful and useful floppies added to popular the entries list:
https://github.com/mikebdp2/floparchive/blob/master/coreboot.rom?raw=true
Descriptions of the most prominent floppies could be found here:
http://dangerousprototypes.com/docs/Lenovo_G505S_hacking#Useful_floppies
Run this coreboot.rom by executing this QEMU command: (some floppies are 64-bit)
qemu-system-x86_64 -L . -m 768 -localtime -vga vmware -net nic,model=rtl8139 \
-net user -soundhw ac97 -bios ./coreboot.rom -boot menu=on -serial stdio
Best regards,
Mike Banon
diff --git a/src/boot.c b/src/boot.c
index 9f82f3c..f94dd27 100644
--- a/src/boot.c
+++ b/src/boot.c
@@ -463,6 +463,7 @@ get_keystroke(int msec)
* Boot menu and BCV execution
****************************************************************/
+#define BOOTMENU_PAGE_SIZE 18
#define DEFAULT_BOOTMENU_WAIT 2500
// Show IPL option menu.
@@ -478,59 +479,282 @@ interactive_bootmenu(void)
;
char *bootmsg = romfile_loadfile("etc/boot-menu-message", NULL);
- int menukey = romfile_loadint("etc/boot-menu-key", 1);
- printf("%s", bootmsg ?: "\nPress ESC for boot menu.\n\n");
+ int menukey = romfile_loadint("etc/boot-menu-key", 1); // custom menukey
+ printf("%s", bootmsg ?: "\nPress ESC or \\ / slash for boot menu.\n\n");
free(bootmsg);
u32 menutime = romfile_loadint("etc/boot-menu-wait",
DEFAULT_BOOTMENU_WAIT);
enable_bootsplash();
int scan_code = get_keystroke(menutime);
disable_bootsplash();
- if (scan_code != menukey)
+ if (scan_code != menukey && // custom menukey
+ scan_code != 1 && // ESC
+ scan_code != 43 && // '\' char on keyboard
+ scan_code != 53 && // '/' char on keyboard
+ scan_code != 98) { // '/' char on numpad
+ if (scan_code == -1)
+ printf("No key pressed.\n");
+ else
+ printf("Not a menukey pressed.\n");
return;
+ }
while (get_keystroke(0) >= 0)
;
- printf("Select boot device:\n\n");
wait_threads();
- // Show menu items
+ char keyboard_keys[35] = {'1','2','3','4','5','6','7','8','9','0',
+ 'q','w','e','r','t','y','u','i','o','p',
+ 'a','s','d','f','g','h','j','k','l',
+ 'z','x','c','v','b','n'}; /* m = TPM */
+ int numpad_scancodes[10] = { 82, 79, 80, 81, 75, 76, 77, 71, 72, 73 };
+ int numpi = 0; // Key index: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.
+ int digits = 0; // Numerical length of a current choice number.
+ int decode = 0; // Decode the current choice number into a letter?
+ int entry_id = 0;
+ char desc[77];
+
+ printf("Select boot device");
+
+ // Show menu items after counting them and determining a number of pages.
+ // Only 35 boot menu items (36 if to count a TPM) are supported currently.
+
int maxmenu = 0;
struct bootentry_s *pos;
- hlist_for_each_entry(pos, &BootList, node) {
- char desc[77];
+ hlist_for_each_entry(pos, &BootList, node)
maxmenu++;
- printf("%d. %s\n", maxmenu
+
+ if (maxmenu > 10) {
+ if (maxmenu > 35)
+ maxmenu = 35;
+ if (maxmenu > BOOTMENU_PAGE_SIZE)
+ printf(" - page 1 :");
+ else
+ printf(": ");
+ printf(" // press ENTER after your numpad input");
+ if (maxmenu > BOOTMENU_PAGE_SIZE)
+ printf(" - if any -\n "
+ " // - or to switch between the pages...\n");
+ else
+ printf(" (if any)\n\n");
+ } else {
+ printf(":\n\n");
+ }
+
+ hlist_for_each_entry(pos, &BootList, node) {
+ if (entry_id == BOOTMENU_PAGE_SIZE) // Show only the first page.
+ break;
+ printf("%c. %s\n", keyboard_keys[entry_id]
, strtcpy(desc, pos->description, ARRAY_SIZE(desc)));
+ entry_id++;
}
+ int tpm_cshm = 0;
if (tpm_can_show_menu()) {
- printf("\nt. TPM Configuration\n");
+ tpm_cshm = 1;
+ printf("\nm-. TPM Configuration");
}
-
- // Get key press. If the menu key is ESC, do not restart boot unless
- // 1.5 seconds have passed. Otherwise users (trained by years of
- // repeatedly hitting keys to enter the BIOS) will end up hitting ESC
- // multiple times and immediately booting the primary boot device.
- int esc_accepted_time = irqtimer_calc(menukey == 1 ? 1500 : 0);
+ printf("\n> ");
+
+ // Do not restart boot on menukey press, unless DEFAULT_BOOTMENU_WAIT msecs
+ // have passed. Otherwise users (trained by years of repeatedly
hitting keys
+ // to enter the BIOS) will end up hitting menukey multiple times and
+ // immediately booting the primary boot device.
+ int esc_accepted_time = irqtimer_calc(DEFAULT_BOOTMENU_WAIT);
+ int choice = 0, kb_choice = 0;
+ int page_num = 1;
+ int enter = 0;
+ int backspace = 0;
+ int tpm_show_menu = 0;
for (;;) {
scan_code = get_keystroke(1000);
- if (scan_code == 1 && !irqtimer_check(esc_accepted_time))
- continue;
- if (tpm_can_show_menu() && scan_code == 20 /* t */) {
+ if (scan_code == menukey || // custom menukey
+ scan_code == 1 || // ESC
+ scan_code == 43 || // '\' char on keyboard
+ scan_code == 53 || // '/' char on keyboard
+ scan_code == 98) { // '/' char on numpad
+ if (!irqtimer_check(esc_accepted_time))
+ continue;
+ if (digits == 2) // Remove the decoded "(*)"
+ printf(" \b\b\b");
+ /* Remove the existing input before printing a message. */
+ for (; digits > 0; digits--)
+ printf("\b \b");
+ printf("Menukey pressed.\n");
+ return;
+ }
+ kb_choice = 0;
+ /* 4 rows of keyboard_keys: 1 row with numbers, 3 rows with letters.
+ Use any of them to select a boot device (except the TPM
'm-' keys) */
+ // 1st range: 1-9 and 0 (10) keys <==> 2-11 scan codes <==>
1-10 choice
+ if (scan_code >= 2 && scan_code <= 11) kb_choice = scan_code - 1;
+ // 2nd range: Q-P row of letters <==> 16-25 scan codes <==>
11-20 choice
+ if (scan_code >= 16 && scan_code <= 25) kb_choice = scan_code - 5;
+ // 3rd range: A-L row of letters <==> 30-38 scan codes <==>
21-29 choice
+ if (scan_code >= 30 && scan_code <= 38) kb_choice = scan_code - 9;
+ // 4th range: Z-N row of letters <==> 44-49 scan codes <==>
30-35 choice
+ if (scan_code >= 44 && scan_code <= 49) kb_choice = scan_code - 14;
+ // ENTER: (28) on keyboard, (96) on numpad.
+ if (scan_code == 28 || scan_code == 96)
+ enter = 1;
+ // BCKSPC: '<-'(14) and 'Delete'(111) on keyboard, '.Del'(83)
on numpad.
+ if (scan_code == 14 || scan_code == 111 || scan_code == 83)
+ backspace = 1;
+ // TPM keys: 'm'(50) and '-'(12) chars on keyboard, '-'(74) on numpad.
+ if ((scan_code == 50 || scan_code == 12 || scan_code == 74)
&& tpm_cshm)
+ tpm_show_menu = 1;
+
+ if (kb_choice != 0 || tpm_show_menu) {
+ if (kb_choice > maxmenu) {
+ if (!tpm_show_menu)
+ continue;
+ } else {
+ choice = kb_choice;
+ }
+ if (digits == 2) // Remove the decoded "(*)"
+ printf(" \b\b\b");
+ /* Remove the existing input before printing a choice. */
+ for (; digits > 0; digits--)
+ printf("\b \b");
+ if (!tpm_show_menu) {
+ // Choice is any of the detected boot devices ==> lets boot!
+ break;
+ }
+ } else {
+ // Internal/USB Numpad console interface.
+ if (digits < 9) {
+ for (numpi = 0; numpi < 10; numpi++) {
+ if (scan_code == numpad_scancodes[numpi]) {
+ if (maxmenu <= 10) { // Console interface is
not needed.
+ if ((numpi != 0 && numpi <= maxmenu) ||
+ (numpi == 0 && 10 <= maxmenu)) { // 10(0)
+ choice = numpi;
+ enter = 1; // Fake ENTER to boot this
entry now.
+ } else { // If no such an entry, don't try to boot.
+ break;
+ }
+ } else {
+ if (digits == 2) {
+ printf(" \b\b\b"); // Remove the
decoded "(*)"
+ if (choice == 0) {
+ printf("\b\b \b\b"); // Remove "10".
+ digits = 0;
+ }
+ }
+ choice = 10 * choice + numpi;
+ }
+ if (choice > 0) {
+ printf("%d", numpi); // Print the entered digit.
+ digits++;
+ } else {
+ if (10 <= maxmenu)
+ printf("10(0)\b\b\b");
+ else
+ printf("10(?)\b\b\b");
+ digits = 2;
+ }
+ if (choice > 9 && digits == 2) // Decode into a letter.
+ decode = 1;
+ break;
+ }
+ }
+ }
+ if (backspace && digits > 0) {
+ backspace = 0;
+ choice = choice / 10;
+ if (digits == 2) {
+ printf(" \b\b\b"); // Remove the decoded "(*)"
+ // 0 turned into 10: one more Backspace is needed
to remove.
+ if (choice == 0) {
+ printf("\b \b");
+ digits--;
+ }
+ }
+ printf("\b \b"); // Remove the last entered digit.
+ digits--;
+ if (choice > 9 && digits == 2) // Decode into a letter.
+ decode = 1;
+ }
+ if (decode) { // Decode the current choice number into a letter.
+ decode = 0;
+ if (choice <= maxmenu) {
+ printf("(%c)", keyboard_keys[choice-1]);
+ } else {
+ if (tpm_cshm && choice == 36)
+ printf("(m)"); // For TPM.
+ else
+ printf("(?)"); // No matching letter found.
+ }
+ printf("\b\b\b"); // Move a cursor before the "(*)"
+ }
+ }
+
+ if (enter) {
+ enter = 0;
+ if (choice == 0) {
+ if (digits == 2) { // for 0 that turned into 10
+ if (10 <= maxmenu)
+ break;
+ else
+ continue;
+ }
+ // If there are two pages - switch between them.
+ if (maxmenu > BOOTMENU_PAGE_SIZE) {
+ entry_id = 0;
+ page_num = 3 - page_num; // 3 - 1 = 2; 3 - 2 = 1.
+ printf("\n\nSelect boot device - page %d :"
+ " // press ENTER after your numpad input"
+ " - if any -\n "
+ " // - or to switch between the pages...\n",
+ page_num);
+ hlist_for_each_entry(pos, &BootList, node) {
+ if ((page_num == 1 && entry_id ==
BOOTMENU_PAGE_SIZE) ||
+ (page_num == 2 && entry_id == 35))
+ break;
+ if (page_num == 1 || entry_id >= BOOTMENU_PAGE_SIZE)
+ printf("%c. %s\n", keyboard_keys[entry_id],
+ strtcpy(desc, pos->description,
ARRAY_SIZE(desc)));
+ entry_id++;
+ }
+ if (tpm_cshm)
+ printf("\nm-. TPM Configuration");
+ printf("\n> ");
+ }
+ } else {
+ if (choice > maxmenu) {
+ if (tpm_cshm && choice == 36)
+ tpm_show_menu = 1;
+ } else {
+ // Choice is any of the detected boot devices ==>
lets boot!
+ break;
+ }
+ }
+ }
+
+ if (tpm_show_menu) {
+ tpm_show_menu = 0;
+ choice = 0;
+ if (digits == 0)
+ printf("TPM key pressed.");
+ else
+ digits = 0;
printf("\n");
tpm_menu();
+ printf("> ");
}
- if (scan_code >= 1 && scan_code <= maxmenu+1)
- break;
+ }
+
+ if (choice == 0) // 10(0)
+ choice = 10;
+
+ if (digits == 0 && choice < 36) {
+ printf("%c", keyboard_keys[choice-1]);
+ if (choice > 9) // Decode into a number.
+ printf("(%d)", choice);
}
printf("\n");
- if (scan_code == 0x01)
- // ESC
- return;
// Find entry and make top priority.
- int choice = scan_code - 1;
hlist_for_each_entry(pos, &BootList, node) {
if (! --choice)
break;
diff --git a/src/config.h b/src/config.h
index 93c8dbc..f85cc14 100644
--- a/src/config.h
+++ b/src/config.h
@@ -19,7 +19,7 @@
// Space to reserve in high-memory for tables
#define BUILD_MAX_HIGHTABLE (256*1024)
// Largest supported externaly facing drive id
-#define BUILD_MAX_EXTDRIVE 16
+#define BUILD_MAX_EXTDRIVE 36
// Number of bytes the smbios may be and still live in the f-segment
#define BUILD_MAX_SMBIOS_FSEG 600
// Maximum number of bytes the mptable may be and still be copied to f-segment
[View Less]
Hi,
I'm working on stuffing a bootable Linux distro into coreboot. In QEMU I
already succeded by using coreboot's built-in kernel loading mechanism, but
that's without SeaBIOS.
I'd love to have it as a SeaBIOS payload so I can also boot other things,
but I guess I'd have to create a custom-sized floppy image for this or
figure out how to create an ELF payload out of a Linux kernel (I'm open to
either, but I wasn't able to find any documentation on the ELF method).
The guy who put Win 3.1 in …
[View More]coreboot attempted the floppy method, but
according to his article he did not find success with this method due to
unknown and complex issues in the floppy-side logic of SeaBIOS.
So, I'm making the question explicit: What would it take to support
custom-sized floppy images? In particular, I'm thinking of a 16MB device...
Alternatively, would it be possible to create an ELF file out of a Linux
kernel+initrd / bootable image?
Cheers,
Rafael
[View Less]
Check whenever pnp roms attempt to redirect int19, and in case it does
log a message and undo the redirect.
A pnp rom should not need this, we have BEVs and BCVs for that.
Nevertheless there are roms in the wild which are redirecting int19.
At least some BIOS implementations for physical hardware have a config
option in the setup to allow/disallow int19 redirections, so just not
allowing this seems to be the way to deal with this situation.
Buglink: https://bugzilla.redhat.com//show_bug.cgi?…
[View More]id=1642135
Signed-off-by: Gerd Hoffmann <kraxel(a)redhat.com>
---
src/optionroms.c | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/src/optionroms.c b/src/optionroms.c
index fc992f649f..e90900b790 100644
--- a/src/optionroms.c
+++ b/src/optionroms.c
@@ -8,6 +8,7 @@
#include "bregs.h" // struct bregs
#include "config.h" // CONFIG_*
#include "farptr.h" // FLATPTR_TO_SEG
+#include "biosvar.h" // GET_IVT
#include "hw/pci.h" // pci_config_readl
#include "hw/pcidevice.h" // foreachpci
#include "hw/pci_ids.h" // PCI_CLASS_DISPLAY_VGA
@@ -308,6 +309,24 @@ fail:
return NULL;
}
+static int boot_irq_captured(void)
+{
+ struct segoff_s current, seabios;
+
+ current = GET_IVT(0x19);
+ seabios = FUNC16(entry_19_official);
+ return ((current.seg != seabios.seg) ||
+ (current.offset != current.offset));
+}
+
+static void boot_irq_restore(void)
+{
+ struct segoff_s seabios;
+
+ seabios = FUNC16(entry_19_official);
+ SET_IVT(0x19, seabios);
+}
+
// Attempt to map and initialize the option rom on a given PCI device.
static void
init_pcirom(struct pci_device *pci, int isvga, u64 *sources)
@@ -327,8 +346,18 @@ init_pcirom(struct pci_device *pci, int isvga, u64 *sources)
if (! rom)
// No ROM present.
return;
+ int irq_was_captured = boot_irq_captured();
+ struct pnp_data *pnp = get_pnp_rom(rom);
setRomSource(sources, rom, RS_PCIROM | (u32)pci);
init_optionrom(rom, pci->bdf, isvga);
+ if (boot_irq_captured() && !irq_was_captured &&
+ !file && !isvga && pnp) {
+ // This PCI rom is misbehaving - recapture the boot irqs
+ char *desc = MAKE_FLATPTR(FLATPTR_TO_SEG(rom), pnp->productname);
+ dprintf(1, "PnP optionrom \"%s\" (bdf %pP) captured int19, restoring\n",
+ desc, pci);
+ boot_irq_restore();
+ }
}
--
2.9.3
[View Less]
According to SMBIOS Specification, section 6.1.3 Text Strings:
"Text strings associated with a given SMBIOS structure are returned in
the dmiStructBuffer, appended directly after the formatted portion of the
structure. This method of returning string information eliminates the
need for application software to deal with pointers embedded in the
SMBIOS structure. Each string is terminated with a null (00h) BYTE and
the set of strings is terminated with an additional null (00h) BYTE”
Furthermore:
…
[View More]"If the formatted portion of the structure contains string-reference
fields and all the string fields are set to 0 (no string references),
the formatted section of the structure is followed by two null (00h)
BYTES"
From the above it can be seen that any SMBIOS type which contains string
references should end with an additional zero byte.
This is currently handled in all SMBIOS types which use
load_str_field_with_default() besides type0.
Therefore, add the missing zero byte to SMBIOS Type 0.
Running QEMU with:
-machine pc-i440fx-2.0 (for legacy smbios)
-smbios type=0,vendor=,version=,date= (for zero str_index)
Will cause SMBIOS type0 entry to overrun type1 entry.
Reviewed-by: Ross Philipson <ross.philipson(a)oracle.com>
Reviewed-By: Liran Alon <liran.alon(a)oracle.com>
Signed-off-by: Sam Eiderman <shmuel.eiderman(a)oracle.com>
---
src/fw/smbios.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/fw/smbios.c b/src/fw/smbios.c
index f3b5ad9d..62a563b2 100644
--- a/src/fw/smbios.c
+++ b/src/fw/smbios.c
@@ -205,6 +205,10 @@ smbios_init_type_0(void *start)
*end = 0;
end++;
+ if (!str_index) {
+ *end = 0;
+ end++;
+ }
return end;
}
--
2.13.3
[View Less]
When I kexec either Xen or Linux in real mode, from either Xen or
Linux, it fails.
The last thing I see looks like SeaBIOS trying to use SMM for call32:
----------------
IN:
0x00000000000f70ec: mov %eax,%esi
0x00000000000f70ef: mov $0xb5,%eax
0x00000000000f70f5: mov $0x1234,%ecx
0x00000000000f70fb: mov $0xef3dc,%ebx
0x00000000000f7101: out %al,$0xb2
0x00000000000f7103: pause
----------------
IN:
0x00000000000ef3db: hlt
This happens when the real mode boot code …
[View More]calls INT 13h to read from
the disk. It seems to happen with virtio and SATA disks.
This is with the Ubuntu-packaged 1.10.2-1ubuntu1 SeaBIOS. Switching to
an IDE disk, or booting with 'edd=skipmbr', makes Xen work and Linux
get a little further before it dies anyway.
[View Less]
Dear Gerd Hoffman,
If my advanced_bootmenu new patch ( [*] link below ) is not
upstream-able ( for what reasons? ), I will be happy if your
alternative solution gets merged - at least because it is better than
what's currently in SeaBIOS master. However, I just tested your
patches and noticed some issues which did not exist at my original
version [*] - https://mail.coreboot.org/hyperkitty/list/seabios@seabios.org/thread/CKWLNT…
. I hope that you could review my feedback and address at least …
[View More]some
of them :
1) It is not possible to choose the boot entry by a single key press
of a fullsize keyboard. We have so many keyboard keys, why are we
limited to 1234567890 ? Seems inefficient. The alternative way, which
I followed, is to continue with qwertyuiop etc. - to utilize 4 rows of
keyboard keys - and make a single key press selection possible.
2) If there are >10 menu entries and you accidentally press '0'
instead of '1', there is no way to backspace/delete this wrong
character. Although, if there is an ability to remove the wrong
characters, then a confirmation of a correct input (e.g. by pressing
the Enter key) is also needed - to confirm that your last character
wasn't wrong. I implemented this mini console interface in my version
- but only for a numpad; for keyboard it wasn't needed because it is a
single key boot there.
3) If there are >20 menu entries, the first menu entries may not fit
on a screen. (That's why I had to add 2 pages support.) Perhaps you
think it's unlikely that anyone would need >20 menu entries: however,
if you are a floppy-based OS developer who is debugging something that
could be reproduced only at bare metal (so no QEMU) - you may want to
add a lot of different versions of your floppy to your coreboot build
- to reduce the amount of BIOS chip reflashes and just for your
convenience. I stopped at 35 max simply because ran out of keyboard
letter keys (could have gone double-letter but wanted to preserve a
single key boot).
4) Although a few USB numpads do have an Escape key (e.g. Motospeed
K23), the majority of them do not - so it is impossible for them to
open a boot menu by pressing Escape - simply because no such key.
(That's why I added "/" character to the menukeys - every USB numpad
has this key.) Also, in your patch version it's not possible to access
TPM from a numpad because no t or m keys on it - so I had to add '-'
character to the menukeys.
To summarize, perhaps the only advantage of your current patch version
is that it is 3 times smaller, so 3 times more likely to be merged.
However it doesn't necessarily mean that my original patch is
inefficient or bloated: I optimized it as best as I could, and quite
sure that if you'd have tried to implement the features mentioned
above - as well as some others I may have forgot to mention - then
your patch linecount would've jumped too.
P.S. If there are any issues with my patch, which have persuaded you
to create your own version (aside from the size issue) - I am open to
suggestions and could try to address them when I will have enough free
time.
Best regards,
Mike Banon
[View Less]
Dear SeaBIOS folks,
It was announced already on the coreboot mailing list [1], so some might
already know this.
redsPL got SeaBIOS and a Win 3.1 (floppy disk) (with coreboot) booting [2].
> Because the standard coreboot payload with SeaBIOS only occupies only
> about 1MB out of the full 8 on the ROM, I thought about embedding a
> full OS on the remaining 7 megs. It turns out that SeaBIOS has
> partial support for booting from flash - it sets up a virtual floppy
> drive with …
[View More]your floppy of choice loaded from floppyimg/<name>.lzma,
> but this gives us at best 2.88MB (standard ED floppy disk size,
> exactly 2x as large as a popular 1.44M), and I wanted more.
I guess it should be reproducible in QEMU too.
Kind regards,
Paul
[1]: https://mail.coreboot.org/hyperkitty/list/coreboot@coreboot.org/thread/2QH5…
[2]: https://sakamoto.pl/weblog/07-windows-3-flash-edition/
[View Less]