Author Topic: Non-Blocking Console  (Read 3346 times)

0 Members and 1 Guest are viewing this topic.

Mike Lobanovsky

  • Guest
Non-Blocking Console
« on: September 10, 2014, 07:04:00 PM »
You've probably noticed how easy it is to turn your Windows console into a PRINT-only message board in your GUI applications, but how difficult it really is to redirect your interactive keyboard input there while keeping your GUI windows still alive and functioning.

The console's keyboard input loop blocks single threaded applications and their window message pumps become unresponsive to system message flow. This is immediately reported by the system with a "program not responding" message and the application terminates abnormally. Similarly, a GUI window message pump (particularly its TranslateMessage() calls) leaves no chance for the console to get through to the user keystrokes interactively.

There are several ways to resolve such a stalemate. One of them is to set up your own loop that would monitor the state of console input buffer in real time and prevent its attempts to seize the keyboard input exclusively. It will also allow the app windows to receive their messages regularly enough not to arouse any suspition on behalf of the system as to whether the app is more dead than alive.

That's exactly what the following script does. It allows you to interact with your graphics windows without restriction but whenever the user presses a key, the focus is switched immediately to the console and the keypress is echoed there as usual. The script is only a skeleton and it doesn't offer full functionality out of the box but it identifies the key points for further development.

It is interesting that neither OxygenBasic nor FBSL can adequately reproduce the WinAPI structures INPUT_RECORD and KEY_EVENT_RECORD that are needed for interaction with the console input buffer. Their proper C language implementation must be having some weird alignment that O2's PACKED and FBSL's ALIGN 1/2/4/8/etc. directives can't cope with. Yet I managed to set up my own KEY_RECORD equivalent to these structures by trial and error so that the script is equally functional in O2 and FBSL's BASIC. FBSL's DynC would utilize the original C header files so it could use the original INPUT_RECORD/KEY_EVENT_RECORD structures where appropriate.

Note that you can't close the console or GUI window by clicking their [X] buttons. That's the simplest way to prevent uncontrolled app termination by a careless user action.

Code: [Select]
  INCLUDEPATH "$/inc/"
  '$FILENAME  "NBC.exe"
  'INCLUDE    "rtl32.inc"
  INCLUDE "MinWin.inc"
  INCLUDE "Console.inc"
 
  INDEXBASE 0
  #LOOKAHEAD
 
  ! SUB DeleteMenu LIB "user32.dll" (DWORD hMenu, DWORD uPosition, DWORD uFlags)
  ! FUNCTION IsWindowVisible LIB "user32.dll" (DWORD hwnd) AS LONG
  ! FUNCTION PeekConsoleInputA LIB "kernel32.dll" (DWORD hConsoleInput, DWORD lpBuffer, DWORD nLength, DWORD lpNumberOfEventsRead) AS LONG
  ! SUB FlushConsoleInputBuffer LIB "kernel32.dll" (DWORD hConsoleInput)
  ! SUB RtlZeroMemory LIB "kernel32.dll" (DWORD ptr, DWORD size)
  ! SUB WriteConsoleInputA LIB "kernel32.dll" (DWORD hConsoleInput, DWORD lpBuffer, DWORD nLength, DWORD lpNumberOfEventsWritten)
  ! FUNCTION GetConsoleWindow LIB "kernel32.dll" () AS DWORD

  PACKED TYPE KEY_RECORD
    EventType       AS WORD
    wReserved       AS WORD
    bKeyDown        AS DWORD
    wRepeatCount    AS WORD
    wVirtualKeyCode AS WORD
    wScanCode       AS WORD
    wAsciiCode      AS WORD
    dwReserved      AS DWORD
  END TYPE

  DIM cmdline AS ASCIIZ PTR, inst AS SYS, hwnd AS DWORD
  &cmdline = GetCommandLine()
  inst = GetModuleHandle(0)

  ' ===================================================================
  WinMain inst, 0, cmdline, SW_SHOW

  SetConsoleTitle ":: Non-Blocking Oxygen Console Demo ::   (F1 shows GUI, Esc quits)"
 
  DIM AS DWORD hcons  = GetConsoleWindow()                   ' console hwnd
  DIM AS DWORD hstdin = GetStdHandle(STD_INPUT_HANDLE)       ' input buffer handle
  DeleteMenu GetSystemMenu(hwnd, 0), SC_CLOSE, MF_BYCOMMAND  ' disable close buttons
  DeleteMenu GetSystemMenu(hcons, 0), SC_CLOSE, MF_BYCOMMAND
 
  MainLoop()

  END
  ' ===================================================================

  SUB MainLoop()
    CONST MAXRECORD = 8
    DIM IR[MAXRECORD] AS KEY_RECORD       ' read 8 records max
    DIM AS LONG i, read                   ' number of records actually read
    DIM w AS WORD                         ' virtual keycode
    DIM c AS BYTE                         ' key ASCII
   
    DO                                    ' main program loop
      PeekConsoleInputA hstdin, @IR[0], 8, @read
      FOR i = 0 TO < MAXRECORD
        IF IR[i].EventType = 1 THEN ' KEY_EVENT
          IF IR[i].bKeyDown THEN
            w = IR[i].wVirtualKeyCode
            c = IR[i].wAsciiCode
            SELECT w                      ' add navigation subs here
              CASE VK_UP
              CASE VK_DOWN
              CASE VK_LEFT
              CASE VK_RIGHT
              CASE VK_END
              CASE VK_HOME
              CASE VK_INSERT
              CASE VK_DELETE
              CASE VK_F1
                IF NOT IsWindowVisible(hwnd) THEN
                  ShowWindow hwnd, SW_SHOWNOACTIVATE
                END IF
                EXIT FOR                  ' don't fall thru to print
              CASE ELSE
            END SELECT
            SELECT c                      ' add backspace/enter subs here
              CASE 0 TO 7                 ' unprintables
              CASE 8                      ' BACKSPACE currently suppressed
              CASE 13                     ' ENTER currently suppressed
              CASE 27                     ' ESCAPE
                IF IsWindowVisible(hwnd) THEN
                  ShowWindow hwnd, SW_HIDE
                ELSE
                  EXIT SUB                ' quit
                END IF
              CASE ELSE                   ' printables
                PRINT CHR(c)
            END SELECT
            EXIT FOR
          END IF
        END IF
      NEXT
      FlushConsoleInputBuffer hstdin
      RtlZeroMemory &IR[0], MAXRECORD * SIZEOF(KEY_RECORD)
      DoEvents
      Sleep 10
    END DO
  END SUB

  SUB DoEvents()
    SYS bRet
    MSG wm
    STATIC bSwitchToConsole AS LONG
   
    IF PeekMessage(@wm, hwnd, 256, 264, PM_NOREMOVE) THEN ' WM_KEYFIRST, WM_KEYLAST
      bSwitchToConsole = 1               ' ... and don't miss this keypress!
      IF wm.message = WM_KEYDOWN AND wm.wParam <> VK_ESCAPE THEN
        DIM ir AS KEY_RECORD, written AS LONG
        WITH ir.
          EventType       = 1 ' KEY_EVENT
          bKeyDown        = 1 ' TRUE
          wRepeatCount    = 1
          wVirtualKeyCode = wm.wParam
          IF GetAsyncKeyState(VK_SHIFT) AND &HF0000000 THEN
            wAsciiCode = wm.wParam
          ELSE
            wAsciiCode = wm.wParam + 32
          END IF
        END WITH
        WriteConsoleInputA hstdin, @ir, 1, @written
      END IF
    END IF
    IF bSwitchToConsole THEN
      bSwitchToConsole = 0 ' FALSE
      SetForegroundWindow hcons
    END IF
    WHILE bRet := PeekMessage (@wm, 0, 0, 0, PM_REMOVE)
      IF bRet = -1 THEN
        '
      ELSE
        TranslateMessage @wm
        DispatchMessage @wm
      END IF
    WEND
  END SUB

  FUNCTION WinMain(SYS inst, prevInst, ASCIIZ* cmdline, SYS show) AS SYS
    WndClass wc
    SYS wwd, wht, wtx, wty, tax
   
    WITH wc.
      style = CS_HREDRAW or CS_VREDRAW
      lpfnWndProc = @WndProc
      cbClsExtra = 0
      cbWndExtra = 0   
      hInstance = inst
      hIcon = LoadIcon 0, IDI_APPLICATION
      hCursor = LoadCursor 0,IDC_ARROW
      hbrBackground = GetStockObject WHITE_BRUSH
      lpszMenuName = NULL
      lpszClassName = strptr "Demo"
    END WITH
   
    RegisterClass (@wc)
     
    Wwd = 320 : Wht = 200
    Tax = GetSystemMetrics SM_CXSCREEN
    Wtx = (Tax - Wwd) / 2
    Tax = GetSystemMetrics SM_CYSCREEN
    Wty = (Tax - Wht) / 2
     
    hwnd = CreateWindowEx 0, wc.lpszClassName, "OXYGEN BASIC", WS_OVERLAPPEDWINDOW, Wtx, Wty, Wwd, Wht, 0, 0, inst, 0
    UpdateWindow hwnd
  END FUNCTION

  FUNCTION WndProc ( hWnd, wMsg, wParam, lparam ) as sys callback
    STATIC AS SYS hdc
    STATIC AS STRING txt
    STATIC AS PAINTSTRUCT Paintst
    STATIC AS RECT crect
     
    SELECT wMsg
      CASE WM_CREATE
        GetClientRect hWnd, &cRect
      CASE WM_DESTROY
        PostQuitMessage 0
      CASE WM_PAINT
        GetClientRect hWnd, &cRect
        hDC = BeginPaint(hWnd, &Paintst)
        SetBkColor hdc, yellow
        SetTextColor hdc, red
        DrawText hDC, "Hello World!", -1, &cRect, 0x25 ' DT_SINGLELINE|DT_VCENTER|DT_CENTER
        EndPaint hWnd, &Paintst
      CASE ELSE
        FUNCTION = DefWindowProc(hWnd,wMsg,wParam,lParam)
    END SELECT
  END FUNCTION ' WndProc

.

JRS

  • Guest
Re: Non-Blocking Console
« Reply #1 on: September 10, 2014, 07:37:12 PM »
Mike,

Charles did a console interface in DLLC which might be helpful.


Mike Lobanovsky

  • Guest
Re: Non-Blocking Console
« Reply #2 on: September 11, 2014, 02:53:50 AM »
This isn't a console interface implementation, John. Oxygen's classic console interface is implemented in Console.inc which this script uses unmodified as an include file. This script is simply its functional extension for console-mostly applications where CUI is the main instrument of man-computer interaction and a GUI window or windows is or are but supplementary visual aids.

For example, imagine an SBLisp or FBLisp console interpreter where this GUI window is equipped with a set of progress bars that are following the interpreter's inner states in real time -- its heap memory growth and garbage collection, symbol hash table population, current position of "code pointer" within the program's body, and what not -- while the interpreter communicates with you and runs its programs in its natural console mode. And it's only one of its possible uses; yet another window may be used concurrently for direct graphical output of interpreted programs.

This policy wouldn't interfere with, or break, the existing structure of the imaginary interpreter as a console program. It will simply extend its meager visualization capabilities in what regards its real time operation and program output. It's almost exactly what Rob is doing by interbreeding his LISP's with Oxygen and/or Java. But Rob's setups are different processes where there's no blocking action of console input in one process on the graphical output in another one, which isn't all that simple to achieve in a single-threaded all-in-one console-mostly interpreter.

Charles Pegge

  • Guest
Re: Non-Blocking Console
« Reply #3 on: September 11, 2014, 01:03:46 PM »
Thanks Mike,

May I include your example in examples/console ?


Using a thread to capture console input without blocking:

A flag is used to monitor and control the input thread.

0 do nothing but sleep
1 allow input
2 input string is ready to read
3 exit the input thread


Code: [Select]
include "$\inc\minwin.inc"
include "$\inc\console.inc"

SetConsoleTitle "Threaded Console Input: 'q' to quit"

string sa 'shared input string
sys    ra 'shared input flag

function InputThread(sys p) as sys callback
===========================================
do
  if ra=1 then
    sa=rtrim input
    ra=2 'signal input available
  elseif ra=3 'quit
    exit do
  else
    sleep 20
  end if
end do
end function

thread=CreateThread 0,0,@InputThread,0,0,0

sys c
ra=1 'ready for input
do
  c++ 'activity
  if ra=2 then
    if sa="q" then
      ra=3 'quit
      exit do
    end if
    print c " activity cycles" cr
    ra=1
  end if
  sleep 20
end do

sleep 100
CloseHandle thread
'waitkey

Mike Lobanovsky

  • Guest
Re: Non-Blocking Console
« Reply #4 on: September 11, 2014, 04:23:43 PM »
Yes Charles,

Of course you may and it will be my honor if you do.

Your multithreaded solution is academically elegant when seen barebones-style. But I think it will get at least as complicated as mine as soon as you actually try to redirect something from the window message pipe (non-existent in your code) into your input() so it could read that something from its console input buffer. :)

Frankolinox

  • Guest
Re: Non-Blocking Console
« Reply #5 on: September 12, 2014, 01:07:19 AM »
thanks mike for your little nice console/gui example. I've changed only for a test yesterday the %WS_OVERLAPPEDWINDOW style with WM_SYSMENU, WM_VISIBLE, WM_MAXIMIZEBOX, WM_MINIMIZEBOD etc to see what happened with "X" closing. only after start I saw this x here at my test, after that, the "X" dissapear (behind or before console window). And yes, without having read charles input function post, I missed this input handling too :) my example I haven't saved on my usb stick, I am at work...

regards, frank

Mike Lobanovsky

  • Guest
Re: Non-Blocking Console
« Reply #6 on: September 12, 2014, 02:11:44 AM »
Hi Frank,

Yes, my code contains an example of how to disallow the user to close a console and/or GUI window by clicking their [X] buttons or pressing Alt+F4. Usually you do it by changing window styles but the shortest and cleanest programmatic way would be just to kill the corresponding item in the existing system menu with a DeleteMenu() call. Also, you can't change the styles of a console window because the system wouldn't let you do it. So this is the only possible way to disable the console menu buttons:

Code: [Select]
  DeleteMenu GetSystemMenu(hwnd, 0), SC_CLOSE, MF_BYCOMMAND  ' GUI window
  DeleteMenu GetSystemMenu(hcons, 0), SC_CLOSE, MF_BYCOMMAND ' console window

Remove these calls if you want your windows to close normally. FBSL shows the corresponding change in the [X] button states immediately but I noticed that O2 windows require some arbitrary redraw cycles to change the visual style of these buttons.

Another one of my console scripts, Text Mode Squash, contains an example of how to also make the window non-resizable by deleting other items in the window's system menu.