Sunday, January 17, 2021

Node.js exec with promisify

I'm working on a project where Nodejs is used for the backend and redis is used for caching application data.  Here's an example of making a shell command to delete redis cache keys using child_process exec and promisify.  I find it easier (and more intuitive) to use async/await than to use callbacks.

Environment:
CentOS Linux release 7.3.1611 (Core)
node v14.9.0

// child process currently doesn't natively support promises, however can wrap the
// methods with promises using built-in promisify util
const { promisify } = require("util");
const execAsync = promisify(require("child_process").exec);

/**
 * Deletes all keys from cache that start with cacheCode param.  If
 * cacheCode is not defined, deletes all cache.
 * @param {String} cacheCode.
 * @throws {Error}
 * @returns {String} how many keys effected
 */
async function flushFromCache(cacheCode) {
  const pattern = cacheCode ? ` --pattern ${cacheCode}_*` : "";
  const command = `redis-cli --scan${pattern} | xargs redis-cli del`;
  try {
    const execResult = await execAsync(command);
    // result looks like { stdout: string, stderr: string }.
    // stdout is populated when everything goes nicely.
    // stderr is populated if error occurred while running command
    if (execResult.stderr) {
      throw new Error(execResult.stderr);
    }

    // When there are keys deleted the result looks like:
    // (integer) 2
    // When there are no keys found the result looks like:
    // ERR wrong number of arguments for 'del' command
    let result;
    if (execResult.stdout.includes("ERR")) {
      result = `No ${cacheCode ? `'${cacheCode}'` : ""} keys to flush`;
    } else {
      result = `${execResult.stdout.trim()} ${
        cacheCode ? `'${cacheCode}'` : ""
      } keys flushed`;
    }
    return result;
  } catch (e) {
    // this error can occur if the command is bad
    throw new Error(e.message);
  }
}

module.exports = {
  flushFromCache
};

These articles were helpful:
https://stackabuse.com/executing-shell-commands-with-node-js/ https://stackoverflow.com/questions/20643470/execute-a-command-line-binary-with-node-js

Friday, December 4, 2020

Docker Compose - change COMPOSE_HTTP_TIMEOUT and verify env_file environment variable configuration options

I occasionally get the below error while running docker-compose on my local VM or while running on Jenkins.

ERROR: for cherryshoe_test_ta_run_609fc9bdd396  UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=60)
An HTTP request took too long to complete. Retry with --verbose to obtain debug information.
If you encounter this issue regularly because of slow network conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher value (current value: 60).

Environment:
CentOS Linux release 7.3.1611 (Core)
Docker version 19.03.1, build 74b1e89

You can verify this environment variable with the docker-compose config command, which prints the resolved application config to the terminal, by accessing the environment variable in the docker-compose file.   A sample docker-compose yml below is used to test this out:

docker-compose-cs.yml
version: "3"
services:
  cherryshoe:
    image: "testconfig:${COMPOSE_HTTP_TIMEOUT}"

The config command verifies the default COMPOSE_HTTP_TIMEOUT value of 60:

$ docker-compose -f docker-compose-cs.yml config
services:
  cherryshoe:
    image: testconfig:60
version: '3.0'

After using the env_file to set the value to a higher number,

.env

#.env file for docker-compose.  Use same file for all environments.
# set COMPOSE_HTTP_TIMEOUT at docker instance level and not inside docker image.
COMPOSE_HTTP_TIMEOUT=120

run the config command again to verify it:

$ docker-compose -f docker-compose-cs.yml config
services:
  cherryshoe:
    image: testconfig:120
version: '3.0'

This article was helpful.


Tuesday, November 10, 2020

Docker - enabling systemd on centos:7

I'm experimenting with enabling systemd on the docker centos:7 image.  Here's a simple example getting the centos7 systemd image set up, then building a docker image with nginx using that, and then running it.  

Environment:
CentOS Linux release 7.3.1611 (Core)
Docker version 19.03.1, build 74b1e89

centos7-systemd.Dockerfile

# systemd is included with centos:7 but not enabled by default - START
# https://hub.docker.com/_/centos:
FROM centos:7
ENV container docker
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*;\
rm -f /etc/systemd/system/*.wants/*;\
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*;\
rm -f /lib/systemd/system/anaconda.target.wants/*;
VOLUME [ "/sys/fs/cgroup" ]
CMD ["/usr/sbin/init"]
# systemd is included with centos:7 but not enabled by default - END

centos7-systemd-nginx.Dockerfile

# This dockerfile is used when running cypress during a commit to the remote repo
FROM local/centos7-systemd

# install yum dependencies ansible
RUN yum install -y epel-release \
  && yum install -y nginx && systemctl enable nginx.service

EXPOSE 80

CMD ["/usr/sbin/init"]

Build images:

docker build --rm -t local/centos7-systemd -f centos7-systemd.Dockerfile .
docker build -t local/centos7-systemd-nginx -f centos7-systemd-nginx.Dockerfile .

Run container:

docker run -it --rm -d -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v /run -p 80:80 --name web local/centos7-systemd-nginx

Test that nginx is running: 

curl http://localhost/index.html

It returned an error HTML page, but I know nginx is running!

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>The page is not found</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <style type="text/css">
            /*<![CDATA[*/
            body {
                background-color: #fff;
                color: #000;
                font-size: 0.9em;
                font-family: sans-serif,helvetica;
                margin: 0;
                padding: 0;
            }
            :link {
                color: #c00;
            }
            :visited {
                color: #c00;
            }
            a:hover {
                color: #f50;
            }
            h1 {
                text-align: center;
                margin: 0;
                padding: 0.6em 2em 0.4em;
                background-color: #294172;
                color: #fff;
                font-weight: normal;
                font-size: 1.75em;
                border-bottom: 2px solid #000;
            }
            h1 strong {
                font-weight: bold;
                font-size: 1.5em;
            }
            h2 {
                text-align: center;
                background-color: #3C6EB4;
                font-size: 1.1em;
                font-weight: bold;
                color: #fff;
                margin: 0;
                padding: 0.5em;
                border-bottom: 2px solid #294172;
            }
            h3 {
                text-align: center;
                background-color: #ff0000;
                padding: 0.5em;
                color: #fff;
            }
            hr {
                display: none;
            }
            .content {
                padding: 1em 5em;
            }
            .alert {
                border: 2px solid #000;
            }
            img {
                border: 2px solid #fff;
                padding: 2px;
                margin: 2px;
            }
            a:hover img {
                border: 2px solid #294172;
            }
            .logos {
                margin: 1em;
                text-align: center;
            }
            /*]]>*/
        </style>
    </head>
    
    <body>
        <h1><strong>nginx error!</strong></h1>
        <div class="content">
            <h3>The page you are looking for is not found.</h3>
            <div class="alert">
                <h2>Website Administrator</h2>
                <div class="content">
                    <p>Something has triggered missing webpage on your
                    website. This is the default 404 error page for
                    <strong>nginx</strong> that is distributed with
                    Fedora.  It is located
                    <tt>/usr/share/nginx/html/404.html</tt></p>

                    <p>You should customize this error page for your own
                    site or edit the <tt>error_page</tt> directive in
                    the <strong>nginx</strong> configuration file
                    <tt>/etc/nginx/nginx.conf</tt>.</p>

                </div>
            </div>

            <div class="logos">
                <a href="http://nginx.net/"><img
                    src="/nginx-logo.png"
                    alt="[ Powered by nginx ]"
                    width="121" height="32" /></a>

                <a href="http://fedoraproject.org/"><img
                    src="/poweredby.png"
                    alt="[ Powered by Fedora ]"
                    width="88" height="31" /></a>
            </div>
        </div>
    </body>
</html>

These articles were helpful:
https://hub.docker.com/_/centos
https://stackoverflow.com/questions/36617368/docker-centos-7-with-systemctl-failed-to-mount-tmpfs-cgroup

Sunday, October 4, 2020

PostgreSQL - get position of second delimiter in string

There's many articles with examples of getting the first or last position of a delimiter in a string. Here's an example of getting the position of the second delimiter in a string. 

Environment: 
PostgreSQL 10.12

Get the position of the first delimiter ';' from the public.cherryshoe.description column:
select description, 
position(';' in description) from public.cherryshoe; 

Get the position of the second delimiter ';'.   Add char_length of each split_part - since we want the position, need two char_length's.  Add an additional length of 2 for each split_part function that is used to account for each of the two delimiters:
select description, 
(char_length(split_part(description, ';', 1))
+ char_length(split_part(description, ';', 2)) 
+ 2) from public.cherryshoe;

Get the position of the third delimiter ';'.  Add char_length of each split_part - since we want the position, need three char_length's.  Add an additional length of 3 for each split_part function that is used to account for each of the three delimiters:
select description, 
(char_length(split_part(description, ';', 1)) 
+ char_length(split_part(description, ';', 2)) 
+ char_length(split_part(description, ';', 3)) 
+ 3) from public.cherryshoe;

So on and so forth...

Tuesday, September 1, 2020

Running jackd command line works, but not via systemd because [Cannot allocate memory]

I am working on a personal project with my husband (also a software dev), where he got the video saving piece working with a raspberry pi, python, and picamera.  Audio wasn't working yet, so I decided to take that piece on.

Environment:
Raspberry Pi 2 Model B
Raspberry Pi Camera Module V2
Raspberry Pi USB WiFi Adapter
Raspberry Pi OS 10
USB Microphone
Python 3.7.3
systemctl --version => systemd 241 (241)

I'm currently working with JACK audio server and jack_capture to capture audio files with JACK.   The jackd2 package installation included the /etc/security/limits.d/audio.conf.disabled file, which when activated, enables realtime permissions and memlock permissions to the audio group.  You can activate it via the following command or rename the file without the .disabled extension at the end.  Make sure to logout and login again for the audio server to have the configuration recognized - no need to reboot/restart.

dpkg-reconfigure -p high jackd
I installed jackd2 via the following commands:

# audio server
sudo apt -y install jackd2
# modify /etc/security/limits.d/audio.conf to bring realtime priorities to the audio group (which is usually fine for a single user desktop usage)
sudo mv /etc/security/limits.d/audio.conf.disabled /etc/security/limits.d/audio.conf

The audio.conf file configures the following; I verified the default pi user is in the audio group.

@audio   -  rtprio     95
@audio   -  memlock    unlimited

I installed jack_capture via the following commands.  The liblo-tools were necessary for me to remote control jack_capture via UDP port with OSC (Open Sound Control) messages.

# audio capture
sudo apt -y install liblo-tools
sudo apt -y install jack-capture

Starting jackd with python via command line worked great!  When I started the python script as a service with systemd this error kept occurring:

Cannot lock down 13589280 byte memory area (Cannot allocate memory)

Why?!  I read through numerous jackd configuration articles.  Running command line was fine.  Checking "ulimit -l" was unlimited (as the audio.conf file told the machine to do).

I stumbled upon this and this article (looked very old but it still applied for my problem!) that pointed me in the right direction that "When using the RPM or Debian packages on systems that use systemd, system limits must be specified via systemd."  Essentially running jackd with systemd ignores the audio.conf file, so you must set it manually with systemd.

# The four settings under Service is for jackd support
# LimitRTPRIO and LimitMEMLOCK must be set in systemd service file, because when run as a service
# etc security limits.d audio.conf is not honored.  MEMLOCK unlimited is infinity in systemd
[Service]
LimitRTPRIO=95
LimitMEMLOCK=infinity
Environment="DISPLAY=:0"
Environment="JACK_NO_AUDIO_RESERVATION=1"

Note:

  • The LimitMEMLOCK is set to "infinity" for the systemd service, but in the audio.conf was set to "unlimited". 
  • The two environment variables DISPLAY and JACK_NO_AUDIO_RESERVATION were set to get rid of these two errors I had previous to the "Cannot allocate memory error".
To bypass device reservation via session bus, set JACK_NO_AUDIO_RESERVATION=1 
Failed to connect to session bus for device reservation Unable to autolaunch a dbus-daemon without a $DISPLAY for X11

These additional articles were helpful:

Friday, August 14, 2020

React - access ref of child component from parent component

This is how to get access to the ref of a child component from a parent component.  I am working with react-table so this example has references to that.

Environment:
CentOS Linux release 7.3.1611 (Core)
React 16.2
react-table 6.10.3

MyParent.js

const ALL_TOOLS = "";
const TOOL_1 = "tool1";
class MyParent extends Component {
  constructor(props) {
    super(props);

    this.state = {
      error: null,
      rows: [],
      numPages: -1,
      summary: null,
      loading: false,
      filter: ""
    };
  }
  
  /**
   * Allow the parent to get a reference to the child component
   * @param {Object} table
   */
  setRef = table => {
    this.childRef = table;
  };

  /**
   * Event handler for filter selection
   * @param {String} filter
   */
  onFilterSelection = filter => {
    // must put this.fetchData in callback because need to guarantee filter will be updated
    // before calling
    this.setState(
      {
        filter: filter
      },
      () => {
        this.childRef.current.state.page = 0; // force react-table back to page 1
        this.fetchData(this.childRef.current.state);
      }
    );
  };

  /**
   * Fetches data for the table
   * @param {Object} tableState: the react table current table
   */
  fetchData = tableState => {
    // show the loading overlay
    this.setState({ loading: true });
    const sortDir = tableState.sorted[0].desc ? "DESC" : "ASC";
    const offset = tableState.page * tableState.pageSize;
    ...
  };

  render() {
    const { error, rows, numPages, summary, loading } = this.state;
    return (
      <div className="MyParent">
        {!error && (
          <div className="content">
            <div className="left-panel">
              <ul>
                <li
                  onClick={() => this.onFilterSelection(ALL_TOOLS)}
                >All</li>
                <li
                  onClick={() => this.onFilterSelection(TOOL_1)}
                >Tool 1</li>
              </ul>
            </div>
            <div className="right-panel">
              <MyChildTable
                setRef={this.setRef}
                rows={rows}
                numPages={numPages}
                summary={summary}
                loading={loading}
                fetchData={this.fetchData}
              />
            </div>
          </div>
        )}
      </div>
    );
  }
}

export default MyParent;

MyChildTable.js


class MyChildTable extends React.PureComponent {
  constructor(props) {
    super(props);

    // add ref to react table, and pass along to parent
    const { setRef } = this.props;
    this.table = React.createRef();
    setRef(this.table);
  }

  render() {
    const { rows, numPages, summary, loading, fetchData } = this.props;
    const columns = [
      ...
    ];
    return (
      <div className="MyChildTable">
        {summary}
        <ReactTable
          ref={this.table}
          data={rows}
          loading={loading}
          pages={numPages}
          columns={columns}
          defaultSorted={[{ id: "tstamp", desc: true }]}
          multiSort={false}
          className="-striped -highlight"
          showPagination={true}
          showPaginationTop={true}
          showPaginationBottom={true}
          showPageSizeOptions={false} // Only allow 25
          pageSizeOptions={[25]} // Only allow 25
          defaultPageSize={25} // Only allow 25
          manual
          onFetchData={fetchData}
        />
      </div>
    );
  }
}

export default MyChildTable;



This article was helpful.

Tuesday, July 28, 2020

Raspberry Pi set up for development

I had a really old Raspberry Pi sitting around that I set up for development yesterday. The Pi 2 doesn't come with WiFi by default, so I set up a WiFi Adapter so it would be easier to develop on my localhost and ssh as necessary.

Environment:
Host: macOS Catalina
Raspberry Pi 2 Model B
Raspberry Pi USB WiFi Adapter (I have this one)
  1. Install Raspberry Pi OS image on an SD card: Used Raspberry Pi Imager rbi-imager for macOS, chose Raspberry Pi OS Lite (32-bit) since I didn’t want the desktop environment - https://www.raspberrypi.org/documentation/installation/installing-images/
  2. Boot into the Raspberry Pi and login
  3. Confirm the OS version. Mine is "Raspbian GNU/Linux 10 (buster)"
  4. cat /etc/os-release
    
  5. Update settings for US. I did I1, then I4, then I3. I think I3 may be the only one I needed for my keyboard to recognize the double-quote key
    sudo raspi-config 
    Choose 4 Localisation Options 
    Choose I1 Change Locale => en_US.UTF-8 UTF-8 
    Choose I3 Change Keyboard Layout => Generic 101-key PC => Other => English (US) (Country of Origin) => English (US) (Keyboard layout) => The default for the keyboard layout => No compose key => 
    Choose I4 Change WLAN Country => US United States 
    sudo reboot
    
  6. Configure Raspberry Pi to connect to WiFi

    Backup file and copy to your home folder
    sudo cp /etc/wpa_supplicant/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf.orig
    cp /etc/wpa_supplicant/wpa_supplicant.conf ~
    

    Add below to the end of the file. I had to add scan_ssid=1 because my SSID is not broadcasting.  NOTE:  If you have special characters in your password, e.g. double quote I didn't have to escape it but I had to add in key_mgmt=WPA-PSK config attribute (with no quotes).
    vi ~/wpa_supplicant.conf 
    
    network={ 
      ssid="<ssid>"
      psk="<secret password>"
      scan_ssid=1 
    } 
    

    Copy updated file back
    sudo cp ~/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf 
    

    Verify if the “inet addr” is available on wlan0
    ifconfig wlan0 
    

    If not, reboot to connect
    sudo reboot 
    

    On reboot, I can see it says “My IP address is XXX.XXX.XX.XXXX”

    To verify internet connectivity perform a wget
    wget www.google.com
    
  7. Enable ssh on Raspberry Pi
    sudo raspi-config 
    Choose I5 Interfacing Options 
    Choose P2 SSH => Yes 
    [Didn’t need to reboot]
    
  8. Connect to Raspberry Pi via ssh from localhost, update macOS hosts file with Raspberry Pi IP
    sudo vi /private/etc/hosts 
    

    Added this to bottom of file
    # raspberry pi 
    XXX.XXX.XX.XXXX csrp
    

    ssh to the Raspberry Pi
    ssh pi@csrp
    
  9. Shutdown the Raspberry Pi
    sudo halt