Install Logical Journey of the Zoombinis (1996) on Linux


The game Logical Journey of the Zoombinis was an amazing game when I was young. It was fun, and apparently I learned a lot. I decided to try to install it on my Devuan ceres laptop. Here is the optimal route for making it work.

How to Install Zoombinis on Linux

The Zoombinis game was published in 1996, and it supports Windows 3.1 and Windows 95. We are going to install Windows 3.1 in DOSBox, install drivers, and then install the game.

Prepare Windows 3.1

Install DOSBox, which is probably in the main package manager on your GNU/Linux platform.

sudo dnf install dosbox
sudo apt-get install dosbox

Extract the Windows 3.1 VHD file (which I had from my old MSDN AA days). Also extract into sub-folders the Zoombinis disc contents, the sound drivers that DOSBox emulates, and the emulated video drivers.

mkdir -p ~/win31 ; cd ~/win31 ; 7z x ~/Downloads/Windows\ 3.1.vhd
mkdir -p ~/win31/zoominst ; cd ~/win31/zoominst && 7z x ~/Downloads/Logical\ Journey\ of\ the\ Zoombinis.iso
mkdir -p ~/win31/drivers/SB ; cd ~/win31/drivers/SB && 7z x ~/Downloads/
mkdir -p ~/win31/drivers/S3 ; cd ~/win31/drivers/S3 && 7z x ~/Downloads/

Prepare a batch file for mounting the right drives in the emulated environment.

cat <<'EOF' > win31.bat
REM for Zoombinis
MOUNT C ~/win31
MOUNT D ~/win31/zoominst


Run DOSBox:

dosbox win31.bat

Run Windows:


It might prompt you about deleting a corrupt swap file. You can select “Y” to do so, and then go into the Control Panel, “386 Enhanced,” Virtual Memory button, and save the information that is present. Then it shouldn’t prompt you anymore.

Install sound and video drivers

Zoombinis will warn you about sound and video settings, so the driver we loaded earlier should cover this. Now we have to install them. DOSBox emulates the SoundBlaster and S3 video hardware, so we just apply those drivers for Windows 3.1, and then audio will work and higher graphics settings too.
In DOSBox, cd to DRIVERS\SB and run the INSTALL utility.


Press enter to install the driver now.
Press enter to do a full installation.
Navigate with the arrow keys down to the Windows 3.1 path and enter in the “C:\Windows” value.

Change the Interrupt setting to the DOSBOX default of 7, as seen in this screenshot (unless you modified it in the dosbox.conf).

If it prompts you about replacing a file, feel free. It will not harm anything to replace it.
When that finishes, you can either reboot or stay on the current session.
Now it is time to install the video drivers. In DOSBox, run the Windows SETUP.EXE utility.


Select the DISPLAY option and press enter.

Scroll all the way to the bottom of the list and select “Other (requires disk…)”
Type in the directory where the video drivers are, e.g. “C:\DRIVERS\S3”
I am uncertain if Windows 3.1 has the ability to change the display resolution, so just pick your preferred screen resolution here.

I picked the 800×600 64K color with small fonts (as opposed to large-print fonts).
The SETUP utility will then return to the full list of system information, and you can select “Accept the configuration shown above.”
You might need to point it to the “C:\DRIVERS\S3” folder once more.
Now you can run Windows 3.1 with sound and an 800×600 display!


Install Zoombinis

Use File Manager to navigate to where we injected the install files.

Run the setup.exe!
Make sure you have enough disk space.

I installed QuickTime as well, although I don’t know what its omission would do.
Now you should have “Broderbund Software” group in Program Manager and the Zoombinis launcher inside it!


First of all, I had to find my old .VCD file (from my Virtual Drive Manager days on a non-free OS). I know those are basically ISO files and can just loop-mount them. Unfortunately, my .VCD file was corrupted. I then had to scrounge around for the old .FCD file and finally found it. But then, I had to try to get the contents out of it. I spent about 45 minutes researching online before I found IsoBuster. That software is shareware, but its free components worked enough to let me copy out the contents of the FCD file. On GNU/Linux these days, it’s all ISOs, and those are much easier to work with and find tutorials.

Did you know that 7zip can extract files from ISO and VHD files? That was very neat to learn.
I tried installing the game in Wine, and after getting the cd check part to work (by modifying the ini file “INSTALLFROMDRIVE=D”), I ran into an error: “Cannot initialize graphics.” And it included some contents of a register, and I never solved it.

err:int21:INT21_Ioctl_Block int21: unknown/not implemented parameters:
int21: AX 440d, BX 0004, CX 0848, DX db46, SI 0000, DI 0000, DS 0000, ES 0000
fixme:reg:RegOpenUserClassesRoot (0x7c, 0x0, 0x2000000, 0xe2e6d8) semi-stub
err:int21:INT21_Ioctl_Block int21: unknown/not implemented parameters:
int21: AX 440d, BX 0004, CX 0848, DX dde2, SI 0000, DI 0000, DS 0000, ES 0000

I think that’s functionality that just hasn’t been added to Wine. I’m pretty sure the game does some early DirectX fullscreen mumbo-jumbo and this version of how it does that was not implemented in wine. Ah, well.
But, during all this work I had read the Zoombini readme and it indicates it can be installed in Windows 3.1, ergo the main part of this article.



General Win 3.1 howto
Another general Win.31 howto, with sound and video driver installations
Sound driver SB16W3X
Video driver S3


Logical or in package dependencies

Logical OR in rpm dependencies

Requires: (wine >= 1.3 or /usr/bin/wine)

Logical OR in dpkg dependencies

Depends: wine (>= 1.3) | wine-staging | winehq-staging | winehq



  2. rpm since 4.13.0

Devuan, Dell Inspiron 1525, and b43 wireless network card

In my attempts to be more diverse in my GNU/Linux experience (and not be so dependent on systemd for everything), I am using Devuan on my old Dell Inspiron 1525.
Getting my Broadcom BCM4312 802.11b/g LP-PHY [14e4:4315] wireless network card working was one of the harder tasks on it. Installing freeipa is another, and it involved upgrading the distro to the rolling release version, ceres.

apt-get install firmware-b43-installer

Make sure that command downloads and extracts the broadcom driver. I think that package is from the contrib or non-free package set (in sources.list) and not main.
If it does not do the downloading part, you might have to remove the package and install it again.

modprobe | grep -E 'b43|wl|bcm'

Make sure the kernel module was installed by checking for it with modprobe.
I rebooted at various points, but it might not be necessary to do so. If you have to remove and add it again, use modprobe -r b43 ; modprobe b43.
I could then see the device listed when I ran

ip link show

I tried to enable it:

ip link set wlan0 up

But it warned me that it might not be able to do that because of rf-kill. Rather than remember/search how to do that on the cli, I just switched to wicd and checked the box for “switch on wifi.” Then I could set it up.
I also had to configure wicd to use “wlan0” as the wireless network card. And I threw in some service networking restart in here somewhere as well.
After all that, then wicd saw the wireless networks near me and I could join my wireless network!


Interesting, how running from systemd has made me embrace closed-source drivers. I don’t know what to say about that. But my laptop is not dependent on a wired network connection or systemd.

Additional notes

eudev:amd64 in ceres is giving me grief and will not install, which messes up pulseaudio I think, and certainly bluez and xserver-xorg-core. If you try this whole process on an Inspiron 1525, go with the 32-bit devuan and let me know it if works better.




Delayed cleanup of temp files for shell script

If you want the temp files from your script to be left around for a few minutes, you can use a few shell tricks to remove a temp directory a few minutes later.

If the environment variable FETCH_NO_CLEAN has any content at all, the script will not clean up at all.
The script sends a heredoc to a separate, nohupped shell instance. The commands are to wait so many seconds (default of 300 seconds), and then remove the directory.

To ensure the cleanup function is called, set a trap on the most common exit codes (0 through 20) to call the function, unset the traps, and then exit.
Then you can define a tempdir, and use it to make some temp files.

Using the TMPDIR variable is safe on FreeBSD mktemp, but bash will be /usr/local/bin/bash.

Get SID from Linux ldapsearch in Active Directory

With the help of a fantastic post on ServerFault, here is a way to find a user’s SID in string format from an ldapsearch against Active Directory.

# Filename:
# Author: YasithaB
# Startdate: 2018-02-14 15:58
# Title: Script that Converts Sid from AD Ldap Hexadecimal into String
# Purpose: Help convert sid to usable value
# History:
#    2018-02-15 Modified to work with kornshell
# Usage:
#    ldapsearch -b 'dc=prod,dc=example,dc=com' -s 'sub' -x -D 'CN=My Username,OU=Domain Users,DC=prod,DC=example,DC=com' -W -H 'ldaps://' '(cn=Target Username)' objectSid | grep -E '^objectSid:' | awk '{print $2}' | ./ --stdin
# Reference:
# Improve:
# Document: Below this line

# Base-64 encoded objectSid
case "${1}" in
   "--stdin" ) read OBJECT_ID ;;
   "") : ;;
   *) OBJECT_ID="${1}" ;;

# Decode it, hex-dump it and store it in an array
H="$(echo -n $OBJECT_ID | base64 -d -i | hexdump -v -e '1/1 "%02X"')"

# SID Structure:
# LESA = Little Endian Sub Authority
# BESA = Big Endian Sub Authority
# LERID = Little Endian Relative ID
# BERID = Big Endian Relative ID



echo "${SID}"

getent passwd -s sss LOCALUSER shows local user


I want to easily and quickly tell if a user is local or domain (don’t care which domain).


  • freeipa-client-4.6.1-3.fc27.x86_64
  • sssd-1.16.0-4.fc27.x86_64

Full story

I am writing a script that will show if a user is local, sssd, can ssh, and is permitted by sssd.

Currently I am doing the check for if the user is from the domain with the getent passwd -s sss $USERNAME command. But I ran into an issue where checking the sssd database returns a local user!

# getent passwd -s sss 'bgstack15-local'

Checking the contents of the database (cache) for sss shows sssd apparently caches all sorts of information about the local user.

# sudo su root -c 'strings /var/lib/sss/db/* | grep bgstack15-local' | sort | uniq
[...output truncated]

I tried clearing the sssd cache overall, and just for the user. Neither made a difference.

# sss_cache -U
# sss_cache -u bgstack15-local

The user does show up as a local user, and I promise it is only a local user!

getent passwd -s files 'bgstack15-local'

The man pages for getent(1) and getpwent(3) don’t help me understand what could be going on. sssd(8) shows me that sssd can cache local users, which actually goes against what I want! The nss section of sssd.conf(5) doesn’t help, but maybe I didn’t take enough time to read it. I’m a little stuck.

My sssd.conf

id_provider = ipa
ipa_server = _srv_,
ipa_domain =
ipa_hostname =
auth_provider = ipa
chpass_provider = ipa
access_provider = ipa
cache_credentials = True
ldap_tls_cacert = /etc/ipa/ca.crt
krb5_store_password_if_offline = True
services = nss, pam, ssh, sudo
domains =
homedir_substring = /home

Last resort

I can try doing my checks against ${USERNAME}@${DOMAIN} when doing the -s sss check, but that means I then have to iterate over all domains in sssd.conf and that would slow the process down.


The option that controls this behavior is buried in sssd.conf(5) on CentOS 7 and Fedora, but not in the online man page.


enable_files_domain = false

Reference 3 shows that sssd makes a “fast cache for local users.”

From man sssd.conf(5) on my Fedora system:

   enable_files_domain (boolean)
       When this option is enabled, SSSD prepends an implicit domain with
       “id_provider=files” before any explicitly configured domains.

       Default: true

Disabling this behavior lets me make a simple check to see if it is a local user or domain user.


  1. ddg: sssd disable caching local users
  4. Fedora 27 sssd.conf(5)

Ldapsearch notes

This post will be updated over time.

List all members of an AD group, including following the nested group membership

ldapsearch -b 'dc=dc=example,dc=com' -s 'sub' -x -D 'CN=B Stack,OU=Domain Users,DC=example,DC=com' -W -H 'ldaps://' '(&(objectClass=user)(memberof:1.2.840.113556.1.4.1941:=CN=complex_sample_group,OU=Linux,OU=Security Groups,DC=example,DC=com))' samaccountname | awk '/^samaccountname/{print $2;}'



Unattended software updates on Devuan

Devuan, as a fork of debian that uses sysvinit (or another– your choice), still uses debian-based utilities. I come from the Fedora/Red Hat/CentOS rpm-based family of distributions, and I struggle with the dpkg-based package management on occasion.

I really dislike how the software upgrades will sometimes pause in the middle, to display the changelog. If I wanted a changelog, I’d go read it! When I issue a command to update packages, I want to walk away, and come back, and it be done, not get stuck at 20% because openssh changed some defaults and wants to tell me. It emails me anyway! I find the defaults of apt-get to be not sane.

Here is how to configure apt-get to run without pausing to display duplicate information or ask you questions.

export DEBIAN_FRONTEND=noninteractive
apt-get -q -y upgrade




Logrotate, audit.log, selinux, cron, and ansible

The story

The disk space for /var/log/audit/audit.log tends to get filled up. The audit daemon has an ability to rotate its own logs. See the man page for auditd.conf.

max_log_file             =  100
max_log_file_action      =  rotate

That’s swell and all, until you realize that auditd cannot compress its rotated logs. On a small /var/log/audit mount point, you’ll fill it up with uncompressed logs.

/dev/mapper/os-var_log_audit          2490M 2136M      355M      86% /var/log/audit

So on a RHEL7 system with logrotate, you can adjust logrotate to handle the audit.log file. Now, logrotate is a finicky application. It has caused me many hours of grief in the past.
You would want to set auditd.conf a certain way:

max_log_file             =  0
max_log_file_action      =  ignore

And set /etc/logrotate.d/audit:

/var/log/audit/*.log {
        rotate 30
        minsize 100k
        maxsize 200M
                touch /var/log/audit/audit.log ||:
                chmod 0600 /var/log/audit/audit.log ||:
                service auditd restart

And ensure you’ve got a /etc/cron.weekly/logrotate:


/usr/sbin/logrotate /etc/logrotate.conf
if [ $EXITVALUE != 0 ]; then
    /usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
exit 0

After a few days, I learned that my logs were getting filled up so fast, the weekly rotation wasn’t good enough. So I had to place it in my cron.hourly.
And then I learned that it wasn’t running every hour. I spent a few days investigating, and eventually learned that some systems use a specific status file for logrotate. I remember in the past logrotate needs an execution with a -f flag to force the rotation the first time and add a new file to the status file. So if a new file was never force-rotated, it won’t be added to the status file.
My manual logrotate -f command was indeed adding my audit.log log file to the status file, but to the wrong one!
Some of my systems use -s /var/lib/logrotate/logrotate.status but the default is /var/lib/logrotate.status.
So I had to reflect that in my ansible playbook. Actually, I had to write some logic to find the one used by the cronjob and then use that status file.

So I got the correct logrotate status file set up in the ansible playbook. I spent the next week figuring out that logrotate simply couldn’t rotate the file when called from cron. I piped the utility to tee, and also included the -v flag on logrotate. I saw a permission denied.
With the permission issue, I had no choices left by selinux. I had to use the audit.log file to determine that the audit.log file is not readable by logrotate when called by cron.
I finally set captured all the actions performed by logrotate by setting the selinux process context to be permissive:

semanage permissive -a logrotate_t
I let it run, and then had to collect all the actions it performed, and saw what had happened.
{ grep logrotate /var/log/audit/audit.log ; zgrep logrotate /var/log/audit/audit.log.1.gz ; } | audit2why

So I used audit2allow to convert it to an selinux policy.

{ grep logrotate /var/log/audit/audit.log ; zgrep logrotate /var/log/audit/audit.log.1.gz ; } | audit2allow -M logrotate-audit

And then after some searching online, I learned how I can keep the text definition file, and compile the policy from it when I need to:

grep logrotate /var/log/audit/audit.log | audit2allow -m logrotate-audit # saves to logrotate-audit.te
checkmodule -M -m -o logrotate-audit.mod logrotate-audit.te # intermediate step
semodule_package -o logrotate-audit.pp -m logrotate-audit.mod # compiled policy
semodule -i logrotate-audit.pp

The text definition of logrotate-audit policy:

#semodule -i logrotate-audit.pp

module logrotate-audit 1.0;

require {
        type auditd_etc_t;
        type logrotate_t;
        type auditd_log_t;
        class file { create getattr ioctl open read rename setattr unlink write };
        class dir { add_name read remove_name write };

#============= logrotate_t ==============
allow logrotate_t auditd_etc_t:file getattr;
allow logrotate_t auditd_log_t:dir { read write add_name remove_name };
allow logrotate_t auditd_log_t:file { create ioctl open read rename getattr setattr unlink write };

Now, I wrote a master ansible playbook that performs this whole operation, from loading the .te file and compiling it and installing it, to setting logrotate to watch the audit file, and telling auditd to ignore rotating it.
Note: It is outside the scope of this task to ensure that the selinux tools are in place on each server. My environment already ensures package libselinux-python is present on each system, which should bring in all the dependencies of this ansible playbook.

# File: /etc/ansible/books/fix_var-log-audit.yml
# Author: bgstack15
# Startdate: 2018-01-24
# Title: Playbook that Fixes the /var/log/audit Space Issue
# Purpose: Logical Disk Free Space is too low
# History:
# Usage:
#    ansible-playbook -i /etc/ansible/inv/hosts /etc/ansible/configuration/fix_var-log-audit.yml -l hostwithproblem201
#    Use the -l host1,host2 parameter.
# Reference:
#    roles/general_conf/tasks/04_selinux.yml
#    roles/general_conf/tasks/05_auditd.yml
# Improve:
# Documentation:
#    The intention with auditd is to minimize the disk usage of the logs

- hosts: all
  remote_user: ansible_user
  become: yes

    auditd_conf: /etc/audit/auditd.conf
    auditd_log_cleanup_regex: '.*audit\.log\.[0-9]+'
    auditd_log_dir: /var/log/audit
    auditd_logrotate_conf: /etc/logrotate.d/audit


# To make it possible to just drop in files to the files directory and have this module read them automatically, use these two.
#  - name: learn full list of semodules available to install, modular list version
#    shell: warn=no find /etc/ansible/roles/general_conf/files/selinux/ -regex '.*.te' -printf '%f\n' | sed -r -e 's/\.te$//;'
#    register: semodules_list
#    changed_when: false
#    delegate_to: localhost
#    ignore_errors: yes
#  - name: learn semodule versions to install, modular list version
#	shell: warn=no grep -E '^\s*module\s+{{ item }}\s+[0-9\.]+;\s*$' /etc/ansible/roles/general_conf/files/selinux/{{ item }}.te | awk '{print $3*1000;}'
#    register: selinux_pol_versions_target
#    changed_when: false
#    delegate_to: localhost
#    with_items:
#    - "{{ semodules_list.stdout_lines }}"

  - name: learn semodule versions to install, static version
    shell: warn=no grep -E '^\s*module\s+{{ item }}\s+[0-9\.]+;\s*$' /etc/ansible/templates/{{ item }}.te | awk '{print $3*1000;}'
    register: selinux_pol_versions_target
    changed_when: false
    delegate_to: localhost
    - logrotate-audit

  #- debug:
  #    msg: "{{ item.item }} should be {{ item.stdout }}"
  #  with_items:
  #  - "{{ selinux_pol_versions_target.results }}"

  - name: learn current semodule versions
    shell: warn=no semodule --list | awk '$1=="{{ item.item }}" {print $2*1000} END {print "0";}' | head -n1
    register: selinux_pol_versions_current
    changed_when: false
    - "{{ selinux_pol_versions_target.results }}"

  - debug:
      msg: "{{ item.item.item }} is currently {{ item.stdout }} and should be {{ item.item.stdout }}"
    - "{{ selinux_pol_versions_current.results }}"

  #- pause:
  #    prompt: "Does the above look good?........................"

  - name: download selinux modules that need to be installed
      src: "/etc/ansible/templates/{{ item.item.item }}.te"
      dest: "/tmp/{{ item.item.item }}.te"
      mode: 0644
      owner: root
      group: root
      backup: no
      force: yes
    changed_when: false
    - "item.item.stdout > item.stdout"
    - "{{ selinux_pol_versions_current.results }}"

  - name: install selinux modules
    shell: chdir=/tmp warn=no /usr/bin/checkmodule -M -m -o "/tmp/{{ item.item.item }}.mod" "/tmp/{{ item.item.item }}.te" && /usr/bin/semodule_package -m "/tmp/{{ item.item.item }}.mod" -o "/tmp/{{ item.item.item }}.pp" && /usr/sbin/semodule -v -i "/tmp/{{ item.item.item }}.pp"
    - "item.item.stdout > item.stdout"
    - "{{ selinux_pol_versions_current.results }}"

  - name: clean any temporary selinux modules files
      path: "/tmp/{{ item[0].item.item }}.{{ item[1] }}"
      state: absent
    changed_when: false
    - "item[0].item.stdout > item[0].stdout"
    - "{{ selinux_pol_versions_current.results }}"
    - [ 'te', 'pp', 'mod' ]


  # modify auditd.conf which notifies the handler
  - name: auditd does not keep logs
      path: "{{ auditd_conf }}"
      regexp: "{{ item.r }}"
      backrefs: yes
      line: "{{ item.l }}"
      create: no
      state: present
      backup: yes
    #notify: auditd handler
    - { r: '^max_log_file_action.*$', l: 'max_log_file_action      =  ignore' }
    - { r: '^max_log_file\s.*$', l: 'max_log_file             =  0' }

  # tarball and cleanup any existing audit.log.1 files
  - name: list all old auditd logs which need to be compressed and cleaned up
    shell: warn=no find /var/log/audit -regex {{ auditd_log_cleanup_regex }}
    register: cleanup_list
    ignore_errors: yes
    changed_when: cleanup_list.stdout_lines | length > 0

  - name: get archive filename
    shell: warn=no echo "audit.log.{{ ansible_date_time.epoch }}.tgz"
    register: audit_log_tgz
    changed_when: audit_log_tgz.stdout_lines | length != 1

  - name: touch archive file
      path: "{{ auditd_log_dir }}/../{{ audit_log_tgz.stdout }}"
      state: touch
      owner: root
      group: root
      mode: 0600
    when: cleanup_list.stdout_lines | length > 0

  - name: archive and cleanup existing audit.log.1 files
      dest: "{{ auditd_log_dir }}/../{{ audit_log_tgz.stdout }}"
      path: "{{ cleanup_list.stdout_lines }}"
      format: gz
      owner: root
      group: root
      remove: yes
    ignore_errors: yes
    when: cleanup_list.stdout_lines | length > 0

  - name: check for existence of new tarball
      path: "{{ auditd_log_dir }}/../{{ audit_log_tgz.stdout }}"
    ignore_errors: yes
    register: audit_log_tarball

  - name: place audit log tarball in auditd_log_dir
    shell: warn=no /bin/mv "{{ auditd_log_dir }}/../{{ audit_log_tgz.stdout }}" "{{ auditd_log_dir }}/"
    ignore_errors: yes
    - audit_log_tarball.stat.exists is defined
    - audit_log_tarball.stat.exists

  - name: get current size of audit log
      path: "{{ auditd_log_dir }}/audit.log"
    ignore_errors: yes
    register: audit_log_stat

  - name: apply logrotate script for audit
      src: /etc/ansible/templates/etc-logrotate.d-audit
      dest: "{{ auditd_logrotate_conf }}"
      owner: root
      group: root
      mode: 0644
      backup: yes

  - name: learn the logrotate.status file to use, if any
    shell: warn=no grep -rE -- 'bin\/logrotate\>.*(-s|--state)(\s|=)[\/[A-Za-z0-9\.]+\>' /etc/cron.* 2>/dev/null | grep -oE '(-s|--state)(\s|=)[\/[A-Za-z0-9\.]+\>' | sort | uniq | head -n1
    ignore_errors: yes
    changed_when: false
    register: this_logrotate_flag

  - name: show which logrotate.status file to use, if any
      msg: "The status file that will be used is {{ this_logrotate_flag.stdout }}"

  - name: run logrotate
    shell: warn=no /usr/sbin/logrotate {{ this_logrotate_flag.stdout }} -f "{{ auditd_logrotate_conf }}"
    register: run_logrotate
    when: ( cleanup_list.stdout_lines | length > 0 ) or ( audit_log_stat.stat.exists and audit_log_stat.stat.size > 190000000 )



So, logrotate can be configured to rotate the audit log. It just takes a few minutes to configure correctly, after about 2 weeks of research and testing.




Personal effort

Hours and hours of my original research
Years of administering RHEL servers with logrotate