notify-portable

notify-portable: a set of C functions for platform/environment independent notifications
Login

notify-portable: a set of C functions for platform/environment independent notifications

Introduction

The main purpose for these C functions is to display notifications for applications in different environments (CGI, Console, GUI, Log).
The functions should be portable between environments and platforms.

Unless using an explicit call, the environment should be automatically detected.
Thus, calling for example the frontend function

np_info("Hello, world!\n");

would automatically choose one of the following backend functions, depending on the detected environment and platform:

All frontend functions use the printf(3) function format string syntax.

Special use case in mind: file name arguments

These C functions are generic in nature, but have been designed with a special case in mind:
When starting an application and passing file names as arguments, display whatever notification during the verification of the file names.

Usage examples:

Passing file name arguments in different environments

The file name arguments may be passed to the application in different ways:

Platform dependent environment detection

To decide which backend function to call for output, the environment must first be detected.
This may vary between platforms. These are the following steps for environment detection:

  1. The CGI environment is probably the easiest environment to detect, as it is platform independent.
    Usually it is sufficient to check a set of environment variables to be present, and another set which should be absent.

  2. A Console environment may be more complicated to detect, as a console application may run in different environments:

    • In a text-only environment (may be true for for a Linux/BSD system not running an X server).
    • As a console window in a GUI environment (always true for MS Windows, may be true for Linux/BSD)
      Besides, the method for detecting a console window is different for the type of console:
      • MS Windows, CMD Command Prompt: cmd.exe
      • MS Windows, MSYS2/MINGW64/Cygwin console: mintty
      • Linux/BSD: xterm etc. (normally one single detection method can be used for all kind of Linux/BSD consoles)
  3. A GUI environment is easy to detect when a console environment has been discharged.
    Additional detection methods may include reading the PE header (MS Windows) or the $DISPLAY environment variable (Linux/BSD).
    The backend funtion is platform dependent:

    • MS Windows: MessageBox()
    • Linux/BSD: zenity, kdialog, or xmessage (whatever that is detected at run-time)
  4. A Log "environment" is not a real environment in this context. It is only used if the detection of the other environments fails.

Platform dependent input/output

Once the environment has been detected, the proper backend function for notification has to be called. This may be also platform dependent.

Generally, Console and CGI input/output functions are platform independent.
For example, printf() and scanf() are included in every C library, and can be used for printing to/reading from directly using any console input/output.
Creating HTML blocks in CGI applications is also a platform independent call.
Note that scanf() is only used for console confirm messages.

On the other hand, GUI input/output functions are platform dependent.
The MsgBox() function is very MS Windows specific, and xmessage is normally only available on Linux/BSD systems.

Printing to a Log file is platform independent, using fprintf(LOGFILE, ...) on any platform.

Split source code files by platform

Not everyone are interested in using code for all platforms.
The source code has been designed with the idea that it should be easy to grab the source code files for one single platform, so code blocks such as

<common source code>
#if defined (__linux__)...
<Linux-specific source code>
#elif defined (_WIN32)...
<MS Windows-specific source code>
#endif
<more common source code>

have been avoided as much as possible.
The compile time OS-related directive have instead been included outside the source code, at a higher level, in the Makefile and in common headers.

Compile time OS-related directives here:

https://blog.kowalczyk.info/article/j/guide-to-predefined-macros-in-c-compilers-gcc-clang-msvc-etc..html

Win32 platform notes

Win32 application compiling

On MS Windows, 3 different ways to compile an application have been tested.
Depending on the compiler, the resulting application may have a DLL dependency or not:

  1. MINGW64 shell - No DLL dependency
  2. MSYS2 shell - DLL dependency: msys-2.0.dll
  3. Cygwin shell - DLL dependency: cygwin1.dll

Win32 application launching

On MS Windows, 5 different ways to launch an application have been tested:

  1. MINGW64 shell
  2. MSYS2 shell
  3. Cygwin shell
  4. CMD prompt (cmd.exe)
  5. File Explorer/GUI (double-click to launch)

Generally, GUI applications may be launched using any of the 5 ways.
Console applications may be launched using any console application (1-4) except File Explorer.
(Note that console applications are not limited to text-only output, but may also create GUI windows.)

Anyhow, due to DLL dependencies, applications compiled using the MSYS2 shell may only be launched from either a MSYS2 or a MINGW64 shell.
The same is true for applications compiled using the Cygwin shell, which may only be launched from a Cygwin shell.
To launch an application using any of the 5 ways, always compile it using the MINGW64 shell.

Win32 environment detection

It is important to detect the correct launching application to know whether to use printf() or MessageBox() as a backend function for output.
To compile an application as either a GUI or Console, the -mconsole (default on MINGW64) or -mwindows compiler flag is used.

When compiled as a Console program, printf() will always work, launch from either a Console or the File Explorer.
(In this case, when launched from the File Explorer, an attached console is opened, and any printf()output goes to that console.)

When compiled as a GUI program (i.e., using -mwindows), the program can be perfectly launched from a console, but printf() output will only be visible if launched from a MINGW64/MSYS2/Cygwin console, but not from the CMD prompt.
A simple workaround for this would be to always use MessageBox() for notifications, but there are more sophisticated methods to detect which output function to use at runtime.

Detect how a Win32 application has been compiled
  1. Read the PE Subsystem header:

    • PE Subsystem header = IMAGE_SUBSYSTEM_WINDOWS_CUI => Compiled with: -mconsole
    • PE Subsystem header = IMAGE_SUBSYSTEM_WINDOWS_GUI => Compiled with: -mwindows
  2. Check if a GUI application has an attached console:

    (GetStdHandle(STD_OUTPUT_HANDLE) != INVALID_HANDLE_VALUE) /* Attached console, compiled with -mconsole */
    (GetStdHandle(STD_OUTPUT_HANDLE) == INVALID_HANDLE_VALUE) /* No attached console, compiled with -mwindows */
    
  3. Check the console's Code Page:

    (GetConsoleOutputCP() != 0) /* Console Code Page may vary, but always non-zero for a console => -mconsole */
    (GetConsoleOutputCP() == 0) /* Console Code Page is 0 => -mconsole */
    
Detect how a Win32 application has been launched
  1. Check the PROMPT variable:

    (getenv("PROMPT") != NULL) /* Launched from CMD prompt */
    (getenv("PROMPT") == NULL) /* Launched from File Explorer (or from MINGW64/MSYS2/Cygwin) */
    
  2. Check the MSYSTEM variable:

    (!strncmp(getenv("MSYSTEM"), "MINGW64")) /* Launched from MINGW64 shell */
    (!strncmp(getenv("MSYSTEM"), "MSYS"))    /* Launched from MSYS2 shell */
    (getenv("MSYSTEM") == NULL)              /* Launched from File Explorer, CMD prompt or Cygwin) */
    
  3. The MINGW64/MSYS2/Cygwin shells use named pipes to emulate a console.
    To get the name of the pipe, call NtQueryInformationFile(), which returns a name like this:

    \cygwin-e022582115c10879-pty0-from-master /* Launched from Cygwin shell */
    \msys-dd50a72ab4668b33-pty0-from-master   /* Launched from MINGW64 or MSYS2 shell */
    
  4. Use the _isatty() call to check if STDIN, STDOUT and/or STDERR are terminal types:

    ((_isatty(0) || _isatty(1) || _isatty(2))  /* Launched from CMD prompt */
    !((_isatty(0) || _isatty(1) || _isatty(2)) /* Launched from File Explorer or from MINGW64/MSYS2/Cygwin */
    

    Note that it is possible to simultaneously use redirection for both STDIN, STDOUT and STDERR, as shown in the following command:

    dir | ./test-if-my-app-is-a-terminal.exe  > my-app.log 2>&1
    

    In this case ((_isatty(0) || _isatty(1) || _isatty(2)) will return 0.

%%% TODO %%% Add "detected console/gui/cgi type" functions to API, not as "private functions"

TODO: REDUCE THE SECTION BELOW (BEFORE API)

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Environment detection

The output format (and function used) depends on the current environment, so the environment has to be detected, in this order: TTY, GUI, or CGI.

Environment: TTY

Note 1:

Theoretically, a call to isatty(0) would suffice to check if the current environment is a terminal or not. In practice, things get a bit more complicated. Not surprisingly, MS Windows

Note 2: It is possible to simultaneously use redirection for both STDIN, STDOUT and STDERR, as shown in the following command:

    dir | ./isatty-for-native-apps.exe  > kk.txt 2>&1

In such a case, the TTY environment would not be detected, as (isatty(0) || isatty(1) || isatty(2)) returns false.
As an extra check, the $TERM environment variable may be queried.
This works for any UNIX-like system as well as for MSYS2/MINGW64/CYGWIN terminals.
The exception is the MS Windows CMD promt, which does not recognize the %TERM% variable.

Environment: GUI

Environment: GUI: MS Windows

Environment: GUI: Linux/*BSD/UNIX

Note:
There are several different graphic environments available for Linux/*BSD/UNIX, many of them with there own GUI dialog boxes.
To take all of them into account would imply linking np_gui_x() to several graphic libraries. Instead, to avoid the graphic library linking, available dialog/notifying/message programs are detected at runtime.

The output function used depends on the detected available program(s) in the following order:

  1. np_env_os_dialog(NP_TYPE, NP_ENV_GUI, NP_OS_UNIX, NP_X_POPUP_ZENITY) if zenity is detected (GNOME)
  2. np_env_os_dialog(NP_TYPE, NP_ENV_GUI, NP_OS_UNIX, NP_X_POPUP_KDIALOG) if kdialog is detected (KDE)
  3. np_env_os_dialog(NP_TYPE, NP_ENV_GUI, NP_OS_UNIX, NP_X_POPUP_XMESSAGE) if xmessage is detected (should be detected with any Xorg-compliant window manager)

The external program is invoked using execvp().
On error, the turn goes to the next program in the list.
If execvp() fails for all external programs in the list, a log message is created instead, including an error message.

Environment: GUI: Mac OSX

Cocoa implies using Object-C and should be avoided:

https://github.com/cocoadialog/cocoadialog

Environment: GUI: Smartphones

These functions were designed with Drag n Drop operations in mind, i.e. Desktop environments.
Smartphone touch screens do not use the Drag n Drop operations to drop files over an application.
Even if the notifications themselves are generic, using these functions in smartphone applications makes little or no sense.
Smartphone devices are just listed here for completeness.

Environment: GUI: Android

TTY on Android:

Applications run in Android's Termux shell, could theoretically use np_tty() for a notification, but it makes probably more sense to use printf() directly instead.

Logging on Android:

https://stackoverflow.com/questions/10274920/how-to-get-printf-messages-written-in-ndk-application

#include <android/log.h>
__android_log_print(ANDROID_LOG_DEBUG, "LOG_TAG", "\n this is a log message. \n");

Environment: GUI: iOS

Environment: CGI

Environment: No environment detected

In the rare case of not detecting any environment, redirect output to a log file.
(In case of log file creation failed, output nothing.)


API

Enums

Internal (OS variables not needed at runtime???):

Typedefs

Settings for a notification.

    typedef struct np_settings_t
    {
        enum NP_TYPE    type;
        enum NP_ENV     env;
        enum NP_X_POPUP x_popup;
    } np_settings_t;

Functions

Main functions

All functions returns 0 (OK) or -1 (ERROR).
Besides, np_confirm() returns 0 on confirm (OK/Yes etc) or 1 (No/Cancel etc).

Usage:

np_info("Hello, world\n");

Generic functions

The functions np_info(), np_warning(), np_error(), np_confirm() are all wrappers for np_msg().

Usage:

  1. Choose NP_TYPE:

    enum NP_TYPE np_type = NP_TYPE_INFO|NP_TYPE_WARNING|NP_TYPE_ERROR|NP_TYPE_CONFIRM;
    
  2. Call np_msg():

    np_msg(np_type, "Hello, world!\n");
    

Backend function, call by all the othes above

Instead of auto-detection, force specific settings, such as environment and/or X Windows popup dialog .

Usage:

  1. Set NP_TYPE, NP_ENV, NP_X_POPUP:

    enum NP_TYPE np_type = NP_TYPE_INFO|NP_TYPE_WARNING|NP_TYPE_ERROR|NP_TYPE_CONFIRM;
    enum NP_ENV np_env = NP_ENV_AUTO|NP_ENV_TTY|NP_ENV_GUI|NP_ENV_CGI|NP_ENV_LOG:
    enum NP_X_POPUP np_x_popup = NP_X_POPUP_AUTO|NP_X_POPUP_ZENITY|NP_X_POPUP_KDIALOG|NP_X_POPUP_XMESSAGE|
    
  2. Settings:

    np_settings_t np_set = {np_type, np_env, np_x_popup);
    
  3. Call np_msg_set():

    np_msg_set(np_set, "Hello, world!\n");
    

For example, these three calls are equivalent:

  1. np_info("Hello, world!\n");
  2. np_msg(NP_TYPE_INFO, "Hello, world!\n");
  3. np_settings_t set = {NP_TYPE_INFO, NP_ENV_AUTO, NP_X_POPUP_AUTO}; np_msg_set(np_set, "Hello, world!\n");

Example:
Force using a GUI popup instead of STDOUT, even if a program is run from a terminal:

`np_settings_t np_set = {NP_TYPE_INFO, NP_ENV_GUI, NP_X_POPUP_AUTO); np_msg_set(np_set, "Hello, world!\n");`

Example:
Force using a log message, even if a program is run from a GUI or a terminal:

`np_settings_t np_set = {NP_TYPE_INFO, NP_ENV_GUI, NP_X_POPUP_NONE); np_msg_set(np_set, "Hello, world!\n");`

NOTE:
Certain combinations for np_settings_t are invalid:

In such invalid cases, the message will be displayed on STDOUT, together with a warning message on STDERR, or logged to a file, if STDOUT/STDERR are not available.


Demo application: Test TTY/GUI/CGI output

The demo application np-demo/np-demo.exe tests all kind of notifications, and is expected to have the following behaviour:

MSYS2 notes:
Note that if the demo executable has been compiled in MSYS2 environment, np-demo.exe cannot be run by double-clicking the icon in MS Windows' File Explorer.


CGI-enabled web servers for TTY/GUI/CGI demo application

See also webservers-cgi project.

The demo application requires an embedded CGI-enabled HTTP server.
The embedded web server:

Besides containing an embedded web server, the demo application itself may act as any CGI application. If installed in a CGI-enabled directory for another web server, that web server may run the demo application instead, as in this case, the embedded web server is not started.

Starting the demos app should also launch a browser with the URL, such as

fossil ui

or

mongoose-free-6.9.exe 
    -start_browser yes (default)

To make the demo application as portable as possible between OS:es, both POSIX sockets and Winsock sockets should be supported.
If HTTP-CGI server compiles both with MSYS2 (POSIX) and with Mingw64 (Winsock), chances are (big) that the web server works for several other OS:es as well.

Note: The resulting executable on MS Windows should always be compiled with Mingw64, not with MSYS2, to avoid the msys-2.0.dll dependency. Executables linked with msys-2.0.dll do not work when running from the MS Windows Shell (File Explorer).



CGI-enabled web server: MinGW64: Civetweb

See also webservers-cgi project.

https://github.com/civetweb/civetweb

MinGW64:

wget https://github.com/civetweb/civetweb/archive/master.zip -O civetweb-master.zip
unzip civetweb-master.zip
cd civetweb-master
mingw32-make CC=gcc
mingw32-make
./civetweb.exe
cp ../cgi-bin/hellohtml.exe .
cp ../cgi-bin/hellohtml.exe hellohtml.cgi
./civetweb.exe -listening_ports 1236 -cgi_pattern '**.exe$|**.cgi'

http://127.0.0.1:1236/hellohtml.exe
http://127.0.0.1:1236/hellohtml.cgi

Workaround for Perl scripts (NOTE: makes C programs stop working as CGI): cgi_interpreter

https://github.com/cztomczak/phpdesktop/issues/68

./civetweb.exe -listening_ports 1236 -cgi_pattern '**.exe$|**.cgi|**.pl' -cgi_interpreter '\\usr\\bin\\perl.exe' <-- ERROR

./civetweb.exe -listening_ports 1236 -cgi_pattern '**.exe$|**.cgi|**.pl' -cgi_interpreter '\\c\\msys64\\usr\\bin\\perl.exe' <-- ERROR

./civetweb.exe -listening_ports 1236 -cgi_pattern '**.exe$|**.cgi|**.pl' -cgi_interpreter 'C:\\msys64\\usr\\bin\\perl.exe' <-- OK (for Perl scripts, but not for C)

CGI-enabled web server: MSYS2: Merecat

See also webservers-cgi project.

http://merecat.troglobit.com/

Merecat works with MSYS2:

wget https://github.com/troglobit/merecat/archive/master.zip -O merecat-master.zip
unzip merecat-master.zip
cd merecat-master
./autogen.sh
mkdir -p build/usr build/var build/etc
./configure --prefix=$PWD/build/usr --localstatedir=$PWD/build/var --sysconfdir=$PWD/build/etc --disable-dirlisting --without-config --without-ssl --without-symlinks --without-zlib
make
make install
build/usr/sbin/merecat.exe -p 8888 -c "**.cgi|**.exe|**.pl|**/cgi-bin/*"
cp ../cgi-bin/hellohtml.exe .

http://127.0.0.1:8888/hellohtml.exe

Other CGI-enabled web servers

CGI-enabled web server: MinGW64: Mongoose

See also webservers-cgi project.

https://github.com/cesanta/mongoose
https://cesanta.com/docs/overview/build-options.html
https://cesanta.com/docs/http/cgi.html
https://cesanta.com/docs/http/server-example.html
https://github.com/cesanta/mongoose/tree/master/examples/simplest_web_server

CGI problems with mongoose compiled with MinGW64:

wget https://github.com/cesanta/mongoose/archive/master.zip -O mongoose-master.zip
unzip mongoose-master.zip
patch mongoose-master.patch # Fix casts and call to thread_enabled function 
cd mongoose-master/examples/simplest_web_server

    s_http_server_opts.enable_directory_listing = "no";

Makefile:
----------------------------------------
PROG = simplest_web_server
# MODULE_CFLAGS=-DMG_DISABLE_DAV_AUTH -DMG_ENABLE_FAKE_DAVLOCK
# MODULE_CFLAGS=-DMG_DISABLE_DAV_AUTH -DMG_ENABLE_FAKE_DAVLOCK -DMG_DISABLE_THREADS
#MODULE_CFLAGS=-DMG_DISABLE_DAV_AUTH -DMG_ENABLE_FAKE_DAVLOCK -DMG_ENABLE_THREADS=0
MODULE_CFLAGS=-DMG_DISABLE_DAV_AUTH -DMG_ENABLE_THREADS=0
MODULE_CFLAGS = \
-DMG_DISABLE_DAV_AUTH \
-DMG_ENABLE_FAKE_DAVLOCK \
-DMG_ENABLE_SSL=0 \
-DMG_ENABLE_IPV6=0 \
-DMG_ENABLE_MQTT=0 \
-DMG_ENABLE_MQTT_BROKER=0 \
-DMG_ENABLE_DNS_SERVER=0 \
-DMG_ENABLE_COAP=0 \
-DMG_ENABLE_HTTP=1 \
-DMG_ENABLE_HTTP_CGI=1 \
-DMG_ENABLE_HTTP_SSI=0 \
-DMG_ENABLE_HTTP_SSI_EXEC=0 \
-DMG_ENABLE_HTTP_WEBDAV=0 \
-DMG_ENABLE_HTTP_WEBSOCKET=0 \
-DMG_ENABLE_BROADCAST=0 \
-DMG_ENABLE_GETADDRINFO=0 \
-DMG_ENABLE_THREADS=0 \
-DMG_DISABLE_HTTP_DIGEST_AUTH=1 \
-DCS_DISABLE_SHA1=1 \
-DCS_DISABLE_MD5=1 \
-DMG_DISABLE_HTTP_KEEP_ALIVE=1
include ../examples.mk
----------------------------------------
./simplest_web_server.exe
cp ../../../cgi-bin/hellohtml.exe .

http://127.0.0.1:8000/hellohtml.exe

thttpd

Tiny web server.

http://www.acme.com/software/thttpd/

MSYS2/MinGW64:

wget http://www.acme.com/software/thttpd/thttpd-2.29.tar.gz
tar xvfz thttpd-2.29.tar.gz
cd thttpd-2.29
./configure --host=win32 --prefix=$PWD/build
make <--- ERROR
make install

shttpd

Tiny web server.

https://docs.huihoo.com/shttpd/ https://sourceforge.net/projects/shttpd/files/shttpd/

tar xvfz shttpd-1.42.tar.gz
cd shttpd-1.42
cd src
make unix DNO_SSL=1 DNO_AUTH=1 DNO_SSI=1 DNO_THREADS=1
./shttpd.exe -ports 8889 -cgi_ext "exe,cgi,pl,php"
cp ../../cgi-bin/hellohtml.exe .

http://127.0.0.1:8889/hellohtml.exe

CGI-enabled web server for both MinGW64/Win32 and MSYS2/POSIX: Merge Civetweb and Merecat code into one single server

See also webservers-cgi project.

%%% TODO MERGE!