├── .github └── workflows │ ├── check.yml │ └── release.yml ├── .travis.yml ├── AUTHORS ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── LICENSE ├── MAINTAINERS ├── Makefile.in ├── README.md ├── compress.c ├── compress.h ├── compress_zstd.c ├── compress_zstd.h ├── configure ├── debian ├── README ├── README.source ├── changelog ├── compat ├── control ├── copyright ├── links ├── manpages └── rules ├── docs ├── ttyplay.1 ├── ttyrec.1 └── ttytime.1 ├── io.c ├── io.h ├── ovh-ttyrec.spec ├── ttyplay.c ├── ttyrec.c ├── ttyrec.h ├── ttytime.c ├── uncrustify.cfg └── update-spec-file-changelog-from-debian.pl /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: basic checks 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | 7 | check: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | 12 | - uses: actions/checkout@v2 13 | 14 | - name: verify debian changelog 15 | run: | 16 | VERSION=$(awk '/static const char version/ { gsub(/[\";]/, "", $6); print $6; exit }' ttyrec.c) 17 | if ! head -n1 debian/changelog | grep -F "ovh-ttyrec ($VERSION) master"; then 18 | echo "inconsistency between version ($VERSION) and debian changelog:" 19 | head -n1 debian/changelog 20 | exit 1 21 | fi 22 | 23 | - name: install prerequisites 24 | run: sudo apt-get install -y uncrustify make git debhelper 25 | 26 | - name: check style 27 | run: | 28 | ./configure 29 | make style 30 | git diff 31 | if ! git diff --quiet; then 32 | echo "Please make style." 33 | exit 1 34 | fi 35 | 36 | - name: compile with zstd autodetection (none) 37 | run: | 38 | sudo apt-get remove --purge -y libzstd-dev 39 | make clean && ./configure && make -j$(nproc) && ./ttyrec -V 40 | if ./ttyrec -V | grep -qF 'zstd'; then 41 | exit 1 42 | fi 43 | 44 | - name: compile with zstd autodetection (static) 45 | run: | 46 | sudo apt-get install -y libzstd-dev 47 | make clean && ./configure && make -j$(nproc) && ./ttyrec -V 48 | ./ttyrec -V | grep -qF 'zstd[static]' 49 | 50 | - name: compile with shared libzstd 51 | run: | 52 | make clean && NO_STATIC_ZSTD=1 ./configure && make -j$(nproc) && ./ttyrec -V 53 | if ./ttyrec -V | grep -qF 'zstd[static]'; then 54 | exit 1 55 | fi 56 | ./ttyrec -V | grep -qF 'zstd' 57 | 58 | - name: compile without zstd support 59 | run: | 60 | make clean && NO_ZSTD=1 ./configure && make -j$(nproc) && ./ttyrec -V 61 | if ./ttyrec -V | grep -qF 'zstd'; then 62 | exit 1 63 | fi 64 | 65 | - name: build and test debian package 66 | run: | 67 | make clean && ./configure && make -j$(nproc) && ./ttyrec -V 68 | make deb 69 | ls -l .. 70 | sudo dpkg -i ../ovh-ttyrec_*.deb 71 | dpkg -L ovh-ttyrec 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: publish release assets 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | 9 | checks: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | 14 | - uses: actions/checkout@v4 15 | with: 16 | persist-credentials: false 17 | 18 | - name: verify changelogs 19 | run: | 20 | VERSION=$(awk '/static const char version/ { gsub(/[\";]/, "", $6); print $6; exit }' ttyrec.c) 21 | if ! head -n1 debian/changelog | grep -F "ovh-ttyrec ($VERSION) master"; then 22 | echo "inconsistency between version ($VERSION) and debian changelog:" 23 | head -n1 debian/changelog 24 | exit 1 25 | fi 26 | if ! grep -F " $VERSION" ovh-ttyrec.spec; then 27 | echo "inconsistency between version ($VERSION) and rpm changelog:" 28 | exit 1 29 | fi 30 | 31 | - name: install prerequisites 32 | run: sudo apt-get update && sudo apt-get install -y uncrustify make git gcc 33 | 34 | - name: check style 35 | run: | 36 | ./configure 37 | make style 38 | git diff 39 | if ! git diff --quiet; then 40 | echo "Please make style." 41 | exit 1 42 | fi 43 | 44 | - name: compile with zstd autodetection (none) 45 | run: | 46 | sudo apt-get remove --purge -y libzstd-dev 47 | make clean && ./configure && make -j$(nproc) && ./ttyrec -V 48 | if ./ttyrec -V | grep -qF 'zstd'; then 49 | exit 1 50 | fi 51 | 52 | - name: compile with zstd autodetection (static) 53 | run: | 54 | sudo apt-get install -y libzstd-dev 55 | make clean && ./configure && make -j$(nproc) && ./ttyrec -V 56 | ./ttyrec -V | grep -qF 'zstd[static]' 57 | 58 | - name: compile with shared libzstd 59 | run: | 60 | make clean && NO_STATIC_ZSTD=1 ./configure && make -j$(nproc) && ./ttyrec -V 61 | if ./ttyrec -V | grep -qF 'zstd[static]'; then 62 | exit 1 63 | fi 64 | ./ttyrec -V | grep -qF 'zstd' 65 | 66 | - name: compile without zstd support 67 | run: | 68 | make clean && NO_ZSTD=1 ./configure && make -j$(nproc) && ./ttyrec -V 69 | if ./ttyrec -V | grep -qF 'zstd'; then 70 | exit 1 71 | fi 72 | 73 | - name: compile fully static version 74 | run: | 75 | make clean && STATIC=1 ./configure && make -j$(nproc) && ./ttyrec -V 76 | ./ttyrec -V | grep -qF 'zstd[static]' 77 | if ldd ttyrec; then 78 | exit 1 79 | fi 80 | 81 | multiarch: 82 | needs: checks 83 | runs-on: ubuntu-latest 84 | strategy: 85 | matrix: 86 | cross: 87 | - arm32v5 88 | - arm32v7 89 | - arm64v8 90 | - ppc64le 91 | - mips64le 92 | - s390x 93 | - i386 94 | - amd64 95 | 96 | steps: 97 | - uses: actions/checkout@v4 98 | with: 99 | persist-credentials: false 100 | 101 | - name: prepare qemu 102 | run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 103 | 104 | - name: install prerequisites 105 | run: sudo apt-get update && sudo apt-get install -y rsync zip 106 | 107 | - name: create source zip file for rpm build 108 | run: | 109 | curdir=$PWD 110 | tempfolder=$(mktemp -d) 111 | cd $tempfolder 112 | mkdir ovh-ttyrec 113 | rsync -va --exclude=.git $curdir/ ovh-ttyrec/ 114 | mkdir -p ~/rpmbuild/SOURCES 115 | zip -9r ~/rpmbuild/SOURCES/master.zip ovh-ttyrec 116 | 117 | - name: build for ${{ matrix.cross }} 118 | run: > 119 | mkdir /tmp/pkg && 120 | docker run --rm -e DEBIAN_FRONTEND=noninteractive 121 | -v $PWD:/pkg/code 122 | -v /tmp/pkg:/pkg 123 | -v $HOME:/root 124 | ${{ matrix.cross }}/debian:10 125 | /bin/bash -c ' 126 | set -exo pipefail; 127 | apt-get update; 128 | apt-get install -y make gcc libzstd-dev dpkg-dev debhelper unzip rpm binutils; 129 | cd /pkg/code; 130 | dpkg-buildpackage -b -rfakeroot -us -uc; 131 | if [ "${{ matrix.cross }}" = i386 ]; then target="--target=i386"; else target=""; fi 132 | set +o pipefail; 133 | rpm --showrc | head; 134 | set -o pipefail; 135 | if [ $(rpm -E "%{_arch}") != "%{_arch}" ]; then 136 | rpmbuild -bb $target ovh-ttyrec.spec; 137 | else 138 | arch=$(rpm --showrc | grep "^build arch" | awk "{print \$4}"); 139 | rpmbuild --define "_arch $arch" -bb $target ovh-ttyrec.spec; 140 | fi; 141 | mv ~/rpmbuild/RPMS/*/*.rpm /pkg; 142 | STATIC=1 ./configure && make clean && make -j$(nproc) && ./ttyrec -V; 143 | ./ttyrec -V | grep -qF "zstd[static]"; 144 | if ldd ttyrec; then 145 | exit 1; 146 | fi; 147 | version=$(./ttyrec -V | head -n1 | cut -d" " -f2 | grep -Eo "[0-9][A-Za-z0-9._-]+"); 148 | mkdir ovh-ttyrec-$version; 149 | strip ttyrec ttyplay ttytime; 150 | install ttyrec ttyplay ttytime ovh-ttyrec-$version; 151 | cp -va docs ovh-ttyrec-$version; 152 | staticname=ovh-ttyrec-${version}_$(dpkg --print-architecture)-linux-static-binary.tar.gz; 153 | tar cvzf /pkg/$staticname ovh-ttyrec-$version; 154 | ' 155 | 156 | - name: get release vars 157 | id: getvars 158 | run: | 159 | rpmpath=$(find /tmp/pkg -mindepth 1 -maxdepth 1 -type f -name "*.rpm") 160 | rpmname=$(basename "$rpmpath") 161 | echo "RPM package name is $rpmname ($rpmpath)" 162 | echo "::set-output name=rpmname::$rpmname" 163 | echo "::set-output name=rpmpath::$rpmpath" 164 | debpath=$(find /tmp/pkg -mindepth 1 -maxdepth 1 -type f -name "*.deb" ! -name "*dbgsym*") 165 | debname=$(basename "$debpath") 166 | echo "Debian package name is $debname ($debpath)" 167 | echo "::set-output name=debname::$debname" 168 | echo "::set-output name=debpath::$debpath" 169 | staticpath=$(find /tmp/pkg -mindepth 1 -maxdepth 1 -type f -name "*-linux-static-binary.tar.gz" | head -n1) 170 | staticname=$(basename "$staticpath") 171 | echo "Static tar.gz archive name is $staticname ($staticpath)" 172 | echo "::set-output name=staticname::$staticname" 173 | echo "::set-output name=staticpath::$staticpath" 174 | 175 | - name: upload rpm package 176 | uses: actions/upload-release-asset@v1 177 | env: 178 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 179 | with: 180 | upload_url: ${{ github.event.release.upload_url }} 181 | asset_path: ${{ steps.getvars.outputs.rpmpath }} 182 | asset_name: ${{ steps.getvars.outputs.rpmname }} 183 | asset_content_type: application/x-rpm 184 | 185 | - name: upload debian package 186 | uses: actions/upload-release-asset@v1 187 | env: 188 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 189 | with: 190 | upload_url: ${{ github.event.release.upload_url }} 191 | asset_path: ${{ steps.getvars.outputs.debpath }} 192 | asset_name: ${{ steps.getvars.outputs.debname }} 193 | asset_content_type: application/x-debian-package 194 | 195 | - name: upload static binary 196 | uses: actions/upload-release-asset@v1 197 | env: 198 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 199 | with: 200 | upload_url: ${{ github.event.release.upload_url }} 201 | asset_path: ${{ steps.getvars.outputs.staticpath }} 202 | asset_name: ${{ steps.getvars.outputs.staticname }} 203 | asset_content_type: application/gzip 204 | 205 | freebsd: 206 | needs: checks 207 | runs-on: macos-latest 208 | name: FreeBSD 209 | steps: 210 | - uses: actions/checkout@v4 211 | with: 212 | persist-credentials: false 213 | - name: Build for FreeBSD 214 | id: build 215 | uses: vmactions/freebsd-vm@v0 216 | with: 217 | usesh: true 218 | sync: sshfs 219 | run: | 220 | set -ex 221 | freebsd-version 222 | pkg install -y gmake zstd 223 | STATIC=1 ./configure 224 | gmake 225 | ./ttyrec -V 226 | ./ttyrec -V | grep -qF "zstd[static]" 227 | file ttyrec | grep -qF "statically" 228 | version=$(./ttyrec -V | head -n1 | cut -d" " -f2 | grep -Eo "[0-9][A-Za-z0-9._-]+") 229 | mkdir ovh-ttyrec-$version 230 | strip ttyrec ttyplay ttytime 231 | install ttyrec ttyplay ttytime ovh-ttyrec-$version 232 | cp -va docs ovh-ttyrec-$version 233 | staticname=ovh-ttyrec-${version}_$(uname -m)-freebsd-static-binary.tar.gz 234 | tar cvzf $staticname ovh-ttyrec-$version 235 | echo "Static tar.gz archive name is $staticname" 236 | echo "::set-output name=staticname::$staticname" 237 | 238 | - name: upload static binary 239 | uses: actions/upload-release-asset@v1 240 | env: 241 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 242 | with: 243 | upload_url: ${{ github.event.release.upload_url }} 244 | asset_path: ${{ steps.build.outputs.staticname }} 245 | asset_name: ${{ steps.build.outputs.staticname }} 246 | asset_content_type: application/gzip 247 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | compiler: 3 | - clang 4 | - gcc 5 | matrix: 6 | include: 7 | - os: osx 8 | osx_image: xcode10.2 9 | 10 | - os: osx 11 | osx_image: xcode9.4 12 | 13 | - os: osx 14 | osx_image: xcode8.3 15 | 16 | - os: osx 17 | osx_image: xcode7.3 18 | 19 | - os: osx 20 | osx_image: xcode6.4 21 | 22 | - os: linux 23 | addons: 24 | apt: 25 | sources: 26 | - ubuntu-toolchain-r-test 27 | packages: 28 | - gcc-4.4 29 | env: 30 | - MATRIX_EVAL="CC=gcc-4.4" 31 | 32 | - os: linux 33 | addons: 34 | apt: 35 | sources: 36 | - ubuntu-toolchain-r-test 37 | packages: 38 | - gcc-4.9 39 | env: 40 | - MATRIX_EVAL="CC=gcc-4.9" 41 | 42 | # works on Precise and Trusty 43 | - os: linux 44 | addons: 45 | apt: 46 | sources: 47 | - ubuntu-toolchain-r-test 48 | packages: 49 | - gcc-5 50 | env: 51 | - MATRIX_EVAL="CC=gcc-5" 52 | 53 | # works on Precise and Trusty 54 | - os: linux 55 | addons: 56 | apt: 57 | sources: 58 | - ubuntu-toolchain-r-test 59 | packages: 60 | - gcc-6 61 | env: 62 | - MATRIX_EVAL="CC=gcc-6" 63 | 64 | # works on Precise and Trusty 65 | - os: linux 66 | addons: 67 | apt: 68 | sources: 69 | - ubuntu-toolchain-r-test 70 | packages: 71 | - gcc-7 72 | env: 73 | - MATRIX_EVAL="CC=gcc-7" 74 | 75 | # works on Precise and Trusty 76 | - os: linux 77 | addons: 78 | apt: 79 | sources: 80 | - ubuntu-toolchain-r-test 81 | packages: 82 | - gcc-7 83 | env: 84 | - MATRIX_EVAL="CC=gcc-8" 85 | 86 | # works on Precise and Trusty 87 | - os: linux 88 | addons: 89 | apt: 90 | sources: 91 | - ubuntu-toolchain-r-test 92 | packages: 93 | - gcc-7 94 | env: 95 | - MATRIX_EVAL="CC=gcc-9" 96 | 97 | before_install: 98 | - eval "${MATRIX_EVAL}" 99 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of ovh-ttyrec authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files 3 | # and it lists the copyright holders only. 4 | 5 | # Names should be added to this file as one of 6 | # Organization's name 7 | # Individual's name 8 | # Individual's name 9 | # See CONTRIBUTORS for the meaning of multiple email addresses. 10 | 11 | # Please keep the list sorted. 12 | 13 | OVH SAS 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ovh-ttyrec 2 | 3 | This project accepts contributions. In order to contribute, you should 4 | pay attention to a few things: 5 | 6 | 1. your code must follow the coding style rules 7 | 2. your code must be unit-tested 8 | 3. your code must be documented 9 | 4. your work must be signed (see below) 10 | 5. you may contribute through GitHub Pull Requests 11 | 12 | # Coding and documentation Style 13 | 14 | Please follow the coding style you'll find in the main ttyrec.c file. 15 | Ensure you `make style` before comitting. 16 | 17 | # Submitting Modifications 18 | 19 | The contributions should be submitted through Github Pull Requests 20 | and follow the DCO which is defined below. 21 | 22 | Note that we want to keep compatibility with ttyrec classic, and as 23 | ovh-ttyrec needs to be extremely stable (as it's used in critical 24 | environment), deep modifications will probably get rejected. 25 | 26 | For this project, stability will always be preferred over fancy. 27 | 28 | # Licensing for new files 29 | 30 | ovh-ttyrec is licensed under a Modified 3-Clause BSD license. Anything 31 | contributed to ovh-ttyrec must be released under this license. 32 | 33 | When introducing a new file into the project, please make sure it has a 34 | copyright header making clear under which license it's being released. 35 | 36 | # Developer Certificate of Origin (DCO) 37 | 38 | To improve tracking of contributions to this project we will use a 39 | process modeled on the modified DCO 1.1 and use a "sign-off" procedure 40 | on patches that are being emailed around or contributed in any other 41 | way. 42 | 43 | The sign-off is a simple line at the end of the explanation for the 44 | patch, which certifies that you wrote it or otherwise have the right 45 | to pass it on as an open-source patch. The rules are pretty simple: 46 | if you can certify the below: 47 | 48 | By making a contribution to this project, I certify that: 49 | 50 | (a) The contribution was created in whole or in part by me and I have 51 | the right to submit it under the open source license indicated in 52 | the file; or 53 | 54 | (b) The contribution is based upon previous work that, to the best of 55 | my knowledge, is covered under an appropriate open source License 56 | and I have the right under that license to submit that work with 57 | modifications, whether created in whole or in part by me, under 58 | the same open source license (unless I am permitted to submit 59 | under a different license), as indicated in the file; or 60 | 61 | (c) The contribution was provided directly to me by some other person 62 | who certified (a), (b) or (c) and I have not modified it. 63 | 64 | (d) The contribution is made free of any other party's intellectual 65 | property claims or rights. 66 | 67 | (e) I understand and agree that this project and the contribution are 68 | public and that a record of the contribution (including all 69 | personal information I submit with it, including my sign-off) is 70 | maintained indefinitely and may be redistributed consistent with 71 | this project or the open source license(s) involved. 72 | 73 | 74 | then you just add a line saying 75 | 76 | Signed-off-by: Random J Developer 77 | 78 | using your real name (sorry, no pseudonyms or anonymous contributions.) 79 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of people who can contribute 2 | # (and typically have contributed) code to the repository. 3 | # 4 | # Names should be added to this file only after verifying that 5 | # the individual or the individual's organization has agreed to 6 | # the appropriate CONTRIBUTING.md file. 7 | # 8 | # Names should be added to this file like so: 9 | # Individual's name 10 | # Individual's name 11 | # 12 | # Please keep the list sorted. 13 | # 14 | Stéphane Lesimple 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2001-2019, OVH SAS. 2 | All rights reserved. 3 | Modified 3-Clause BSD 4 | 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of OVH SAS nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | # This is the official list of the project maintainers. 2 | # This is mostly useful for contributors that want to push 3 | # significant pull requests or for project management issues. 4 | # 5 | # 6 | # Names should be added to this file like so: 7 | # Individual's name 8 | # Individual's name 9 | # 10 | # Please keep the list sorted. 11 | # 12 | Stéphane Lesimple 13 | -------------------------------------------------------------------------------- /Makefile.in: -------------------------------------------------------------------------------- 1 | CC ?= %CC% 2 | CFLAGS += -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -I/usr/local/include %CFLAGS% $(RPM_OPT_FLAGS) 3 | LDFLAGS += -L/usr/local/lib 4 | LDLIBS += %LDLIBS% %PTHREAD% 5 | 6 | BINARIES = ttyrec ttyplay ttytime 7 | 8 | include config.mk 9 | PREFIX ?= /usr/local 10 | BINDIR ?= $(PREFIX)/bin 11 | 12 | UNAME_S := $(shell uname -s) 13 | ifeq ($(UNAME_S),FreeBSD) 14 | MANDIR ?= $(PREFIX)/man 15 | else 16 | MANDIR ?= $(PREFIX)/share/man 17 | endif 18 | 19 | .PHONY: all deb rpm clean distclean style dist install test 20 | 21 | all: $(BINARIES) 22 | 23 | deb: 24 | dpkg-buildpackage -b -rfakeroot -us -uc 25 | 26 | rpm: clean 27 | o=$$PWD && t=$$(mktemp -d) && cd $$t && mkdir ovh-ttyrec && rsync -a --exclude=.git $$o/ ovh-ttyrec/ && zip -9r ~/rpmbuild/SOURCES/master.zip ovh-ttyrec 28 | rpmbuild -bb ovh-ttyrec.spec 29 | ls -lh ~/rpmbuild/RPMS/*/ovh-ttyrec*.rpm 30 | 31 | ttyrec: ttyrec.o io.o compress.o %COMPRESS_ZSTD% 32 | $(CC) $(CFLAGS) -o $@ ttyrec.o io.o compress.o %COMPRESS_ZSTD% $(LDFLAGS) $(LDLIBS) 33 | 34 | ttyplay: ttyplay.o io.o compress.o %COMPRESS_ZSTD% 35 | $(CC) $(CFLAGS) -o $@ ttyplay.o io.o compress.o %COMPRESS_ZSTD% $(LDFLAGS) $(LDLIBS) 36 | 37 | ttytime: ttytime.o io.o compress.o %COMPRESS_ZSTD% 38 | $(CC) $(CFLAGS) -o $@ ttytime.o io.o compress.o %COMPRESS_ZSTD% $(LDFLAGS) $(LDLIBS) 39 | 40 | clean: 41 | rm -f *.o $(BINARIES) ttyrecord *~ 42 | 43 | distclean: clean 44 | rm -f Makefile configure.h 45 | 46 | style: 47 | uncrustify -c uncrustify.cfg -l C --no-backup *.h *.c 48 | 49 | dist: 50 | tar cvzf ttyrec.tar.gz *.c *.h docs/ debian/ configure Makefile.in uncrustify.cfg 51 | 52 | install: 53 | install -d $(DESTDIR)$(BINDIR) 54 | install $(BINARIES) $(DESTDIR)$(BINDIR)/ 55 | install -d $(DESTDIR)$(MANDIR)/man1 56 | install -m 0644 docs/* $(DESTDIR)$(MANDIR)/man1/ 57 | 58 | test: all 59 | ./ttyrec -V 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ovh-ttyrec 2 | ========== 3 | 4 | `ttyrec` is a terminal (tty) recorder, it comes with `ttyplay`, which is a tty player. 5 | 6 | The original ttyrec is Copyright (c) 2000 Satoru Takabayashi. 7 | 8 | The original ttyrec is based on the `script` program, Copyright (c) 1980 Regents of the University of California. 9 | 10 | ovh-ttyrec is based (and compatible with) the original ttyrec, and can be used as a drop-in replacement. It is licensed under the 3-clause BSD license (see LICENSE file). 11 | 12 | Efforts have been made to ensure the code is portable. It is known to work under at least: 13 | 14 | - Linux (all versions and distros) 15 | - BSD (known to work under at least FreeBSD, NetBSD, OpenBSD, DragonFlyBSD) 16 | - Darwin (macOS aka OS X aka Mac OS X) 17 | - Haiku (community OS compatible with BeOS) 18 | - OpenSolaris (known to work under at least OmniOS CE) 19 | 20 | It should work under any POSIX OS that support either `openpty()` or the `grantpt()`/`unlockpt() `mechanisms. 21 | 22 | ## features 23 | 24 | - Drop-in replacement of the classic ttyrec, additional features don't break compatibility 25 | - The code is portable and OS features that can be used are detected at compile time 26 | - Supports on-the-fly (de)compression using the zstd algorithm 27 | - Supports ttyrec output file rotation without interrupting the session 28 | - Supports locking the session after a keyboard input timeout, optionally displaying a custom message 29 | - Supports terminating the session after a keyboard input timeout 30 | - Supports manually locking or terminating the session via "cheatcodes" (specific keystrokes) 31 | - Supports a no-tty mode, relying on pipes instead of pseudottys, while still recording stdout/stderr 32 | - Automatically detects whether to use pseudottys or pipes, also overridable from command-line 33 | - Supports reporting the number of bytes that were output to the terminal on session exit 34 | 35 | ## compilation 36 | 37 | To compile the binaries and build the man pages, just run: 38 | 39 | $ ./configure && make 40 | 41 | You'll need `libzstd` on the build machine if you want ttyrec to be compiled with zstd support. The library will be statically linked when possible. 42 | 43 | If you explicitly don't want libzstd, define `NO_ZSTD=1` before running configure. If you want it but dynamically linked, define `NO_STATIC_ZSTD=1`. 44 | 45 | Installation: 46 | 47 | $ make install 48 | 49 | Note that installation is not needed to test the binaries: you can just call `./ttyrec` from the build folder. 50 | 51 | ## build a .deb package 52 | 53 | If you want to build a .deb (Debian/Ubuntu) package, just run: 54 | 55 | $ ./configure && make deb 56 | 57 | ## build a .rpm package 58 | 59 | If you want to build a .rpm (RHEL/CentOS) package, just run: 60 | 61 | $ ./configure && make rpm 62 | 63 | ## usage 64 | 65 | The simplest usage is just calling the binary, it'll execute the users' shell and record the session until exit: 66 | 67 | $ ttyrec 68 | 69 | To replay this session: 70 | 71 | $ ttyplay ./ttyrecord 72 | 73 | Run some shell commands: 74 | 75 | $ ttyrec -f cmds.ttyrec -- sh -c 'for i in a b c; do echo $i; done' 76 | 77 | Connect to a remote machine interactively, lock the session after 1 minute of inactivity, and kill it after 5 minutes of inactivity: 78 | 79 | $ ttyrec -t 60 -k 300 -- ssh remoteserver 80 | 81 | Execute a local script remotely with the default remote shell: 82 | 83 | $ cat script.sh | ttyrec -- ssh remoteserver 84 | 85 | Record a screen session, with on-the-fly compression: 86 | 87 | $ ttyrec -Z screen 88 | 89 | Usage information: 90 | 91 | $ ttyrec -h 92 | 93 | ## version scheme 94 | 95 | We follow the version format `A.B.C.D`. The following rules apply: 96 | 97 | - A is incremented when the file format of ttyrec changes, as long as A=1, the format is compatible with the original ttyrec (and original ttyplay) 98 | - B is incremented for a breaking change in the way ttyrec can be called (a command-line option was removed for example), which means in that case, other programs or scripts using ttyrec should be checked for compatibility 99 | - C is incremented for any non-hotfix change that stays backwards compatible (a new feature that can be enabled with a new command-line option for example) 100 | - D is incremented for a quickfix/hotfix, or a change in the build system, docs, etc. 101 | 102 | When a digit is incremented, all the "lower" ones go back to zero, i.e. if we are at version 4.7.1.5, and we implement a breaking change, the version number becomes 4.8.0.0. 103 | -------------------------------------------------------------------------------- /compress.c: -------------------------------------------------------------------------------- 1 | // vim: noai:ts=4:sw=4:expandtab: 2 | 3 | /* Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | * Copyright 2019 The ovh-ttyrec Authors. All rights reserved. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "compress.h" 14 | #include "configure.h" 15 | 16 | #ifdef HAVE_zstd 17 | # include "compress_zstd.h" 18 | #endif 19 | 20 | size_t (*fread_wrapper)(void *ptr, size_t size, size_t nmemb, FILE *stream) = fread; 21 | size_t (*fwrite_wrapper)(const void *ptr, size_t size, size_t nmemb, FILE *stream) = fwrite; 22 | int (*fclose_wrapper)(FILE *fp) = fclose; 23 | 24 | static long compress_level = -1; 25 | 26 | int set_compress_mode(compress_mode_t cm) 27 | { 28 | switch (cm) 29 | { 30 | case COMPRESS_NONE: 31 | fread_wrapper = fread; 32 | fwrite_wrapper = fwrite; 33 | fclose_wrapper = fclose; 34 | break; 35 | 36 | #ifdef HAVE_zstd 37 | case COMPRESS_ZSTD: 38 | fread_wrapper = fread_wrapper_zstd; 39 | fwrite_wrapper = fwrite_wrapper_zstd; 40 | fclose_wrapper = fclose_wrapper_zstd; 41 | break; 42 | #endif 43 | 44 | default: 45 | fprintf(stderr, "ttyrec: unsupported compression mode\r\n"); 46 | return 1; 47 | } 48 | return 0; 49 | } 50 | 51 | 52 | void set_compress_level(long level) 53 | { 54 | compress_level = level; 55 | } 56 | 57 | 58 | long get_compress_level(void) 59 | { 60 | return compress_level; 61 | } 62 | -------------------------------------------------------------------------------- /compress.h: -------------------------------------------------------------------------------- 1 | #ifndef __TTYREC_COMPRESS_H__ 2 | #define __TTYREC_COMPRESS_H__ 3 | 4 | #include 5 | #include 6 | 7 | extern size_t (*fwrite_wrapper)(const void *ptr, size_t size, size_t nmemb, FILE *stream); 8 | extern size_t (*fread_wrapper)(void *ptr, size_t size, size_t nmemb, FILE *stream); 9 | extern int (*fclose_wrapper)(FILE *fp); 10 | 11 | typedef enum 12 | { 13 | COMPRESS_NONE = 0, 14 | COMPRESS_ZSTD = 1, 15 | } compress_mode_t; 16 | 17 | int set_compress_mode(compress_mode_t cm); 18 | void set_compress_level(long level); 19 | long get_compress_level(void); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /compress_zstd.c: -------------------------------------------------------------------------------- 1 | // vim: noai:ts=4:sw=4:expandtab: 2 | 3 | /* Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | * Copyright 2019 The ovh-ttyrec Authors. All rights reserved. 6 | */ 7 | 8 | #include "compress.h" 9 | #include "compress_zstd.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | static ZSTD_CStream *cstream = NULL; 18 | static size_t buffOutSize = 0; 19 | static void *buffOut; 20 | static long zstd_max_flush_seconds = ZSTD_MAX_FLUSH_SECONDS_DEFAULT; 21 | 22 | void zstd_set_max_flush(long seconds) 23 | { 24 | zstd_max_flush_seconds = seconds; 25 | } 26 | 27 | 28 | size_t fwrite_wrapper_zstd(const void *ptr, size_t size, size_t nmemb, FILE *stream) 29 | { 30 | static time_t last_sync = 0; 31 | long compress_level = get_compress_level(); 32 | 33 | if (cstream == NULL) 34 | { 35 | cstream = ZSTD_createCStream(); 36 | if (cstream == NULL) 37 | { 38 | fprintf(stderr, "ZSTD_createCStream() error\r\n"); 39 | exit(10); 40 | } 41 | 42 | if (compress_level < 0) 43 | { 44 | compress_level = 3; 45 | } 46 | size_t const initResult = ZSTD_initCStream(cstream, compress_level); 47 | if (ZSTD_isError(initResult)) 48 | { 49 | fprintf(stderr, "ZSTD_initCStream() error: %s\r\n", ZSTD_getErrorName(initResult)); 50 | exit(11); 51 | } 52 | 53 | if (buffOutSize == 0) 54 | { 55 | buffOutSize = ZSTD_CStreamOutSize(); 56 | buffOut = malloc(buffOutSize); 57 | if (buffOut == NULL) 58 | { 59 | fprintf(stderr, "couldn't malloc() zstd out buffer\r\n"); 60 | exit(12); 61 | } 62 | } 63 | 64 | last_sync = time(NULL); 65 | } 66 | 67 | size_t written = 0; 68 | ZSTD_inBuffer input = { ptr, size *nmemb, 0 }; 69 | 70 | while (input.pos < input.size) 71 | { 72 | ZSTD_outBuffer output = { buffOut, buffOutSize, 0 }; 73 | size_t toRead = ZSTD_compressStream(cstream, &output, &input); /* toRead is guaranteed to be <= ZSTD_CStreamInSize() */ 74 | if (ZSTD_isError(toRead)) 75 | { 76 | fprintf(stderr, "ZSTD_compressStream() error: %s\r\n", ZSTD_getErrorName(toRead)); 77 | exit(13); 78 | } 79 | size_t thisWritten = fwrite(buffOut, 1, output.pos, stream); 80 | if (thisWritten != output.pos) 81 | { 82 | return thisWritten; // error or eof, pass to caller 83 | } 84 | written += thisWritten; 85 | } 86 | //fprintf(stderr, "[zstd:nbwr=%lu]", written); 87 | // if we actually did write data to disk (instead of just compressing in memory), 88 | // then we can reset last_sync 89 | if (written > 0) 90 | { 91 | last_sync = time(NULL); 92 | //fprintf(stderr, "[zstd:rst]"); 93 | } 94 | // otherwise, check for last sync time. if it's > X seconds, force zstd to flush its buffers 95 | // and write to disk. we don't want to lose data from almost-idle sessions in case of server crash 96 | else if (last_sync + zstd_max_flush_seconds < time(NULL)) 97 | { 98 | ZSTD_outBuffer output = { buffOut, buffOutSize, 0 }; 99 | written = ZSTD_flushStream(cstream, &output); 100 | if (ZSTD_isError(written)) 101 | { 102 | fprintf(stderr, "ZSTD_flushStream() error: %s\r\n", ZSTD_getErrorName(written)); 103 | exit(14); 104 | } 105 | //fprintf(stderr, "[zstd:tmoutflushed=%lu]", output.pos); 106 | written = fwrite(buffOut, 1, output.pos, stream); 107 | //fprintf(stderr, "[zstd:tmoutnbwr=%lu]", written); 108 | last_sync = time(NULL); 109 | } 110 | return written; 111 | } 112 | 113 | 114 | int fclose_wrapper_zstd(FILE *fp) 115 | { 116 | if (cstream != NULL) 117 | { 118 | ZSTD_outBuffer output = { buffOut, buffOutSize, 0 }; 119 | size_t const remainingToFlush = ZSTD_endStream(cstream, &output); /* close frame */ 120 | if (remainingToFlush) 121 | { 122 | fprintf(stderr, "error: zstd not fully flushed\r\n"); 123 | } 124 | fwrite(buffOut, 1, output.pos, fp); 125 | //fprintf(stderr, "[closezstd:written=%lu]", output.pos); 126 | ZSTD_freeCStream(cstream); 127 | cstream = NULL; 128 | } 129 | return fclose(fp); 130 | } 131 | 132 | 133 | size_t fread_wrapper_zstd(void *ptr, size_t size, size_t nmemb, FILE *stream) 134 | { 135 | // input: compressed data read from file 136 | // output: decompressed data from (a part of) input.src 137 | // buffOutPtr: pointing to decompressed not-yet-returned-to-caller data (remaining bytes is buffOutPtrLen) 138 | 139 | static ZSTD_inBuffer input = { NULL, 0, 0 }; 140 | 141 | static size_t buffOutSize; 142 | static ZSTD_outBuffer output = { NULL, 0, 0 }; 143 | 144 | static char *buffOutPtr = NULL; 145 | static size_t buffOutPtrLen = 0; // number of valid not-yet-returned bytes after ptr 146 | 147 | static ZSTD_DStream *dstream = NULL; 148 | // static because ZSTD_initDStream return the first recommended input size, we'll use ot for first fread() 149 | static size_t toRead; 150 | 151 | size_t remainingBytesToReturn = size * nmemb; 152 | char *returnData = (char *)ptr; 153 | 154 | // init dstream if needed (first call only) 155 | if (dstream == NULL) 156 | { 157 | dstream = ZSTD_createDStream(); 158 | toRead = ZSTD_initDStream(dstream); 159 | 160 | input.src = malloc(ZSTD_DStreamInSize()); 161 | 162 | buffOutSize = ZSTD_DStreamOutSize(); 163 | output.dst = malloc(buffOutSize); 164 | } 165 | 166 | // do we have remaining decompressed data from a previous call, ready to be returned? 167 | GOTDATA: 168 | if (buffOutPtrLen > 0) 169 | { 170 | if (buffOutPtrLen >= remainingBytesToReturn) 171 | { 172 | // easy: we already have all the wanted data in the previous runs buffer 173 | // so we'll just consume data from it and return 174 | memcpy(returnData, buffOutPtr, remainingBytesToReturn); 175 | buffOutPtrLen -= remainingBytesToReturn; 176 | buffOutPtr += remainingBytesToReturn; 177 | return nmemb; 178 | } 179 | else 180 | { 181 | // we have SOME data in the previous runs buffer, use it 182 | memcpy(returnData, buffOutPtr, buffOutPtrLen); 183 | returnData += buffOutPtrLen; 184 | remainingBytesToReturn -= buffOutPtrLen; 185 | buffOutPtrLen = 0; 186 | buffOutPtr = NULL; 187 | } 188 | } 189 | 190 | // if we're here, we don't have any data left in buffOutPtr, and the caller wants more data 191 | // but maybe we still have not-yet-decompressed data from a previously read compressed chunk? 192 | DECOMPRESS: 193 | if (input.pos < input.size) 194 | { 195 | output.pos = 0; 196 | output.size = buffOutSize; 197 | toRead = ZSTD_decompressStream(dstream, &output, &input); /* toRead: size of next compressed block */ 198 | if (ZSTD_isError(toRead)) 199 | { 200 | fprintf(stderr, "ZSTD_decompressStream() error: %s\r\n", ZSTD_getErrorName(toRead)); 201 | exit(16); 202 | } 203 | buffOutPtr = output.dst; // aka buffOut 204 | buffOutPtrLen = output.pos; 205 | if (buffOutPtrLen == 0) 206 | { 207 | // ok this is an empty frame (or beginning of zst stream), read again 208 | goto DECOMPRESS; 209 | } 210 | goto GOTDATA; 211 | } 212 | // nope we don't, alright, decompress a new chunk then 213 | else 214 | { 215 | if (toRead == 0) 216 | { 217 | // the current stream is over, but maybe we have additional streams 218 | // concatenated back-to-back in the file, such as when --append is used? 219 | ZSTD_freeDStream(dstream); 220 | dstream = ZSTD_createDStream(); 221 | toRead = ZSTD_initDStream(dstream); 222 | } 223 | 224 | size_t read = fread((void *)input.src, 1, toRead, stream); 225 | if (read == 0) 226 | { 227 | // eof or error, return it 228 | return 0; 229 | } 230 | input.size = read; 231 | input.pos = 0; 232 | goto DECOMPRESS; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /compress_zstd.h: -------------------------------------------------------------------------------- 1 | #ifndef __TTYREC_COMPRESS_ZSTD_H__ 2 | #define __TTYREC_COMPRESS_ZSTD_H__ 3 | 4 | #include 5 | 6 | #define ZSTD_MAX_FLUSH_SECONDS_DEFAULT 15 7 | 8 | size_t fread_wrapper_zstd(void *ptr, size_t size, size_t nmemb, FILE *stream); 9 | size_t fwrite_wrapper_zstd(const void *ptr, size_t size, size_t nmemb, FILE *stream); 10 | int fclose_wrapper_zstd(FILE *fp); 11 | void zstd_set_max_flush(long seconds); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | curdir="$(dirname "$0")" 3 | 4 | # parse options, they can be specified by the build system (for .deb or .rpm), such as: 5 | # deb: 6 | # --build=x86_64-linux-gnu --prefix=/usr --includedir=\${prefix}/include --mandir=\${prefix}/share/man --infodir=\${prefix}/share/info --sysconfdir=/etc --localstatedir=/var --disable-silent-rules --libdir=\${prefix}/lib/x86_64-linux-gnu --libexecdir=\${prefix}/lib/x86_64-linux-gnu --disable-maintainer-mode --disable-dependency-tracking 7 | # rpm: 8 | # --host=x86_64-pc-linux-gnu --build=x86_64-pc-linux-gnu --program-prefix= --disable-dependency-tracking --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin --sbindir=/usr/sbin --sysconfdir=/etc --datadir=/usr/share --includedir=/usr/include --libdir=/usr/lib64 --libexecdir=/usr/lib/x86_64-linux-gnu --localstatedir=/var --sharedstatedir=/usr/com --mandir=/usr/share/man --infodir=/usr/share/info 9 | 10 | MKCONF="$curdir/config.mk" 11 | rm -f "$MKCONF" 12 | touch "$MKCONF" 13 | while [ -n "$1" ]; do 14 | value=$(echo "$1" | cut -d= -f2- | sed -e "s/\${prefix}/\$(PREFIX)/g") 15 | case "$1" in 16 | --prefix=*) echo "PREFIX ?= $value" >> "$MKCONF"; 17 | echo "Will use PREFIX=$value";; 18 | --mandir=*) echo "MANDIR ?= $value" >> "$MKCONF"; 19 | echo "Will use MANDIR=$value";; 20 | --bindir=*) echo "BINDIR ?= $value" >> "$MKCONF"; 21 | echo "Will use BINDIR=$value";; 22 | esac 23 | shift 24 | done 25 | 26 | echo '#ifndef CONFIGURE_H' >"$curdir/configure.h" 27 | echo '#define CONFIGURE_H' >>"$curdir/configure.h" 28 | 29 | printf "%b" "Looking for compiler... " 30 | [ -z "$CC" ] && CC=gcc 31 | command -v $CC >/dev/null 2>&1 || CC=clang 32 | command -v $CC >/dev/null 2>&1 || CC=cc 33 | echo "$CC" 34 | 35 | LDLIBS='' 36 | DEFINES_STR='uses:' 37 | CFLAGS='-std=c99' 38 | PTHREAD='' 39 | COMPRESS_ZSTD='' 40 | 41 | if [ "$STATIC" = 1 ]; then 42 | CFLAGS="$CFLAGS -static" 43 | fi 44 | 45 | os=$(uname -s) 46 | if [ "$os" = Linux ]; then 47 | CFLAGS="$CFLAGS -D_XOPEN_SOURCE=600 -D_XOPEN_SOURCE_EXTENDED -D_GNU_SOURCE -pipe" 48 | elif [ "$os" = NetBSD ]; then 49 | true 50 | elif [ "$os" = FreeBSD ]; then 51 | true 52 | elif [ "$os" = OpenBSD ]; then 53 | true 54 | elif [ "$os" = DragonFly ]; then 55 | true 56 | elif [ "$os" = Darwin ]; then 57 | true 58 | elif [ "$os" = SunOS ]; then 59 | CFLAGS="$CFLAGS -D__EXTENSIONS__" 60 | elif [ "$os" = Haiku ]; then 61 | true 62 | fi 63 | 64 | srcfile=$(mktemp) 65 | mv "$srcfile" "$srcfile.c" 66 | # shellcheck disable=SC2064 67 | trap "rm -f $srcfile.c" INT HUP EXIT 68 | 69 | printf "%b" "Checking if compiler can create executables... " 70 | cat >"$srcfile.c" </dev/null 2>&1; then 74 | echo "yes" 75 | else 76 | echo "no" 77 | echo 78 | echo "Please ensure you have a working C compiler and relaunch configure." 79 | exit 1 80 | fi 81 | 82 | printf "%b" "Checking how to get pthread support... " 83 | cat >"$srcfile.c" </dev/null 2>&1; then 87 | echo "-pthread" 88 | PTHREAD='-pthread' 89 | else 90 | echo "-lpthread" 91 | PTHREAD='-lpthread' 92 | fi 93 | 94 | 95 | printf "%b" "Looking for libzstd... " 96 | cat >"$srcfile.c" < 98 | int main(void) { ZSTD_CStream *c = ZSTD_createCStream(); ZSTD_initCStream(c, 3); ZSTD_freeCStream(c); return 0; } 99 | EOF 100 | if [ "$NO_ZSTD" != 1 ] && $CC "$srcfile.c" -L/usr/local/lib -I/usr/local/include -lzstd -o /dev/null >/dev/null 2>&1; then 101 | echo "yes" 102 | echo '#define HAVE_zstd' >>"$curdir/configure.h" 103 | COMPRESS_ZSTD='compress_zstd.o' 104 | printf "%b" "Checking whether we can link zstd statically... " 105 | for dir in $($CC -print-search-dirs | awk '/^libraries:/ {$1=""; print}' | tr : "\n") /usr/local/lib 106 | do 107 | test -f "$dir/libzstd.a" && libzstda="$dir/libzstd.a" 108 | done 109 | if [ -n "$libzstda" ] && [ -f "$libzstda" ] && [ "$NO_STATIC_ZSTD" != 1 ]; then 110 | echo "yes ($libzstda)" 111 | DEFINES_STR="$DEFINES_STR zstd[static]" 112 | LDLIBS="$LDLIBS $libzstda" 113 | else 114 | echo "no" 115 | DEFINES_STR="$DEFINES_STR zstd" 116 | LDLIBS="$LDLIBS -lzstd" 117 | fi 118 | else 119 | echo "no" 120 | fi 121 | 122 | printf "%b" "Looking for isastream()... " 123 | cat >"$srcfile.c" < 125 | int main(void) { return isastream(0); } 126 | EOF 127 | if $CC "$srcfile.c" -o /dev/null >/dev/null 2>&1; then 128 | echo "yes" 129 | echo '#define HAVE_isastream' >>"$curdir/configure.h" 130 | DEFINES_STR="$DEFINES_STR isastream" 131 | else 132 | echo "no" 133 | fi 134 | 135 | printf "%b" "Looking for cfmakeraw()... " 136 | cat >"$srcfile.c" < 138 | #include 139 | int main(void) { cfmakeraw(0); return 0; } 140 | EOF 141 | if $CC "$srcfile.c" -o /dev/null >/dev/null 2>&1; then 142 | echo "yes" 143 | echo '#define HAVE_cfmakeraw' >>"$curdir/configure.h" 144 | DEFINES_STR="$DEFINES_STR cfmakeraw" 145 | else 146 | echo "no" 147 | fi 148 | 149 | printf "%b" "Looking for getpt()... " 150 | cat >"$srcfile.c" < 152 | int main(void) { return getpt(); } 153 | EOF 154 | if $CC "$srcfile.c" -o /dev/null >/dev/null 2>&1; then 155 | echo "yes" 156 | echo '#define HAVE_getpt' >>"$curdir/configure.h" 157 | DEFINES_STR="$DEFINES_STR getpt" 158 | else 159 | echo "no" 160 | fi 161 | 162 | printf "%b" "Looking for posix_openpt()... " 163 | cat >"$srcfile.c" < 165 | #include 166 | int main(void) { return posix_openpt(0); } 167 | EOF 168 | if $CC "$srcfile.c" -o /dev/null >/dev/null 2>&1; then 169 | echo "yes" 170 | echo '#define HAVE_posix_openpt' >>"$curdir/configure.h" 171 | DEFINES_STR="$DEFINES_STR posix_openpt" 172 | else 173 | echo "no" 174 | fi 175 | 176 | printf "%b" "Looking for grantpt()... " 177 | cat >"$srcfile.c" < 179 | int main(void) { (void)ptsname(0); return grantpt(0) + unlockpt(0); } 180 | EOF 181 | if $CC "$srcfile.c" -o /dev/null >/dev/null 2>&1; then 182 | echo "yes" 183 | echo '#define HAVE_grantpt' >>"$curdir/configure.h" 184 | DEFINES_STR="$DEFINES_STR grantpt" 185 | else 186 | echo "no" 187 | fi 188 | 189 | printf "%b" "Looking for openpty()... " 190 | cat >"$srcfile.c" < 192 | int main(void) { return openpty(0, 0, 0, 0, 0); } 193 | EOF 194 | if $CC "$srcfile.c" -lutil -o /dev/null >/dev/null 2>&1; then 195 | echo "yes (pty.h, libutil)" 196 | LDLIBS="$LDLIBS -lutil" 197 | echo '#define HAVE_openpty' >>"$curdir/configure.h" 198 | echo '#define HAVE_openpty_pty_h' >>"$curdir/configure.h" 199 | DEFINES_STR="$DEFINES_STR openpty[pty.h]" 200 | else 201 | cat >"$srcfile.c" < 203 | int main(void) { return openpty(0, 0, 0, 0, 0); } 204 | EOF 205 | if $CC "$srcfile.c" -lutil -o /dev/null >/dev/null 2>&1; then 206 | echo "yes (util.h, libutil)" 207 | LDLIBS="$LDLIBS -lutil" 208 | echo '#define HAVE_openpty' >>"$curdir/configure.h" 209 | echo '#define HAVE_openpty_util_h' >>"$curdir/configure.h" 210 | DEFINES_STR="$DEFINES_STR openpty[util.h]" 211 | else 212 | cat >"$srcfile.c" < 214 | int main(void) { return openpty(0, 0, 0, 0, 0); } 215 | EOF 216 | if $CC "$srcfile.c" -lutil -o /dev/null >/dev/null 2>&1; then 217 | echo "yes (libutil.h, libutil)" 218 | LDLIBS="$LDLIBS -lutil" 219 | echo '#define HAVE_openpty' >>"$curdir/configure.h" 220 | echo '#define HAVE_openpty_libutil_h' >>"$curdir/configure.h" 221 | DEFINES_STR="$DEFINES_STR openpty[libutil.h]" 222 | else 223 | cat >"$srcfile.c" < 225 | int main(void) { return openpty(0, 0, 0, 0, 0); } 226 | EOF 227 | if $CC "$srcfile.c" -lbsd -o /dev/null >/dev/null 2>&1; then 228 | echo "yes (pty.h, libbsd)" 229 | echo '#define HAVE_openpty' >>"$curdir/configure.h" 230 | echo '#define HAVE_openpty_pty_h' >>"$curdir/configure.h" 231 | DEFINES_STR="$DEFINES_STR openpty[pty.h/libbsd]" 232 | LDLIBS="$LDLIBS -lbsd" 233 | else 234 | echo "no" 235 | fi 236 | fi 237 | fi 238 | fi 239 | 240 | echo "Checking for supported compiler options..." 241 | for w in -Wall -Wextra -pedantic -Wno-unused-result -Wbad-function-cast -Wmissing-declarations \ 242 | -Wmissing-prototypes -Wnested-externs -Wold-style-definition -Wstrict-prototypes \ 243 | -Wpointer-sign -Wmissing-parameter-type -Wold-style-declaration -Wl,--as-needed \ 244 | -Wno-unused-command-line-argument 245 | do 246 | echo 'int main(void) { return 0; }' >"$srcfile.c" 247 | if [ "$($CC "$srcfile.c" $w -o /dev/null 2>&1 | wc -l)" = 0 ]; then 248 | echo "... OK $w" 249 | if echo "$w" | grep -q -- '-Wl,'; then 250 | LDFLAGS="$LDFLAGS $w" 251 | else 252 | CFLAGS="$CFLAGS $w" 253 | fi 254 | else 255 | echo "... unsupported $w" 256 | fi 257 | done 258 | 259 | cat "$(dirname "$0")"/Makefile.in > "$(dirname "$0")"/Makefile.tmp 260 | for i in CC LDLIBS CFLAGS COMPRESS_ZSTD PTHREAD 261 | do 262 | replace=$(eval printf "%b" "\"\$$i\"") 263 | sed "s:%$i%:$replace:g" "$(dirname "$0")"/Makefile.tmp > "$(dirname "$0")"/Makefile 264 | cat "$(dirname "$0")"/Makefile > "$(dirname "$0")"/Makefile.tmp 265 | done 266 | rm -f "$(dirname "$0")"/Makefile.tmp 267 | 268 | cat >>"$curdir/configure.h" < Thu, 09 May 2019 12:56:27 +0200 8 | -------------------------------------------------------------------------------- /debian/README.source: -------------------------------------------------------------------------------- 1 | ovh-ttyrec for Debian 2 | --------------------- 3 | 4 | See the included README.md file for more information. 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | ovh-ttyrec (1.1.7.1) master; urgency=medium 2 | 3 | * fix: ttyplay: playing zstd-compressed files created in append mode halted after first stream 4 | 5 | -- Stéphane Lesimple (deb packages) Mon, 18 Sep 2023 10:12:21 +0200 6 | 7 | ovh-ttyrec (1.1.7.0) master; urgency=medium 8 | 9 | * feat: add --stealth-stdout and --stealth-stderr 10 | 11 | -- Stéphane Lesimple (deb packages) Fri, 15 Sep 2023 15:39:33 +0200 12 | 13 | ovh-ttyrec (1.1.6.7) master; urgency=medium 14 | 15 | * fix: rare interlocking on exit 16 | * enh: default install prefix is now /usr/local 17 | * fix: FreeBSD compilation 18 | * chore: autobuild for FreeBSD 19 | 20 | -- Stéphane Lesimple (deb packages) Mon, 29 Mar 2021 17:48:37 +0200 21 | 22 | ovh-ttyrec (1.1.6.6) master; urgency=medium 23 | 24 | * chore: display machine triplet in -V 25 | 26 | -- Stéphane Lesimple (deb packages) Mon, 09 Nov 2020 10:23:41 +0100 27 | 28 | ovh-ttyrec (1.1.6.5) master; urgency=medium 29 | 30 | * fix: race condition when running w/o pty 31 | 32 | -- Stéphane Lesimple (deb packages) Tue, 15 Sep 2020 10:59:22 +0200 33 | 34 | ovh-ttyrec (1.1.6.4) master; urgency=medium 35 | 36 | * fix: -k was not working correctly when used without -t 37 | 38 | -- Stéphane Lesimple (deb packages) Thu, 05 Mar 2020 16:29:12 +0100 39 | 40 | ovh-ttyrec (1.1.6.3) master; urgency=medium 41 | 42 | * fix: race condition on exit when a sighandler gets called while we're in libc's exit(), fixes #7 43 | 44 | -- Stéphane Lesimple (deb packages) Thu, 10 Oct 2019 14:43:17 +0200 45 | 46 | ovh-ttyrec (1.1.6.2) master; urgency=medium 47 | 48 | * fix: race condition on exit where ttyrec could get stuck 49 | 50 | -- Stéphane Lesimple (deb packages) Fri, 30 Aug 2019 12:39:23 +0200 51 | 52 | ovh-ttyrec (1.1.6.1) master; urgency=medium 53 | 54 | * enh: with -f, auto-append .zst if -Z or --zstd was specified 55 | * fix: allow usage of -f even if -F if specified 56 | 57 | -- Stéphane Lesimple (deb packages) Fri, 14 Jun 2019 12:41:54 +0200 58 | 59 | ovh-ttyrec (1.1.6.0) master; urgency=medium 60 | 61 | * feat: added generic fread/fwrite/fclose wrappers as a framework to support several (de)compression algorithms 62 | * feat: add zstd support to ttyrec and ttyplay 63 | ttyrec: add -Z option to enable on-the-fly compression if available 64 | ttyrec: add --zstd to force on-the-fly zstd compression 65 | ttyrec: add -l option to fine-tune the zstd compression ratio (between 1 and 19, default 3) 66 | ttyrec: add --max-flush-time to specify a number of seconds after which we force zstd to flush 67 | its output buffers to ensure somewhat idle sessions still get flushed to disk regularly 68 | ttyplay: zstd decompression is automatically enabled if the file suffix is ".zst" 69 | ttyplay: add -Z option to force on-the-fly zstd decompression regardless of the file suffix 70 | ttytime: add a warning if timing a .zst file is attempted (not supported) 71 | * feat: implement long-options parsing for ttyrec 72 | * feat: add --name-format (-F) to specify a custom file name compatible with strftime() 73 | * feat: add --warn-before-lock and --warn-before-kill options 74 | * fix: abort if doinput() can't write to master 75 | * chore: nicify termios debug output 76 | * chore: get rid of help2man requirement 77 | * chore: portability fixes, tested under Linux, FreeBSD, NetBSD, OpenBSD, DragonFlyBSD, Darwin, Haiku, OmniOS 78 | 79 | -- Stéphane Lesimple (deb packages) Tue, 04 Jun 2019 10:17:11 +0200 80 | 81 | ovh-ttyrec (1.1.5.0) master; urgency=medium 82 | 83 | * First public release 84 | * Add -c option to enable cheatcodes, as they're now disabled by default 85 | 86 | -- Stéphane Lesimple (deb packages) Thu, 09 May 2019 12:55:21 +0200 87 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: ovh-ttyrec 2 | Section: ovh 3 | Priority: extra 4 | Maintainer: Stéphane Lesimple 5 | Build-Depends: debhelper (>= 7.0.50~) 6 | Standards-Version: 3.8.4 7 | 8 | Package: ovh-ttyrec 9 | Architecture: any 10 | Depends: ${shlibs:Depends}, ${misc:Depends} 11 | Conflicts: ttyrec 12 | Description: Extended (but compatible) fork of ttyrec 13 | ttyrec is a terminal (tty) recorder, it comes with ttyplay, which is a tty player. 14 | Some features of ovh-ttyrec follow: 15 | - Drop-in replacement of the classic ttyrec, additional features don't break compatibility 16 | - The code is portable and OS features that can be used are detected at compile time 17 | - Supports ttyrec output file rotation without interrupting the session 18 | - Supports locking the session after a keyboard input timeout, optionally displaying a custom message 19 | - Supports terminating the session after a keyboard input timeout 20 | - Supports manually locking or terminating the session via "cheatcodes" (specific keystrokes) 21 | - Supports a no-tty mode, relying on pipes instead of pseudottys, while still recording stdout/stderr 22 | - Automatically detects whether to use pseudottys or pipes, also overridable from command-line 23 | - Supports reporting the number of bytes that were output to the terminal on session exit 24 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | This work was packaged for Debian by: 2 | 3 | Stéphane Lesimple on Fri, 13 Jul 2012 15:16:31 +0200 4 | 5 | Upstream Author(s): 6 | 7 | Stéphane Lesimple 8 | 9 | Copyright: 10 | 11 | Copyright 2001-2019, OVH SAS. 12 | 13 | License: 14 | 15 | See included LICENSE file. 16 | 17 | The Debian packaging is: 18 | 19 | Under the same terms as the included LICENSE file. 20 | 21 | -------------------------------------------------------------------------------- /debian/links: -------------------------------------------------------------------------------- 1 | usr/bin/ttyrec usr/bin/ovh-ttyrec 2 | -------------------------------------------------------------------------------- /debian/manpages: -------------------------------------------------------------------------------- 1 | docs/ttyplay.1 2 | docs/ttyrec.1 3 | docs/ttytime.1 4 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ 14 | -------------------------------------------------------------------------------- /docs/ttyplay.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\" This manual page is written by NAKANO Takeo 3 | .\" 4 | .TH TTYPLAY 1 5 | .SH NAME 6 | ttyplay \- player of the tty session recorded by ttyrec 7 | .SH SYNOPSIS 8 | .br 9 | .B ttyplay 10 | .I [\-s SPEED] [\-n] [\-p] file 11 | .br 12 | .SH DESCRIPTION 13 | .B Ttyplay 14 | plays the tty session in 15 | .IR file , 16 | which was recorded previously by 17 | .BR ttyrec (1). 18 | .PP 19 | When 20 | .B \-p 21 | option is given, 22 | .B ttyplay 23 | output the 24 | .I file 25 | as it grows. 26 | It means that you can see the "live" shell session 27 | running by another user. 28 | .PP 29 | If you hit any key during playback, it will go right to the next 30 | character typed. This is handy when examining sessions where a user 31 | spends a lot of time at a prompt. 32 | .PP 33 | Additionally, there are some special keys defined: 34 | .TP 35 | .BI + " or " f 36 | double the speed of playback. 37 | .TP 38 | .BI \- " or " s 39 | halve the speed of playback. 40 | .TP 41 | .BI 1 42 | set playback to speed 1.0 again. 43 | 44 | .SH OPTIONS 45 | .TP 46 | .BI \-s " SPEED" 47 | multiple the playing speed by 48 | .I SPEED 49 | (default is 1). 50 | .TP 51 | .B \-n 52 | no wait mode. 53 | Ignore the timing information in 54 | .IR file . 55 | .TP 56 | .B \-p 57 | peek another person's tty session. 58 | .SH "SEE ALSO" 59 | .BR script (1), 60 | .BR ttyrec (1), 61 | .BR ttytime (1) 62 | 63 | -------------------------------------------------------------------------------- /docs/ttyrec.1: -------------------------------------------------------------------------------- 1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. 2 | .TH TTYREC "1" "June 2019" "ttyrec v1.1.5.0" "User Commands" 3 | .SH NAME 4 | ttyrec \- manual page for ttyrec v1.1.5.0 5 | .SH SYNOPSIS 6 | .B ttyrec 7 | [\fI\,options\/\fR] \fI\,-- \/\fR[\fI\,command options\/\fR] 8 | .TP 9 | legacy compatibility mode: 10 | .B ttyrec \fB\-e\fR [options] [ttyrec file name] 11 | .SH OPTIONS 12 | .TP 13 | \fB\-z\fR, \fB\-\-uuid\fR UUID 14 | specify an UUID (can be any string) that will appear in the ttyrec output file names, 15 | and kept with SIGUSR1 rotations (default: own PID) 16 | .TP 17 | \fB\-f\fR, \fB\-\-output\fR FILE 18 | full path of the first ttyrec file to write to (autogenerated if omitted) 19 | .TP 20 | \fB\-d\fR, \fB\-\-dir\fR FOLDER 21 | folder where to write the ttyrec files (taken from \fB\-f\fR if omitted, 22 | defaulting to working directory if both \fB\-f\fR and \fB\-d\fR are omitted) 23 | .TP 24 | \fB\-F\fR, \fB\-\-name\-format\fR FMT 25 | custom strftime\-compatible format string to qualify the full path of the output files, 26 | including the SIGUSR1 rotated ones 27 | .TP 28 | \fB\-a\fR, \fB\-\-append\fR 29 | open the ttyrec output file in append mode instead of write\-clobber mode 30 | .TP 31 | \fB\-Z\fR 32 | enable on\-the\-fly compression if available, silently fallback to no compression if not 33 | .TP 34 | \fB\-\-zstd\fR 35 | force on\-the\-fly compression of output file using zstd, 36 | the resulting file will have a '.ttyrec.zst' extension 37 | .TP 38 | \fB\-\-max\-flush\-time\fR S 39 | specify the maximum number of seconds after which we'll force zstd to flush its output buffers 40 | to ensure that even somewhat quiet sessions gets regularly written out to disk, default is 15 41 | .TP 42 | \fB\-l\fR, \fB\-\-level\fR LEVEL 43 | set compression level, must be between 1 and 19 for zstd, default is 3 44 | .TP 45 | \fB\-n\fR, \fB\-\-count\-bytes\fR 46 | count the number of bytes out and print it on termination (experimental) 47 | .TP 48 | \fB\-t\fR, \fB\-\-lock\-timeout\fR S 49 | lock session on input timeout after S seconds 50 | .TP 51 | \fB\-\-warn\-before\-lock\fR S 52 | warn S seconds before locking (see \fB\-\-lock\-timeout\fR) 53 | .TP 54 | \fB\-k\fR, \fB\-\-kill\-timeout\fR S 55 | kill session on input timeout after S seconds 56 | .TP 57 | \fB\-\-warn\-before\-kill\fR S 58 | warn S seconds before killing (see \fB\-\-kill\-timeout\fR) 59 | .TP 60 | \fB\-C\fR, \fB\-\-no\-cheatcodes\fR 61 | disable cheat\-codes (see below), this is the default 62 | .TP 63 | \fB\-c\fR, \fB\-\-cheatcodes\fR 64 | enable cheat\-codes (see below) 65 | .TP 66 | \fB\-p\fR, \fB\-\-no\-openpty\fR 67 | don't use openpty() even when it's available 68 | .TP 69 | \fB\-T\fR, \fB\-\-term\fR MODE 70 | MODE can be either 'never' (never allocate a pseudotty, even if stdin is a tty, and use pipes to 71 | handle stdout/stderr instead), 'always' (always allocate a pseudotty, even if stdin is not a tty) 72 | or 'auto' (default, allocate a pseudotty if stdin is a tty, uses pipes otherwise) 73 | .TP 74 | \fB\-v\fR, \fB\-\-verbose\fR 75 | verbose (debug) mode, use twice for more verbosity 76 | .TP 77 | \fB\-V\fR, \fB\-\-version\fR 78 | show version information 79 | .TP 80 | \fB\-e\fR, \fB\-\-shell\-cmd\fR CMD 81 | enables legacy compatibility mode and specifies the command to be run under the user's $SHELL \fB\-c\fR 82 | .SH EXAMPLES 83 | .TP 84 | Run some shell commands in legacy mode: 85 | .B ttyrec \-e 'for i in a b c; do echo $i; done' outfile.ttyrec 86 | .TP 87 | Run some shell commands in normal mode: 88 | .B ttyrec \-f /tmp/normal.ttyrec \-\- sh \-c 'for i in a b c; do echo $i; done' 89 | .TP 90 | Connect to a remote machine interactively: 91 | .B ttyrec \-t 60 \-k 300 \-\- ssh remoteserver 92 | .TP 93 | Execute a local script remotely with the default remote shell: 94 | .B ttyrec \-\- ssh remoteserver < script.sh 95 | .TP 96 | Record a screen session: 97 | .B ttyrec screen 98 | .SH FOOTNOTES 99 | .SS "Handled signals:" 100 | .TP 101 | SIGUSR1 102 | close current ttyrec file and reopen a new one (log rotation) 103 | .TP 104 | SIGURG 105 | lock session 106 | .TP 107 | SIGUSR2 108 | unlock session 109 | .SS "Cheat-codes (magic keystrokes combinations):" 110 | .TP 111 | ^L^L^L^L^L^L^L^L 112 | lock your session (that's 8 CTRL+L's) 113 | .TP 114 | ^K^I^L^L^K^I^L^L 115 | kill your session 116 | .SS "Remark about session lock and session kill:" 117 | .IP 118 | If we don't have a tty, we can't lock, so \-t will be ignored, 119 | whereas \-k will be applied without warning, as there's no tty to output a warning to. 120 | -------------------------------------------------------------------------------- /docs/ttytime.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\" This manual page is written by NAKANO Takeo 3 | .\" 4 | .TH TTYTIME 1 5 | .SH NAME 6 | ttytime \- print the time of the recorded session data by ttyrec(1) 7 | .SH SYNOPSIS 8 | .br 9 | .B ttytime 10 | .I file... 11 | .SH DESCRIPTION 12 | .B Ttytime 13 | tells you the time of recorded data in seconds. 14 | For example: 15 | .sp 16 | .RS 17 | .nf 18 | % ttytime *.tty 19 | 173 foo.tty 20 | 1832 bar.tty 21 | .fi 22 | .RE 23 | .SH "SEE ALSO" 24 | .BR script (1), 25 | .BR ttyrec (1), 26 | .BR ttyplay (1) 27 | 28 | -------------------------------------------------------------------------------- /io.c: -------------------------------------------------------------------------------- 1 | /* Use of this source code is governed by a BSD-style 2 | * license that can be found in the LICENSE file. 3 | * Copyright 2001-2019 The ovh-ttyrec Authors. All rights reserved. 4 | * 5 | * This work is based on the original ttyrec, whose license text 6 | * can be found below unmodified. 7 | * 8 | * Copyright (c) 2000 Satoru Takabayashi 9 | * All rights reserved. 10 | * 11 | * Redistribution and use in source and binary forms, with or without 12 | * modification, are permitted provided that the following conditions 13 | * are met: 14 | * 1. Redistributions of source code must retain the above copyright 15 | * notice, this list of conditions and the following disclaimer. 16 | * 2. Redistributions in binary form must reproduce the above copyright 17 | * notice, this list of conditions and the following disclaimer in the 18 | * documentation and/or other materials provided with the distribution. 19 | * 3. All advertising materials mentioning features or use of this software 20 | * must display the following acknowledgement: 21 | * This product includes software developed by the University of 22 | * California, Berkeley and its contributors. 23 | * 4. Neither the name of the University nor the names of its contributors 24 | * may be used to endorse or promote products derived from this software 25 | * without specific prior written permission. 26 | * 27 | * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 28 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 30 | * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 31 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 32 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 33 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 34 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 35 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 36 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 37 | * SUCH DAMAGE. 38 | */ 39 | 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | 47 | #include "io.h" 48 | #include "ttyrec.h" 49 | #include "compress.h" 50 | 51 | #define SWAP_ENDIAN(val) \ 52 | ((unsigned int)( \ 53 | (((unsigned int)(val) & (unsigned int)0x000000ffU) << 24) | \ 54 | (((unsigned int)(val) & (unsigned int)0x0000ff00U) << 8) | \ 55 | (((unsigned int)(val) & (unsigned int)0x00ff0000U) >> 8) | \ 56 | (((unsigned int)(val) & (unsigned int)0xff000000U) >> 24))) 57 | 58 | static int is_little_endian(void) 59 | { 60 | static int retval = -1; 61 | 62 | if (retval == -1) 63 | { 64 | int n = 1; 65 | char *p = (char *)&n; 66 | char x[] = { 1, 0, 0, 0 }; 67 | 68 | assert(sizeof(int) == 4); 69 | 70 | if (memcmp(p, x, 4) == 0) 71 | { 72 | retval = 1; 73 | } 74 | else 75 | { 76 | retval = 0; 77 | } 78 | } 79 | 80 | return retval; 81 | } 82 | 83 | 84 | static int convert_to_little_endian(int x) 85 | { 86 | if (is_little_endian()) 87 | { 88 | return x; 89 | } 90 | else 91 | { 92 | return SWAP_ENDIAN(x); 93 | } 94 | } 95 | 96 | 97 | int read_header(FILE *fp, Header *h) 98 | { 99 | int buf[3]; 100 | 101 | if (fread_wrapper(buf, sizeof(int), 3, fp) == 0) 102 | { 103 | return 0; 104 | } 105 | 106 | h->tv.tv_sec = convert_to_little_endian(buf[0]); 107 | h->tv.tv_usec = convert_to_little_endian(buf[1]); 108 | h->len = convert_to_little_endian(buf[2]); 109 | 110 | return 1; 111 | } 112 | 113 | 114 | int write_header(FILE *fp, Header *h) 115 | { 116 | int buf[3]; 117 | 118 | buf[0] = convert_to_little_endian(h->tv.tv_sec); 119 | buf[1] = convert_to_little_endian(h->tv.tv_usec); 120 | buf[2] = convert_to_little_endian(h->len); 121 | 122 | if (fwrite_wrapper(buf, sizeof(int), 3, fp) == 0) 123 | { 124 | return 0; 125 | } 126 | 127 | return 1; 128 | } 129 | 130 | 131 | static char *progname = ""; 132 | void set_progname(const char *name) 133 | { 134 | progname = strdup(name); 135 | } 136 | 137 | 138 | FILE *efopen(const char *path, const char *mode) 139 | { 140 | FILE *fp = fopen(path, mode); 141 | 142 | if (fp == NULL) 143 | { 144 | fprintf(stderr, "%s: %s: %s\n", progname, path, strerror(errno)); 145 | exit(EXIT_FAILURE); 146 | } 147 | return fp; 148 | } 149 | 150 | 151 | int edup(int oldfd) 152 | { 153 | int fd = dup(oldfd); 154 | 155 | if (fd == -1) 156 | { 157 | fprintf(stderr, "%s: dup failed: %s\n", progname, strerror(errno)); 158 | exit(EXIT_FAILURE); 159 | } 160 | return fd; 161 | } 162 | 163 | 164 | int edup2(int oldfd, int newfd) 165 | { 166 | int fd = dup2(oldfd, newfd); 167 | 168 | if (fd == -1) 169 | { 170 | fprintf(stderr, "%s: dup2 failed: %s\n", progname, strerror(errno)); 171 | exit(EXIT_FAILURE); 172 | } 173 | return fd; 174 | } 175 | 176 | 177 | FILE *efdopen(int fd, const char *mode) 178 | { 179 | FILE *fp = fdopen(fd, mode); 180 | 181 | if (fp == NULL) 182 | { 183 | fprintf(stderr, "%s: fdopen failed: %s\n", progname, strerror(errno)); 184 | exit(EXIT_FAILURE); 185 | } 186 | return fp; 187 | } 188 | -------------------------------------------------------------------------------- /io.h: -------------------------------------------------------------------------------- 1 | #ifndef __TTYREC_IO_H__ 2 | #define __TTYREC_IO_H__ 3 | 4 | #include "ttyrec.h" 5 | 6 | int read_header(FILE *fp, Header *h); 7 | int write_header(FILE *fp, Header *h); 8 | FILE *efopen(const char *path, const char *mode); 9 | int edup(int oldfd); 10 | int edup2(int oldfd, int newfd); 11 | FILE *efdopen(int fd, const char *mode); 12 | void set_progname(const char *name); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /ovh-ttyrec.spec: -------------------------------------------------------------------------------- 1 | Summary: Extended (but compatible) fork of ttyrec 2 | Name: ovh-ttyrec 3 | Version: 1.1.7.1 4 | Release: 1 5 | License: BSD 6 | Group: Applications/System 7 | Source: https://github.com/ovh/ovh-ttyrec/archive/master.zip 8 | 9 | %description 10 | Extended (but compatible) fork of ttyrec. 11 | ttyrec is a terminal (tty) recorder, 12 | it comes with ttyplay, which is a tty player. 13 | 14 | Some features of ovh-ttyrec follow: 15 | - Drop-in replacement of the classic ttyrec, 16 | additional features don't break compatibility 17 | - The code is portable and OS features that 18 | can be used are detected at compile time 19 | - Supports ttyrec output file rotation 20 | without interrupting the session 21 | - Supports locking the session after a keyboard input 22 | timeout, optionally displaying a custom message 23 | - Supports terminating the session after a keyboard input timeout 24 | - Supports manually locking or terminating the 25 | session via "cheatcodes" (specific keystrokes) 26 | - Supports a no-tty mode, relying on pipes instead 27 | of pseudottys, while still recording stdout/stderr 28 | - Automatically detects whether to use pseudottys 29 | or pipes, also overridable from command-line 30 | - Supports reporting the number of bytes that 31 | were output to the terminal on session exit 32 | 33 | %prep 34 | 35 | %setup -q -n ovh-ttyrec 36 | 37 | %build 38 | %configure 39 | %make_build 40 | 41 | %install 42 | %make_install 43 | find "$RPM_BUILD_ROOT"/usr/bin -type f -exec strip '{}' \; 44 | find "$RPM_BUILD_ROOT" 45 | 46 | %clean 47 | rm -rf -- "$RPM_BUILD_ROOT" 48 | 49 | %files 50 | %{_mandir}/man1/ttyplay.* 51 | %{_mandir}/man1/ttytime.* 52 | %{_mandir}/man1/ttyrec.* 53 | %{_bindir}/ttyplay 54 | %{_bindir}/ttytime 55 | %{_bindir}/ttyrec 56 | 57 | %changelog 58 | * Mon Sep 18 2023 Stéphane Lesimple (deb packages) 1.1.7.1 59 | - fix: ttyplay: playing zstd-compressed files created in append mode halted after first stream 60 | 61 | * Fri Sep 15 2023 Stéphane Lesimple (deb packages) 1.1.7.0 62 | - feat: add --stealth-stdout and --stealth-stderr 63 | 64 | * Mon Mar 29 2021 Stéphane Lesimple (deb packages) 1.1.6.7 65 | - fix: rare interlocking on exit 66 | - enh: default install prefix is now /usr/local 67 | - fix: FreeBSD compilation 68 | - chore: autobuild for FreeBSD 69 | 70 | * Mon Nov 09 2020 Stéphane Lesimple (deb packages) 1.1.6.6 71 | - chore: display machine triplet in -V 72 | 73 | * Tue Sep 15 2020 Stéphane Lesimple (deb packages) 1.1.6.5 74 | - fix: race condition when running w/o pty 75 | 76 | * Thu Mar 05 2020 Stéphane Lesimple (deb packages) 1.1.6.4 77 | - fix: -k was not working correctly when used without -t 78 | 79 | * Thu Oct 10 2019 Stéphane Lesimple (deb packages) 1.1.6.3 80 | - fix: race condition on exit when a sighandler gets called while we're in libc's exit(), fixes #7 81 | 82 | * Fri Aug 30 2019 Stéphane Lesimple (deb packages) 1.1.6.2 83 | - fix: race condition on exit where ttyrec could get stuck 84 | 85 | * Fri Jun 14 2019 Stéphane Lesimple (deb packages) 1.1.6.1 86 | - enh: with -f, auto-append .zst if -Z or --zstd was specified 87 | - fix: allow usage of -f even if -F if specified 88 | 89 | * Tue Jun 04 2019 Stéphane Lesimple (deb packages) 1.1.6.0 90 | - feat: added generic fread/fwrite/fclose wrappers as a framework to support several (de)compression algorithms 91 | - feat: add zstd support to ttyrec and ttyplay 92 | ttyrec: add -Z option to enable on-the-fly compression if available 93 | ttyrec: add --zstd to force on-the-fly zstd compression 94 | ttyrec: add -l option to fine-tune the zstd compression ratio (between 1 and 19, default 3) 95 | ttyrec: add --max-flush-time to specify a number of seconds after which we force zstd to flush 96 | its output buffers to ensure somewhat idle sessions still get flushed to disk regularly 97 | ttyplay: zstd decompression is automatically enabled if the file suffix is ".zst" 98 | ttyplay: add -Z option to force on-the-fly zstd decompression regardless of the file suffix 99 | ttytime: add a warning if timing a .zst file is attempted (not supported) 100 | - feat: implement long-options parsing for ttyrec 101 | - feat: add --name-format (-F) to specify a custom file name compatible with strftime() 102 | - feat: add --warn-before-lock and --warn-before-kill options 103 | - fix: abort if doinput() can't write to master 104 | - chore: nicify termios debug output 105 | - chore: get rid of help2man requirement 106 | - chore: portability fixes, tested under Linux, FreeBSD, NetBSD, OpenBSD, DragonFlyBSD, Darwin, Haiku, OmniOS 107 | 108 | * Thu May 09 2019 Stéphane Lesimple (deb packages) 1.1.5.0 109 | - First public release 110 | - Add -c option to enable cheatcodes, as they're now disabled by default 111 | -------------------------------------------------------------------------------- /ttyplay.c: -------------------------------------------------------------------------------- 1 | // vim: noai:ts=4:sw=4:expandtab: 2 | 3 | /* Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | * Copyright 2001-2019 The ovh-ttyrec Authors. All rights reserved. 6 | * 7 | * This work is based on the original ttyrec, whose license text 8 | * can be found below unmodified. 9 | * 10 | * Copyright (c) 2000 Satoru Takabayashi 11 | * All rights reserved. 12 | * 13 | * Redistribution and use in source and binary forms, with or without 14 | * modification, are permitted provided that the following conditions 15 | * are met: 16 | * 1. Redistributions of source code must retain the above copyright 17 | * notice, this list of conditions and the following disclaimer. 18 | * 2. Redistributions in binary form must reproduce the above copyright 19 | * notice, this list of conditions and the following disclaimer in the 20 | * documentation and/or other materials provided with the distribution. 21 | * 3. All advertising materials mentioning features or use of this software 22 | * must display the following acknowledgement: 23 | * This product includes software developed by the University of 24 | * California, Berkeley and its contributors. 25 | * 4. Neither the name of the University nor the names of its contributors 26 | * may be used to endorse or promote products derived from this software 27 | * without specific prior written permission. 28 | * 29 | * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 30 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 31 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 32 | * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 33 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 34 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 35 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 36 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 37 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 38 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 39 | * SUCH DAMAGE. 40 | */ 41 | 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | 50 | #include "ttyrec.h" 51 | #include "io.h" 52 | #include "compress.h" 53 | #include "configure.h" 54 | 55 | typedef double (*WaitFunc) (struct timeval prev, 56 | struct timeval cur, 57 | double speed); 58 | typedef int (*ReadFunc) (FILE *fp, Header *h, char **buf); 59 | typedef void (*WriteFunc) (char *buf, int len); 60 | typedef void (*ProcessFunc) (FILE *fp, double speed, 61 | ReadFunc read_func, WaitFunc wait_func); 62 | 63 | struct timeval timeval_diff(struct timeval tv1, struct timeval tv2); 64 | struct timeval timeval_div(struct timeval tv1, double n); 65 | double ttywait(struct timeval prev, struct timeval cur, double speed); 66 | double ttynowait(struct timeval prev, struct timeval cur, double speed); 67 | int ttyread(FILE *fp, Header *h, char **buf); 68 | int ttypread(FILE *fp, Header *h, char **buf); 69 | void ttywrite(char *buf, int len); 70 | void ttynowrite(char *buf, int len); 71 | void ttyplay(FILE *fp, double speed, ReadFunc read_func, WriteFunc write_func, WaitFunc wait_func); 72 | void ttyskipall(FILE *fp); 73 | void ttyplayback(FILE *fp, double speed, ReadFunc read_func, WaitFunc wait_func); 74 | void ttypeek(FILE *fp, double speed, ReadFunc read_func, WaitFunc wait_func); 75 | void usage(void); 76 | FILE *input_from_stdin(void); 77 | 78 | 79 | struct timeval timeval_diff(struct timeval tv1, struct timeval tv2) 80 | { 81 | struct timeval diff; 82 | 83 | diff.tv_sec = tv2.tv_sec - tv1.tv_sec; 84 | diff.tv_usec = tv2.tv_usec - tv1.tv_usec; 85 | if (diff.tv_usec < 0) 86 | { 87 | diff.tv_sec--; 88 | diff.tv_usec += 1000000; 89 | } 90 | 91 | return diff; 92 | } 93 | 94 | 95 | struct timeval timeval_div(struct timeval tv1, double n) 96 | { 97 | double x = ((double)tv1.tv_sec + (double)tv1.tv_usec / 1000000.0) / n; 98 | struct timeval div; 99 | 100 | div.tv_sec = (int)x; 101 | div.tv_usec = (x - (int)x) * 1000000; 102 | 103 | return div; 104 | } 105 | 106 | 107 | double ttywait(struct timeval prev, struct timeval cur, double speed) 108 | { 109 | static struct timeval drift = { 0, 0 }; 110 | struct timeval start; 111 | struct timeval diff = timeval_diff(prev, cur); 112 | fd_set readfs; 113 | 114 | gettimeofday(&start, NULL); 115 | 116 | assert(speed != 0); 117 | diff = timeval_diff(drift, timeval_div(diff, speed)); 118 | if (diff.tv_sec < 0) 119 | { 120 | diff.tv_sec = diff.tv_usec = 0; 121 | } 122 | 123 | FD_SET(STDIN_FILENO, &readfs); 124 | 125 | /* 126 | * We use select() for sleeping with subsecond precision. 127 | * select() is also used to wait user's input from a keyboard. 128 | * 129 | * Save "diff" since select(2) may overwrite it to {0, 0}. 130 | */ 131 | struct timeval orig_diff = diff; 132 | 133 | select(1, &readfs, NULL, NULL, &diff); 134 | diff = orig_diff; /* Restore the original diff value. */ 135 | if (FD_ISSET(0, &readfs)) /* a user hits a character? */ 136 | { 137 | char c; 138 | if (read(STDIN_FILENO, &c, 1) == 1) 139 | { 140 | /* drain the character */ 141 | switch (c) 142 | { 143 | case '+': 144 | case 'f': 145 | speed *= 2; 146 | break; 147 | 148 | case '-': 149 | case 's': 150 | speed /= 2; 151 | break; 152 | 153 | case '1': 154 | speed = 1.0; 155 | break; 156 | } 157 | } 158 | drift.tv_sec = drift.tv_usec = 0; 159 | } 160 | else 161 | { 162 | struct timeval stop; 163 | gettimeofday(&stop, NULL); 164 | /* Hack to accumulate the drift */ 165 | if ((diff.tv_sec == 0) && (diff.tv_usec == 0)) 166 | { 167 | diff = timeval_diff(drift, diff); // diff = 0 - drift. 168 | } 169 | drift = timeval_diff(diff, timeval_diff(start, stop)); 170 | } 171 | return speed; 172 | } 173 | 174 | 175 | double ttynowait(struct timeval prev, struct timeval cur, double speed) 176 | { 177 | /* do nothing */ 178 | (void)prev; 179 | (void)cur; 180 | (void)speed; 181 | return 0; /* Speed isn't important. */ 182 | } 183 | 184 | 185 | int ttyread(FILE *fp, Header *h, char **buf) 186 | { 187 | if (read_header(fp, h) == 0) 188 | { 189 | return 0; 190 | } 191 | 192 | *buf = malloc(h->len); 193 | if (*buf == NULL) 194 | { 195 | perror("malloc"); 196 | return 1; 197 | } 198 | 199 | if (fread_wrapper(*buf, 1, h->len, fp) == 0) 200 | { 201 | perror("fread"); 202 | } 203 | return 1; 204 | } 205 | 206 | 207 | int ttypread(FILE *fp, Header *h, char **buf) 208 | { 209 | /* 210 | * Read persistently just like tail -f. 211 | */ 212 | while (ttyread(fp, h, buf) == 0) 213 | { 214 | struct timeval w = { 0, 250000 }; 215 | select(0, NULL, NULL, NULL, &w); 216 | clearerr(fp); 217 | } 218 | return 1; 219 | } 220 | 221 | 222 | void ttywrite(char *buf, int len) 223 | { 224 | fwrite(buf, 1, len, stdout); 225 | } 226 | 227 | 228 | void ttynowrite(char *buf, int len) 229 | { 230 | /* do nothing */ 231 | (void)buf; 232 | (void)len; 233 | } 234 | 235 | 236 | void ttyplay(FILE *fp, double speed, ReadFunc read_func, WriteFunc write_func, WaitFunc wait_func) 237 | { 238 | int first_time = 1; 239 | struct timeval prev; 240 | 241 | setbuf(stdout, NULL); 242 | setbuf(fp, NULL); 243 | 244 | while (1) 245 | { 246 | char *buf; 247 | Header h; 248 | 249 | if (read_func(fp, &h, &buf) == 0) 250 | { 251 | break; 252 | } 253 | 254 | if (!first_time) 255 | { 256 | speed = wait_func(prev, h.tv, speed); 257 | } 258 | first_time = 0; 259 | 260 | write_func(buf, h.len); 261 | prev = h.tv; 262 | free(buf); 263 | } 264 | } 265 | 266 | 267 | void ttyskipall(FILE *fp) 268 | { 269 | /* 270 | * Skip all records. 271 | */ 272 | ttyplay(fp, 0, ttyread, ttynowrite, ttynowait); 273 | } 274 | 275 | 276 | void ttyplayback(FILE *fp, double speed, ReadFunc read_func, WaitFunc wait_func) 277 | { 278 | (void)read_func; 279 | ttyplay(fp, speed, ttyread, ttywrite, wait_func); 280 | } 281 | 282 | 283 | void ttypeek(FILE *fp, double speed, ReadFunc read_func, WaitFunc wait_func) 284 | { 285 | (void)read_func; 286 | (void)wait_func; 287 | ttyskipall(fp); 288 | ttyplay(fp, speed, ttypread, ttywrite, ttynowait); 289 | } 290 | 291 | 292 | void usage(void) 293 | { 294 | printf("Usage: ttyplay [OPTION] [FILE]\n"); 295 | printf(" -s SPEED Set speed to SPEED [1.0]\n"); 296 | printf(" -n No wait mode\n"); 297 | printf(" -p Peek another person's ttyrecord\n"); 298 | #ifdef HAVE_zstd 299 | printf(" -Z Enable on-the-fly zstd decompression\n"); 300 | printf("\nThe -Z flag is implied if the file suffix is \".zst\"\n"); 301 | #endif 302 | exit(EXIT_FAILURE); 303 | } 304 | 305 | 306 | /* 307 | * We do some tricks so that select(2) properly works on 308 | * STDIN_FILENO in ttywait(). 309 | */ 310 | FILE *input_from_stdin(void) 311 | { 312 | int fd = edup(STDIN_FILENO); 313 | 314 | edup2(STDOUT_FILENO, STDIN_FILENO); 315 | return efdopen(fd, "r"); 316 | } 317 | 318 | 319 | int main(int argc, char **argv) 320 | { 321 | double speed = 1.0; 322 | ReadFunc read_func = ttyread; 323 | WaitFunc wait_func = ttywait; 324 | ProcessFunc process = ttyplayback; 325 | FILE *input = NULL; 326 | struct termios old, new; 327 | 328 | set_progname(argv[0]); 329 | while (1) 330 | { 331 | #ifdef HAVE_zstd 332 | int ch = getopt(argc, argv, "hs:npZ"); 333 | #else 334 | int ch = getopt(argc, argv, "hs:np"); 335 | #endif 336 | if (ch == EOF) 337 | { 338 | break; 339 | } 340 | switch (ch) 341 | { 342 | case 's': 343 | if (optarg == NULL) 344 | { 345 | perror("-s option requires an argument"); 346 | exit(EXIT_FAILURE); 347 | } 348 | sscanf(optarg, "%lf", &speed); 349 | break; 350 | 351 | case 'n': 352 | wait_func = ttynowait; 353 | break; 354 | 355 | case 'p': 356 | process = ttypeek; 357 | break; 358 | 359 | #ifdef HAVE_zstd 360 | case 'Z': 361 | set_compress_mode(COMPRESS_ZSTD); 362 | break; 363 | #endif 364 | 365 | case 'h': 366 | default: 367 | usage(); 368 | } 369 | } 370 | 371 | if (optind < argc) 372 | { 373 | input = efopen(argv[optind], "r"); 374 | #ifdef HAVE_zstd 375 | if (strstr(argv[optind], ".zst") == argv[optind] + strlen(argv[optind]) - 4) 376 | { 377 | set_compress_mode(COMPRESS_ZSTD); 378 | } 379 | #endif 380 | } 381 | else 382 | { 383 | input = input_from_stdin(); 384 | } 385 | assert(input != NULL); 386 | 387 | tcgetattr(0, &old); /* Get current terminal state */ 388 | new = old; /* Make a copy */ 389 | new.c_lflag &= ~(ICANON | ECHO | ECHONL); /* unbuffered, no echo */ 390 | tcsetattr(0, TCSANOW, &new); /* Make it current */ 391 | 392 | process(input, speed, read_func, wait_func); 393 | tcsetattr(0, TCSANOW, &old); /* Return terminal state */ 394 | 395 | return 0; 396 | } 397 | -------------------------------------------------------------------------------- /ttyrec.c: -------------------------------------------------------------------------------- 1 | // vim: noai:sts=4:ts=4:sw=4:et 2 | 3 | /* Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | * Copyright 2001-2019 The ovh-ttyrec Authors. All rights reserved. 6 | * 7 | * This work is based on the original ttyrec, whose license text 8 | * can be found below unmodified. 9 | * 10 | * Copyright (c) 1980 Regents of the University of California. 11 | * All rights reserved. 12 | * 13 | * Redistribution and use in source and binary forms, with or without 14 | * modification, are permitted provided that the following conditions 15 | * are met: 16 | * 1. Redistributions of source code must retain the above copyright 17 | * notice, this list of conditions and the following disclaimer. 18 | * 2. Redistributions in binary form must reproduce the above copyright 19 | * notice, this list of conditions and the following disclaimer in the 20 | * documentation and/or other materials provided with the distribution. 21 | * 3. All advertising materials mentioning features or use of this software 22 | * must display the following acknowledgement: 23 | * This product includes software developed by the University of 24 | * California, Berkeley and its contributors. 25 | * 4. Neither the name of the University nor the names of its contributors 26 | * may be used to endorse or promote products derived from this software 27 | * without specific prior written permission. 28 | * 29 | * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 30 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 31 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 32 | * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 33 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 34 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 35 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 36 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 37 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 38 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 39 | * SUCH DAMAGE. 40 | */ 41 | 42 | /* 1999-02-22 Arkadiusz Miśkiewicz 43 | * - added Native Language Support 44 | */ 45 | 46 | /* 2000-12-27 Satoru Takabayashi 47 | * - modify `script' to create `ttyrec'. 48 | */ 49 | 50 | /* 2001-2010 Several OVH contributors who will recognize themselves 51 | * - lots of forgotten things 52 | */ 53 | 54 | /* 2010-2019 Stéphane Lesimple 55 | * - bugfixes (SIGWINCH handling and others) 56 | * - BSD/MacOS compatibility 57 | * - SIGUSR1 handling for ttyrec log rotation 58 | * - input timeout locking mechanism 59 | * - more things (see features in the README.md file) 60 | */ 61 | 62 | /* 63 | * script 64 | */ 65 | #include // open, waitpid 66 | #include // open 67 | #include // open, access 68 | #include // tcsetattr 69 | #include // ioctl 70 | #include // gettimeofday 71 | #ifdef __HAIKU__ 72 | # include // waitpid 73 | #else 74 | # include // waitpid 75 | #endif 76 | #include // dirname 77 | #include // printf, ... 78 | #include // read, write, usleep, ... 79 | #include // strlen, memset, ... 80 | #include // exit, free, getpt, ... 81 | #include // errno 82 | #include // pthread_create 83 | #include // sigaction 84 | #include // uname 85 | #include // localtime 86 | #include // getopt_long 87 | 88 | #include "configure.h" 89 | #include "ttyrec.h" 90 | #include "io.h" 91 | #include "compress.h" 92 | 93 | #ifdef HAVE_openpty 94 | # if defined(HAVE_openpty_pty_h) 95 | # include 96 | # elif defined(HAVE_openpty_util_h) 97 | # include 98 | # elif defined(HAVE_openpty_libutil_h) 99 | # include 100 | # endif 101 | #endif 102 | 103 | // for ZSTD_versionNumber() 104 | // and zstd_set_max_flush() 105 | #ifdef HAVE_zstd 106 | # include 107 | # include "compress_zstd.h" 108 | #endif 109 | 110 | #if defined(__linux__) 111 | # define OS_STR "Linux" 112 | #elif defined(__FreeBSD__) 113 | # define OS_STR "FreeBSD" 114 | #elif defined(__NetBSD__) 115 | # define OS_STR "NetBSD" 116 | #elif defined(__OpenBSD__) 117 | # define OS_STR "OpenBSD" 118 | #elif defined(__DragonFly__) 119 | # define OS_STR "DragonFlyBSD" 120 | #elif defined(__bsdi__) 121 | # define OS_STR "BSD" 122 | #elif defined(__SVR4) || defined(__svr4__) || defined(sun) || defined(__sun) 123 | # define SUN_OS 124 | # define OS_STR "SUN" 125 | #elif defined(macintosh) || defined(Macintosh) || (defined(__APPLE__) && defined(__MACH__)) 126 | # define OS_STR "Darwin" 127 | #elif defined(__HAIKU__) 128 | # define OS_STR "Haiku" 129 | #else 130 | # define OS_STR "UnknownOS" 131 | #endif 132 | 133 | #ifdef HAVE_isastream 134 | # include 135 | #endif 136 | 137 | #ifdef SUN_OS 138 | # include 139 | #endif 140 | 141 | #ifdef SUN_OS 142 | # define PID_T_FORMAT "%ld" 143 | #else 144 | # define PID_T_FORMAT "%d" 145 | #endif 146 | 147 | #define HAVE_inet_aton 148 | #define HAVE_scsi_h 149 | #define HAVE_kd_h 150 | 151 | #if !defined(CDEL) 152 | # if defined(_POSIX_VDISABLE) 153 | # define CDEL _POSIX_VDISABLE 154 | # elif defined(CDISABLE) 155 | # define CDEL CDISABLE 156 | # else /* not _POSIX_VISIBLE && not CDISABLE */ 157 | # define CDEL 255 158 | # endif /* not _POSIX_VISIBLE && not CDISABLE */ 159 | #endif /* !CDEL */ 160 | 161 | #define printdbg(...) if (opt_debug > 0) { fprintf(stderr, __VA_ARGS__); } 162 | #define printdbg2(...) if (opt_debug > 1) { fprintf(stderr, __VA_ARGS__); } 163 | 164 | // functions used in the main() before the forks 165 | void fixtty(void); 166 | void help(void); 167 | void set_ttyrec_file_name(char **nameptr); 168 | void getmaster(void); 169 | 170 | // functions used by the parent 171 | void doinput(void); 172 | void sigwinch_handler_parent(int signal); 173 | void *timeout_watcher(void *arg); 174 | void do_lock(void); 175 | void handle_cheatcodes(char c); 176 | 177 | // functions used by the child 178 | void dooutput(void); 179 | void sigwinch_handler_child(int signal); 180 | 181 | // functions used by the subchild 182 | void doshell(const char *, char **); 183 | void getslave(void); 184 | 185 | // sighandlers (parent and child) 186 | void swing_output_file(int signal); 187 | void unlock_session(int signal); 188 | void lock_session(int signal); 189 | void finish(int signal); 190 | void sigterm_handler(int signal); 191 | void sighup_handler(int signal); 192 | 193 | // other functions used by parent and child 194 | void done(int status); 195 | void fail(void); 196 | void print_termios_info(int fd, const char *prefix); 197 | 198 | // ansi control codes 199 | static const char *ansi_clear = "\033[2J"; 200 | static const char *ansi_home = "\033[H"; 201 | static const char *ansi_hidecursor = "\033[?25l"; 202 | static const char *ansi_showcursor = "\033[?25h"; 203 | static const char *ansi_save = "\033[?47h"; 204 | static const char *ansi_restore = "\033[?47l"; 205 | static const char *ansi_savecursor = "\0337"; 206 | static const char *ansi_restorecursor = "\0338"; 207 | 208 | static time_t last_activity = 0; 209 | static time_t locked_since = 0; 210 | static int lock_warned = 0; 211 | static int kill_warned = 0; 212 | 213 | static const char version[] = "1.1.7.1"; 214 | 215 | static FILE *fscript; 216 | static int child; 217 | static int subchild; 218 | static char *me = NULL; 219 | #ifdef HAVE_openpty 220 | static int openpty_used = 0; 221 | static int openpty_disable = 0; 222 | #endif 223 | // below: only used in notty mode 224 | int stdout_pipe[2]; // subchild will write to it, child will read from it 225 | int stderr_pipe[2]; // subchild will write to it, child will read from it 226 | // below: only used in tty mode 227 | static int master; 228 | static int slave; 229 | 230 | static char *fname = NULL; 231 | static char *dname = NULL; 232 | static char *uuid = NULL; 233 | static char *namefmt = NULL; 234 | static long timeout_lock = 0; 235 | static long timeout_kill = 0; 236 | static long warn_before_lock_seconds = 0; 237 | static long warn_before_kill_seconds = 0; 238 | 239 | static struct termios parent_stdin_termios; 240 | static struct winsize parent_stdin_winsize; 241 | static int parent_stdin_isatty = 0; 242 | #if !defined(HAVE_openpty) 243 | static char line[] = "/dev/ptyXX"; 244 | #endif 245 | 246 | static long opt_compress_level = 0; 247 | static int opt_zstd = 0; 248 | static int opt_want_tty = 1; // never=0, auto=1, force=2 249 | static int opt_append = 0; 250 | static int opt_debug = 0; 251 | static int opt_count_bytes = 0; 252 | static int opt_cheatcodes = 0; 253 | static int opt_stealth_stdout = 0; 254 | static int opt_stealth_stderr = 0; 255 | static char *opt_custom_message = NULL; 256 | 257 | static int use_tty = 1; // no=0, yes=1 258 | static int can_exit = 0; 259 | static int childexit = 254; 260 | 261 | int main(int argc, char **argv) 262 | { 263 | char *command = NULL; 264 | char **params = NULL; 265 | char *shell = NULL; 266 | int legacy = 0; 267 | int ch; 268 | 269 | shell = getenv("SHELL"); 270 | if (shell == NULL) 271 | { 272 | shell = "/bin/sh"; 273 | } 274 | 275 | while (1) 276 | { 277 | static struct option long_options[] = 278 | { 279 | /* 280 | * "long-opt-name", a, b, c 281 | * a: 1 if requires an arg, 0 otherwise 282 | * b: always 0 283 | * c: an optional char for the corresponding short-option, 0 otherwise 284 | */ 285 | { "zstd", 0, 0, 0 }, 286 | { "level", 1, 0, 'l' }, 287 | { "verbose", 0, 0, 'v' }, 288 | { "append", 0, 0, 'a' }, 289 | { "cheatcodes", 0, 0, 'c' }, 290 | { "no-cheatcodes", 0, 0, 'C' }, 291 | { "shell-cmd", 1, 0, 'e' }, 292 | { "dir", 1, 0, 'd' }, 293 | { "output", 1, 0, 'f' }, 294 | { "uuid", 1, 0, 'z' }, 295 | { "no-openpty", 0, 0, 'p' }, 296 | { "lock-timeout", 1, 0, 'l' }, 297 | { "kill-timeout", 1, 0, 'k' }, 298 | { "msg", 1, 0, 's' }, 299 | { "count-bytes", 0, 0, 'n' }, 300 | { "term", 1, 0, 'T' }, 301 | { "stealth-stdout", 0, 0, 0 }, 302 | { "stealth-stderr", 0, 0, 0 }, 303 | { "version", 0, 0, 'V' }, 304 | { "help", 0, 0, 'h' }, 305 | { "max-flush-time", 1, 0, 0 }, 306 | { "name-format", 1, 0, 'F' }, 307 | { "warn-before-lock", 1, 0, 0 }, 308 | { "warn-before-kill", 1, 0, 0 }, 309 | { "help", 0, 0, 'h' }, 310 | { "usage", 0, 0, 'h' }, 311 | { 0, 0, 0, 0 } 312 | }; 313 | int option_index = 0; 314 | ch = getopt_long(argc, argv, "ZcCupVhvanf:z:d:t:T:k:s:e:l:F:", long_options, &option_index); 315 | if (ch == -1) 316 | { 317 | break; 318 | } 319 | 320 | switch ((char)ch) 321 | { 322 | // long option without short-option counterpart 323 | case 0: 324 | if (strcmp(long_options[option_index].name, "zstd") == 0) 325 | { 326 | if (set_compress_mode(COMPRESS_ZSTD) != 0) 327 | { 328 | fprintf(stderr, "zstd support has not been enabled at compile time.\r\n"); 329 | fail(); 330 | } 331 | opt_zstd++; 332 | } 333 | else if (strcmp(long_options[option_index].name, "max-flush-time") == 0) 334 | { 335 | #ifdef HAVE_zstd 336 | errno = 0; 337 | long max_flush_seconds = strtol(optarg, NULL, 10); 338 | if ((errno != 0) || (max_flush_seconds <= 0)) 339 | { 340 | help(); 341 | fprintf(stderr, "Invalid value passed to --%s (%s), expected a strictly positive integer\r\n", long_options[option_index].name, optarg); 342 | exit(EXIT_FAILURE); 343 | } 344 | zstd_set_max_flush(max_flush_seconds); 345 | #endif 346 | } 347 | else if (strcmp(long_options[option_index].name, "warn-before-lock") == 0) 348 | { 349 | errno = 0; 350 | warn_before_lock_seconds = strtol(optarg, NULL, 10); 351 | if ((errno != 0) || (warn_before_lock_seconds <= 0)) 352 | { 353 | help(); 354 | fprintf(stderr, "Invalid value passed to --%s (%s), expected a strictly positive integer\r\n", long_options[option_index].name, optarg); 355 | exit(EXIT_FAILURE); 356 | } 357 | } 358 | else if (strcmp(long_options[option_index].name, "warn-before-kill") == 0) 359 | { 360 | errno = 0; 361 | warn_before_kill_seconds = strtol(optarg, NULL, 10); 362 | if ((errno != 0) || (warn_before_kill_seconds <= 0)) 363 | { 364 | help(); 365 | fprintf(stderr, "Invalid value passed to --%s (%s), expected a strictly positive integer\r\n", long_options[option_index].name, optarg); 366 | exit(EXIT_FAILURE); 367 | } 368 | } 369 | else if (strcmp(long_options[option_index].name, "stealth-stdout") == 0) 370 | { 371 | opt_stealth_stdout = 1; 372 | } 373 | else if (strcmp(long_options[option_index].name, "stealth-stderr") == 0) 374 | { 375 | opt_stealth_stderr = 1; 376 | } 377 | else 378 | { 379 | fprintf(stderr, "Unknown long option %s\r\n", long_options[option_index].name); 380 | fail(); 381 | } 382 | break; 383 | 384 | // on-the-fly zstd compression 385 | case 'Z': 386 | if (set_compress_mode(COMPRESS_ZSTD) == 0) 387 | { 388 | opt_zstd++; 389 | } 390 | break; 391 | 392 | // compression level of compression algorithm 393 | case 'l': 394 | errno = 0; 395 | opt_compress_level = strtol(optarg, NULL, 10); 396 | if ((errno != 0) || (opt_compress_level <= 0)) 397 | { 398 | help(); 399 | fprintf(stderr, "Invalid value passed to -%c (%s), expected a strictly positive integer\r\n", (char)ch, optarg); 400 | exit(EXIT_FAILURE); 401 | } 402 | printdbg("level %c=%ld\r\n", ch, opt_compress_level); 403 | set_compress_level(opt_compress_level); 404 | break; 405 | 406 | // debug ttyrec 407 | case 'v': 408 | opt_debug++; 409 | break; 410 | 411 | // open ttyrec file in append mode instead of write mode 412 | case 'a': 413 | opt_append++; 414 | break; 415 | 416 | // inhibit cheatcodes (force lock, force kill) 417 | case 'C': 418 | opt_cheatcodes = 0; 419 | break; 420 | 421 | // enable cheatcodes (force lock, force kill) 422 | case 'c': 423 | opt_cheatcodes = 1; 424 | break; 425 | 426 | // ignored (for compatibility with ttyrec classic) 427 | case 'u': 428 | break; 429 | 430 | // ttyrec classic way of specifying the command to launch, it uses sh -c 431 | case 'e': 432 | if (legacy == 1) 433 | { 434 | help(); 435 | fprintf(stderr, "Option -e specified more than once.\r\n"); 436 | exit(EXIT_FAILURE); 437 | } 438 | legacy = 1; 439 | params = malloc(sizeof(char *) * 4); 440 | command = shell; 441 | params[0] = strrchr(shell, '/') + 1; 442 | params[1] = "-c"; 443 | params[2] = strdup(optarg); 444 | params[3] = NULL; 445 | break; 446 | 447 | // directory to write ttyrec files to (autogenerated) 448 | case 'd': 449 | dname = strdup(optarg); 450 | break; 451 | 452 | // fullpath of ttyrec file to write to (optional, autogenerated if missing) 453 | case 'f': 454 | fname = strdup(optarg); 455 | break; 456 | 457 | // uuid, will appear in my ttyrec output file names, to keep track even after rotation (if omitted, will default to my pid) 458 | case 'z': 459 | uuid = strdup(optarg); 460 | break; 461 | 462 | // custom format 463 | case 'F': 464 | namefmt = strdup(optarg); 465 | break; 466 | 467 | // openpty_disable, don't prefer openpty() on systems that support it 468 | case 'p': 469 | #ifdef HAVE_openpty 470 | openpty_disable++; 471 | #else 472 | fprintf(stderr, "Ignored option 'p': openpty() not supported on this system.\r\n"); 473 | #endif 474 | break; 475 | 476 | // timeout before lock (t) or kill (k) 477 | case 't': 478 | case 'k': 479 | errno = 0; 480 | long timeout = strtol(optarg, NULL, 10); 481 | if ((errno != 0) || (timeout <= 0)) 482 | { 483 | help(); 484 | fprintf(stderr, "Invalid value passed to -%c (%s), expected a strictly positive integer\r\n", (char)ch, optarg); 485 | exit(EXIT_FAILURE); 486 | } 487 | printdbg("timeout %c=%ld\r\n", ch, timeout_lock); 488 | if ((char)ch == 't') 489 | { 490 | timeout_lock = timeout; 491 | } 492 | else if ((char)ch == 'k') 493 | { 494 | timeout_kill = timeout; 495 | } 496 | break; 497 | 498 | case 's': 499 | opt_custom_message = strdup(optarg); 500 | break; 501 | 502 | // if specified, will count number of bytes out and print it on termination (experimental) 503 | case 'n': 504 | opt_count_bytes++; 505 | break; 506 | 507 | case 'T': 508 | if (strncmp(optarg, "never", strlen("never")) == 0) 509 | { 510 | opt_want_tty = 0; 511 | } 512 | else if (strncmp(optarg, "auto", strlen("auto")) == 0) 513 | { 514 | opt_want_tty = 1; 515 | } 516 | else if (strncmp(optarg, "always", strlen("always")) == 0) 517 | { 518 | opt_want_tty = 2; 519 | } 520 | else 521 | { 522 | help(); 523 | fprintf(stderr, "Invalid value passed to -T (%s), expected either 'never', 'auto' or 'always'\r\n", optarg); 524 | exit(EXIT_FAILURE); 525 | } 526 | break; 527 | 528 | // version 529 | case 'V': 530 | printf("ttyrec v%s (%s)\n", version, MACHINE_STR); 531 | #ifdef DEFINES_STR 532 | printf("%s (%s)\n", DEFINES_STR, OS_STR); 533 | #endif 534 | #ifdef __VERSION__ 535 | printf("compiler version %s (%s)\n", __VERSION__, COMPILER_NAME); 536 | #endif 537 | #ifdef HAVE_zstd 538 | printf("libzstd version %u (%d.%d.%d)\n", ZSTD_versionNumber(), ZSTD_VERSION_MAJOR, ZSTD_VERSION_MINOR, ZSTD_VERSION_RELEASE); 539 | #endif 540 | exit(0); 541 | 542 | // 'h', and any other unknown option 543 | case 'h': 544 | default: 545 | help(); 546 | exit(EXIT_FAILURE); 547 | } 548 | } 549 | argc -= optind; 550 | argv += optind; 551 | 552 | printdbg("remaining non-parsed options argc=%d\r\n", argc); 553 | for (int i = 0; i < argc; i++) 554 | { 555 | printdbg("option %d: <%s>\r\n", i, argv[i]); 556 | } 557 | 558 | if ((namefmt != NULL) && ((dname != NULL) || (uuid != NULL))) 559 | { 560 | fprintf(stderr, "Option -F (--name-format) can't be used with -d (--dir) or -z (--uuid)\n"); 561 | fail(); 562 | } 563 | 564 | if (uuid == NULL) 565 | { 566 | uuid = malloc(sizeof(char) * BUFSIZ); 567 | snprintf(uuid, BUFSIZ, PID_T_FORMAT, getpid()); 568 | } 569 | 570 | if ((timeout_lock > 0) && (timeout_kill > 0) && (timeout_kill < timeout_lock)) 571 | { 572 | help(); 573 | fprintf(stderr, "specified timeout_lock (%ld) is higher than timeout_kill (%ld), this doesn't make sense\r\n", timeout_lock, timeout_kill); 574 | exit(EXIT_FAILURE); 575 | } 576 | 577 | if ((warn_before_lock_seconds > 0) && (timeout_lock == 0)) 578 | { 579 | help(); 580 | fprintf(stderr, "You specified --warn-before-lock without enabling --timeout-lock, this doesn't make sense\r\n"); 581 | exit(EXIT_FAILURE); 582 | } 583 | 584 | if (warn_before_lock_seconds > timeout_lock) 585 | { 586 | help(); 587 | fprintf(stderr, "The specified value for --warn-before-lock is higher than --timeout-lock, this doesn't make sense\r\n"); 588 | exit(EXIT_FAILURE); 589 | } 590 | 591 | if ((warn_before_kill_seconds > 0) && (timeout_kill == 0)) 592 | { 593 | help(); 594 | fprintf(stderr, "You specified --warn-before-kill without enabling --timeout-kill, this doesn't make sense\r\n"); 595 | exit(EXIT_FAILURE); 596 | } 597 | 598 | if (warn_before_kill_seconds > timeout_kill) 599 | { 600 | help(); 601 | fprintf(stderr, "The specified value for --warn-before-kill is higher than --timeout-kill, this doesn't make sense\r\n"); 602 | exit(EXIT_FAILURE); 603 | } 604 | 605 | if (legacy) 606 | { 607 | // strdup: make it free()able 608 | fname = (argv[0] == NULL ? strdup("ttyrecord") : strdup(argv[0])); 609 | } 610 | else 611 | { 612 | if (argv[0] == NULL) 613 | { 614 | command = shell; 615 | params = malloc(sizeof(char *) * 3); 616 | params[0] = strrchr(shell, '/') + 1; 617 | params[1] = "-i"; 618 | params[2] = NULL; 619 | } 620 | else 621 | { 622 | command = argv[0]; 623 | params = argv; 624 | } 625 | } 626 | 627 | printdbg("will execvp %s with the following params:\r\n", command); 628 | if (params == NULL) 629 | { 630 | printdbg("(none)\r\n"); 631 | } 632 | else 633 | { 634 | for (int index = 0; params[index] != NULL; index++) 635 | { 636 | printdbg("- '%s'\r\n", params[index]); 637 | } 638 | } 639 | 640 | // if neither dname nor fname are given, set dname to current dir 641 | if ((dname == NULL) && (fname == NULL)) 642 | { 643 | dname = strdup("."); 644 | } 645 | 646 | // if no file name given, generate it (dname is used as directory) 647 | if (fname == NULL) 648 | { 649 | set_ttyrec_file_name(&fname); 650 | } 651 | else 652 | { 653 | // otherwise, append .zst if applicable 654 | if (opt_zstd) 655 | { 656 | fname = realloc(fname, strlen(fname) + 4 + 1); 657 | if (fname == NULL) 658 | { 659 | perror("realloc"); 660 | exit(EXIT_FAILURE); 661 | } 662 | strcat(fname, ".zst"); 663 | } 664 | } 665 | 666 | // if dname == ".", it might be because we've set it 667 | if (dname == NULL) 668 | { 669 | char *tmpfname = strdup(fname); 670 | // strdup(dirname) because in done() we free() dname 671 | dname = strdup(dirname(tmpfname)); 672 | free(tmpfname); 673 | } 674 | 675 | if (dname == NULL) 676 | { 677 | fprintf(stderr, "failed to find a proper dname from fname=<%s>\r\n", fname); 678 | exit(EXIT_FAILURE); 679 | } 680 | printdbg("will use %s as dname\r\n", dname); 681 | 682 | if ((fscript = fopen(fname, opt_append ? "a" : "w")) == NULL) 683 | { 684 | perror(fname); 685 | exit(EXIT_FAILURE); 686 | } 687 | free(fname); 688 | setbuf(fscript, NULL); 689 | 690 | { 691 | struct sigaction act; 692 | memset(&act, '\0', sizeof(act)); 693 | act.sa_handler = &finish; 694 | act.sa_flags = SA_NOCLDSTOP | SA_RESTART; 695 | if (sigaction(SIGCHLD, &act, NULL)) 696 | { 697 | perror("sigaction"); 698 | exit(EXIT_FAILURE); 699 | } 700 | 701 | memset(&act, '\0', sizeof(act)); 702 | act.sa_handler = &swing_output_file; 703 | act.sa_flags = SA_RESTART; 704 | if (sigaction(SIGUSR1, &act, NULL)) 705 | { 706 | perror("sigaction"); 707 | exit(EXIT_FAILURE); 708 | } 709 | 710 | memset(&act, '\0', sizeof(act)); 711 | act.sa_handler = &unlock_session; 712 | act.sa_flags = SA_RESTART; 713 | if (sigaction(SIGUSR2, &act, NULL)) 714 | { 715 | perror("sigaction"); 716 | exit(EXIT_FAILURE); 717 | } 718 | 719 | memset(&act, '\0', sizeof(act)); 720 | act.sa_handler = &lock_session; 721 | act.sa_flags = SA_RESTART; 722 | if (sigaction(SIGURG, &act, NULL)) 723 | { 724 | perror("sigaction"); 725 | exit(EXIT_FAILURE); 726 | } 727 | 728 | memset(&act, '\0', sizeof(act)); 729 | act.sa_handler = &sigterm_handler; 730 | act.sa_flags = SA_RESTART; 731 | if (sigaction(SIGTERM, &act, NULL)) 732 | { 733 | perror("sigaction"); 734 | exit(EXIT_FAILURE); 735 | } 736 | 737 | // we can get SIGHUP if our tty is closed, fclose() properly in that case 738 | memset(&act, '\0', sizeof(act)); 739 | act.sa_handler = &sighup_handler; 740 | act.sa_flags = SA_RESTART; 741 | if (sigaction(SIGHUP, &act, NULL)) 742 | { 743 | perror("sigaction"); 744 | exit(EXIT_FAILURE); 745 | } 746 | } 747 | 748 | parent_stdin_isatty = isatty(0); 749 | switch (opt_want_tty) 750 | { 751 | case 2: 752 | use_tty = 1; 753 | break; 754 | 755 | case 0: 756 | use_tty = 0; 757 | break; 758 | 759 | default: 760 | use_tty = parent_stdin_isatty; 761 | } 762 | printdbg("parent: isatty(stdin) == %d, opt_want_tty == %d, use_tty == %d\r\n", parent_stdin_isatty, opt_want_tty, use_tty); 763 | 764 | if (use_tty) 765 | { 766 | getmaster(); 767 | print_termios_info(master, "parent master"); 768 | print_termios_info(0, "parent stdin b4 fixtty"); 769 | if (parent_stdin_isatty) 770 | { 771 | fixtty(); 772 | } 773 | print_termios_info(0, "parent stdin after fixtty"); 774 | } 775 | else 776 | { 777 | // pipe[0] is read, pipe[1] is write 778 | if (pipe(stdout_pipe)) 779 | { 780 | perror("pipe"); 781 | exit(EXIT_FAILURE); 782 | } 783 | if (pipe(stderr_pipe)) 784 | { 785 | perror("pipe"); 786 | exit(EXIT_FAILURE); 787 | } 788 | } 789 | 790 | child = fork(); 791 | if (child < 0) 792 | { 793 | perror("fork"); 794 | exit(EXIT_FAILURE); 795 | } 796 | else if (child == 0) 797 | { 798 | // we are the child 799 | printdbg("child pid is %ld\r\n", (long int)getpid()); 800 | print_termios_info(0, "child stdin"); 801 | subchild = child = fork(); 802 | if (child < 0) 803 | { 804 | perror("fork"); 805 | fail(); 806 | } 807 | else if (child) 808 | { 809 | // we are still the child (parent of subchild) 810 | me = "child"; 811 | dooutput(); 812 | } 813 | else 814 | { 815 | // we are the subchild 816 | printdbg("subchild pid is %ld\r\n", (long int)getpid()); 817 | print_termios_info(0, "subchild stdin"); 818 | me = "subchild"; 819 | doshell(command, params); 820 | } 821 | } 822 | else 823 | { 824 | // we are the parent 825 | me = "parent"; 826 | printdbg("parent pid is %ld\r\n", (long int)getpid()); 827 | sigwinch_handler_parent(SIGWINCH); 828 | if (timeout_lock || timeout_kill) 829 | { 830 | pthread_t watcher_thread; 831 | if (pthread_create(&watcher_thread, NULL, timeout_watcher, NULL) == -1) 832 | { 833 | perror("pthread"); 834 | fail(); 835 | } 836 | } 837 | doinput(); 838 | } 839 | 840 | return 0; 841 | } 842 | 843 | 844 | void handle_cheatcodes(char c) 845 | { 846 | static int lockseq = 0; 847 | static int killseq = 0; 848 | 849 | if (opt_cheatcodes != 1) 850 | { 851 | return; 852 | } 853 | 854 | // LOCK 855 | if (c == '\x0c') // ^L 856 | { 857 | if (++lockseq >= 8) 858 | { 859 | lockseq = 0; 860 | do_lock(); 861 | } 862 | } 863 | else 864 | { 865 | lockseq = 0; 866 | } 867 | 868 | // KILL 869 | if (((c == '\x0b') && ((killseq == 0) || (killseq == 4))) || // ^K 870 | ((c == '\x09') && ((killseq == 1) || (killseq == 5))) || // ^I 871 | ((c == '\x0c') && ((killseq == 2) || (killseq == 3) || (killseq == 6) || (killseq == 7)))) // ^L 872 | { 873 | killseq++; 874 | } 875 | else 876 | { 877 | killseq = 0; 878 | } 879 | if (killseq >= 8) 880 | { 881 | kill(child, SIGTERM); 882 | } 883 | } 884 | 885 | 886 | // called by parent 887 | void doinput(void) 888 | { 889 | int cc; 890 | char ibuf[BUFSIZ]; 891 | 892 | (void)fclose_wrapper(fscript); 893 | #ifdef HAVE_openpty 894 | if (openpty_used) 895 | { 896 | // openpty opens the master and the slave in a single call to getmaster(), 897 | // but in that case we don't want the slave (we won't call getslave()) 898 | (void)close(slave); 899 | } 900 | #endif 901 | 902 | struct sigaction act; 903 | memset(&act, '\0', sizeof(act)); 904 | act.sa_handler = &sigwinch_handler_parent; 905 | act.sa_flags = SA_RESTART; 906 | if (sigaction(SIGWINCH, &act, NULL)) 907 | { 908 | perror("sigaction"); 909 | fail(); 910 | } 911 | last_activity = time(NULL); 912 | lock_warned = 0; 913 | kill_warned = 0; 914 | 915 | if (use_tty) 916 | { 917 | print_termios_info(master, "parent master in doinput"); 918 | 919 | #ifdef __HAIKU__ 920 | // under Haiku, if we use BUFSIZ as read size, it reads 4 bytes per 4 bytes 921 | // instead of returning read data as soon as possible 922 | const size_t readsz = 1; 923 | #else 924 | const size_t readsz = BUFSIZ; 925 | #endif 926 | while ((cc = read(0, ibuf, readsz)) > 0) 927 | { 928 | printdbg2("[in:%d]", cc); 929 | if (!locked_since) 930 | { 931 | if (write(master, ibuf, cc) == -1) 932 | { 933 | perror("write[parent-master]"); 934 | fail(); 935 | } 936 | last_activity = time(NULL); 937 | lock_warned = 0; 938 | kill_warned = 0; 939 | if (cc == 1) 940 | { 941 | handle_cheatcodes(ibuf[0]); 942 | } 943 | } 944 | } 945 | 946 | if (opt_debug && (cc == -1)) 947 | { 948 | perror("read"); 949 | } 950 | } 951 | else 952 | { 953 | // we won't use a pseudotty, just pipes, but as we're the parent so we don't need those 954 | // also our STDIN is passed thru the subchild, so we don't need to handle it ourselves, we'll just wait for our child exit 955 | close(0); 956 | close(stdout_pipe[0]); 957 | close(stdout_pipe[1]); 958 | close(stderr_pipe[0]); 959 | close(stderr_pipe[1]); 960 | } 961 | 962 | printdbg("%s("PID_T_FORMAT "): end doinput, waiting for child\r\n", me, getpid()); 963 | waitpid(-1, &childexit, 0); 964 | 965 | printdbg("%s("PID_T_FORMAT "): end doinput, child exited with status=%d, exiting too\r\n", me, getpid(), childexit); 966 | done(childexit); 967 | } 968 | 969 | 970 | // handler of SIGCHLD 971 | void finish(int signal) 972 | { 973 | int waitedpid; 974 | int die = 0; 975 | 976 | (void)signal; 977 | printdbg("%s("PID_T_FORMAT "): got SIGCHLD, calling waitpid\r\n", me, getpid()); 978 | 979 | while ((waitedpid = waitpid(-1, &childexit, WNOHANG)) > 0) 980 | { 981 | if (waitedpid == subchild) 982 | { 983 | printdbg("%s("PID_T_FORMAT "): subchild exited with %d, setting can_exit to 1\r\n", me, getpid(), childexit); 984 | can_exit = 1; 985 | } 986 | else if (waitedpid == child) 987 | { 988 | printdbg("%s("PID_T_FORMAT "): child exited with %d, exiting too\r\n", me, getpid(), childexit); 989 | die = 1; 990 | } 991 | } 992 | 993 | if (die) 994 | { 995 | done(childexit); 996 | } 997 | } 998 | 999 | 1000 | void set_ttyrec_file_name(char **nameptr) 1001 | { 1002 | struct timeval tv; 1003 | struct tm *t = NULL; 1004 | 1005 | if (gettimeofday(&tv, NULL)) 1006 | { 1007 | perror("gettimeofday()"); 1008 | fail(); 1009 | } 1010 | 1011 | t = localtime((const time_t *)&tv.tv_sec); 1012 | if (t == NULL) 1013 | { 1014 | perror("localtime()"); 1015 | fail(); 1016 | } 1017 | 1018 | *nameptr = malloc(sizeof(char) * BUFSIZ); 1019 | if (*nameptr == NULL) 1020 | { 1021 | perror("malloc()"); 1022 | fail(); 1023 | } 1024 | 1025 | if (namefmt == NULL) 1026 | { 1027 | // - 4: length of potential ".zst" we might add below 1028 | if (snprintf(*nameptr, BUFSIZ - 4, "%s/%04u-%02u-%02u.%02u-%02u-%02u.%06lu.%s.ttyrec", dname, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, (long unsigned int)tv.tv_usec, uuid) == -1) 1029 | { 1030 | perror("snprintf()"); 1031 | free(*nameptr); 1032 | fail(); 1033 | } 1034 | } 1035 | else 1036 | { 1037 | // - 4: length of potential ".zst" we might add below 1038 | if (strftime(*nameptr, BUFSIZ - 4, namefmt, t) == 0) 1039 | { 1040 | perror("strftime()"); 1041 | free(*nameptr); 1042 | fail(); 1043 | } 1044 | (*nameptr)[BUFSIZ - 5] = '\0'; 1045 | 1046 | char usec[7]; 1047 | if (snprintf(usec, 7, "%06lu", (unsigned long)tv.tv_usec) < 0) 1048 | { 1049 | perror("snprintf()"); 1050 | free(*nameptr); 1051 | fail(); 1052 | } 1053 | 1054 | char *ptr = strstr(*nameptr, "#usec#"); 1055 | while (ptr != NULL) 1056 | { 1057 | memcpy(ptr, usec, 6); 1058 | ptr = strstr(ptr + 6, "#usec#"); 1059 | } 1060 | } 1061 | if (opt_zstd) 1062 | { 1063 | // we can strcat safely because we used BUFSIZ - 4 above 1064 | strcat(*nameptr, ".zst"); 1065 | } 1066 | } 1067 | 1068 | 1069 | void swing_output_file(int signal) 1070 | { 1071 | char *newname = NULL; 1072 | 1073 | (void)signal; 1074 | 1075 | if (subchild != 0) 1076 | { 1077 | set_ttyrec_file_name(&newname); 1078 | 1079 | fclose_wrapper(fscript); 1080 | 1081 | if ((fscript = fopen(newname, "w")) == NULL) 1082 | { 1083 | perror("fopen()"); 1084 | free(newname); 1085 | fail(); 1086 | } 1087 | free(newname); 1088 | setbuf(fscript, NULL); 1089 | } 1090 | } 1091 | 1092 | 1093 | // SIGUSR2 1094 | void unlock_session(int signal) 1095 | { 1096 | last_activity = time(NULL); 1097 | lock_warned = 0; 1098 | kill_warned = 0; 1099 | // to avoid signal storm, abort if not locked 1100 | if (!locked_since) 1101 | { 1102 | return; 1103 | } 1104 | 1105 | printdbg("%s("PID_T_FORMAT "): unlock_session()\r\n", me, getpid()); 1106 | locked_since = 0; 1107 | 1108 | // in case only the parent or the child got the SIG, 1109 | // ensure the other also gets it 1110 | kill(subchild > 0 ? getppid() : child, signal); 1111 | 1112 | if (subchild == 0) 1113 | { 1114 | // if we're the parent, force our children to redraw after unlock 1115 | usleep(1000 * 300); 1116 | 1117 | struct winsize tmpwin; 1118 | (void)ioctl(master, TIOCGWINSZ, (char *)&tmpwin); 1119 | 1120 | int pixels_per_row = tmpwin.ws_ypixel / tmpwin.ws_row; 1121 | tmpwin.ws_row++; 1122 | tmpwin.ws_ypixel += pixels_per_row; 1123 | (void)ioctl(master, TIOCSWINSZ, (char *)&tmpwin); 1124 | kill(child, SIGWINCH); 1125 | 1126 | usleep(1000 * 300); 1127 | 1128 | tmpwin.ws_row--; 1129 | tmpwin.ws_ypixel -= pixels_per_row; 1130 | (void)ioctl(master, TIOCSWINSZ, (char *)&tmpwin); 1131 | kill(child, SIGWINCH); 1132 | } 1133 | else 1134 | { 1135 | // child: restore console, make cursor visible again, restore its position 1136 | (void)fputs(ansi_restore, stdout); 1137 | (void)fputs(ansi_restorecursor, stdout); 1138 | (void)fputs(ansi_showcursor, stdout); 1139 | } 1140 | } 1141 | 1142 | 1143 | // SIGURG 1144 | void lock_session(int signal) 1145 | { 1146 | (void)signal; 1147 | // to avoid signal storm, abort if locked 1148 | if (locked_since) 1149 | { 1150 | return; 1151 | } 1152 | 1153 | printdbg("%s("PID_T_FORMAT "): lock_session()\r\n", me, getpid()); 1154 | locked_since = time(NULL); 1155 | 1156 | // in case only the parent or the child got the SIG, 1157 | // ensure the other also gets it 1158 | kill(subchild > 0 ? getppid() : child, signal); 1159 | 1160 | // if we're the parent, nothing more to do 1161 | if (subchild == 0) 1162 | { 1163 | return; 1164 | } 1165 | 1166 | const char *lock = "\033[31m" \ 1167 | "██╗ ██████╗ ██████╗██╗ ██╗███████╗██████╗ \r\n" 1168 | "██║ ██╔═══██╗██╔════╝██║ ██╔╝██╔════╝██╔══██╗\r\n" 1169 | "██║ ██║ ██║██║ █████╔╝ █████╗ ██║ ██║\r\n" 1170 | "██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ██║\r\n" 1171 | "███████╗╚██████╔╝╚██████╗██║ ██╗███████╗██████╔╝\r\n" 1172 | "╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ \033[0m\r\n" \ 1173 | " .--------. \r\n" \ 1174 | " / .------. \\ \r\n" \ 1175 | " / / \\ \\ \033[33mThe current session is \033[31mLOCKED\033[0m\r\n" \ 1176 | " | | | | \r\n" \ 1177 | " _| |________| |_ \033[33mSince %s\033[0m\r\n" \ 1178 | ".' |_| |_| '. \r\n" \ 1179 | "'._____ ____ _____.' \033[33mDue to input inactivity timeout\033[0m\r\n" \ 1180 | "| .'____'. | \r\n" \ 1181 | "'.__.'.' '.'.__.' \r\n" \ 1182 | "'.__ | | __.' \r\n" \ 1183 | "| '.'.____.'.' | \033[33m%s,\033[0m\r\n" \ 1184 | "'.____'.____.'____.' \r\n" \ 1185 | "'.________________.' \033[33m%s.\033[0m\r\n"; 1186 | 1187 | char hostname[BUFSIZ]; 1188 | struct utsname name; 1189 | 1190 | if (uname(&name)) 1191 | { 1192 | hostname[0] = '\0'; 1193 | } 1194 | else 1195 | { 1196 | strncpy(hostname, name.nodename, BUFSIZ); 1197 | hostname[BUFSIZ - 1] = '\0'; 1198 | } 1199 | 1200 | const char *salute[] = 1201 | { 1202 | "Kind regards", "Your dearest friend", 1203 | "Love", "xoxoxo", "XoXoXo", 1204 | "Respectfully yours", "Best", 1205 | "Sincerely yours", "Take care", 1206 | "Cheers", "With deepest sympathy", 1207 | }; 1208 | 1209 | #define salute_len (sizeof(salute) / sizeof(const char *)) 1210 | 1211 | // save cursor pos, save buffer, clear screen, put cursor at home position, hide cursor 1212 | (void)fputs(ansi_savecursor, stdout); 1213 | (void)fputs(ansi_save, stdout); 1214 | (void)fputs(ansi_clear, stdout); 1215 | (void)fputs(ansi_home, stdout); 1216 | (void)fputs(ansi_hidecursor, stdout); 1217 | 1218 | time_t now = time(NULL); 1219 | struct tm *tm_now = localtime(&now); 1220 | char ctime_now[BUFSIZ]; 1221 | strftime(ctime_now, BUFSIZ, "%A %Y-%m-%d %H:%M:%S", tm_now); 1222 | 1223 | srand(now); 1224 | (void)printf(lock, ctime_now, salute[rand() % salute_len], hostname); 1225 | 1226 | if (opt_custom_message != NULL) 1227 | { 1228 | (void)fputs("\r\n", stdout); 1229 | (void)puts(opt_custom_message); 1230 | } 1231 | } 1232 | 1233 | 1234 | void do_lock(void) 1235 | { 1236 | locked_since = time(NULL); 1237 | kill(child, SIGURG); 1238 | } 1239 | 1240 | 1241 | void *timeout_watcher(void *arg) 1242 | { 1243 | (void)arg; 1244 | for ( ; ;) 1245 | { 1246 | sleep(1); 1247 | time_t now = time(NULL); 1248 | if (use_tty && !locked_since) 1249 | { 1250 | // handle warn: if input is idle and we didn't already, warn 1251 | if ((warn_before_lock_seconds > 0) && (lock_warned == 0) && (now - last_activity + warn_before_lock_seconds > timeout_lock)) 1252 | { 1253 | lock_warned = 1; 1254 | fprintf(stderr, "warning: your session will be locked in %lu seconds if no input activity is detected.", warn_before_lock_seconds); 1255 | } 1256 | // handle lock: if input is idle, and warn wasn't enough, lock 1257 | if ((timeout_lock > 0) && (now - last_activity > timeout_lock)) 1258 | { 1259 | printdbg("parent: timeout_watcher: do_lock()\r\n"); 1260 | do_lock(); 1261 | } 1262 | } 1263 | // handle kill 1264 | if (timeout_kill > 0) 1265 | { 1266 | // if we're locked, check against the locked_since (never happens if !use_tty) 1267 | if (locked_since) 1268 | { 1269 | if ((warn_before_kill_seconds > 0) && (kill_warned == 0) && (now - locked_since > timeout_kill - timeout_lock - warn_before_kill_seconds)) 1270 | { 1271 | kill_warned = 1; 1272 | fprintf(stderr, "warning: your session will be killed in %lu seconds if no input activity is detected.", warn_before_kill_seconds); 1273 | } 1274 | else if (now - locked_since > timeout_kill - timeout_lock) 1275 | { 1276 | printdbg("parent: timeout_watcher: kill (locked)\r\n"); 1277 | kill(child, SIGTERM); 1278 | } 1279 | } 1280 | // handle kill cont'd: if we're not locked, check against the last_activity 1281 | else 1282 | { 1283 | if ((warn_before_kill_seconds > 0) && (kill_warned == 0) && (now - last_activity > timeout_kill - warn_before_kill_seconds)) 1284 | { 1285 | kill_warned = 1; 1286 | fprintf(stderr, "warning: your session will be killed in %lu seconds if no input activity is detected.", warn_before_kill_seconds); 1287 | } 1288 | else if (now - last_activity > timeout_kill) 1289 | { 1290 | printdbg("parent: timeout_watcher: kill (unlocked), now=%d last_activity=%d timeout_kill=%ld\r\n", (int)now, (int)last_activity, timeout_kill); 1291 | kill(child, SIGTERM); 1292 | } 1293 | } 1294 | } 1295 | } 1296 | } 1297 | 1298 | 1299 | // called by child 1300 | void dooutput(void) 1301 | { 1302 | int cc; 1303 | char obuf[BUFSIZ]; 1304 | int waitedpid; 1305 | unsigned long long bytes_out = 0; 1306 | int stdout_pipe_opened = 1; 1307 | int stderr_pipe_opened = 1; 1308 | int target_fd = 1; // stdout by default 1309 | 1310 | setbuf(stdout, NULL); 1311 | (void)close(0); // the subchild will consume it, not us 1312 | #ifdef HAVE_openpty 1313 | if (openpty_used) 1314 | { 1315 | // openpty opens the master and the slave in a single call to getmaster(), 1316 | // but in that case we don't want the slave (we won't call getslave()) 1317 | (void)close(slave); 1318 | } 1319 | #endif 1320 | struct sigaction act; 1321 | memset(&act, '\0', sizeof(act)); 1322 | act.sa_handler = &sigwinch_handler_child; 1323 | act.sa_flags = SA_RESTART; 1324 | if (sigaction(SIGWINCH, &act, NULL)) 1325 | { 1326 | perror("sigaction"); 1327 | fail(); 1328 | } 1329 | 1330 | if (!use_tty) 1331 | { 1332 | close(stdout_pipe[1]); 1333 | close(stderr_pipe[1]); 1334 | } 1335 | else 1336 | { 1337 | // ensure that our user's terminal is not in altscreen on launch, 1338 | // or (sh)he might get surprised when (s)he launches then exits programs such as vim 1339 | (void)fputs(ansi_restore, stdout); 1340 | } 1341 | 1342 | for ( ; ;) 1343 | { 1344 | Header h; 1345 | int dont_write = 0; 1346 | 1347 | // we have a tty 1348 | if (use_tty) 1349 | { 1350 | cc = read(master, obuf, BUFSIZ); 1351 | 1352 | if (cc == 0) 1353 | { 1354 | printdbg("\r\n%s(" PID_T_FORMAT "): got EOF, there's nothing left to read from\r\n", me, getpid()); 1355 | break; 1356 | } 1357 | if (cc < 0) 1358 | { 1359 | printdbg2("[out:%d,%s]", cc, strerror(errno)); 1360 | if (errno != EINTR) 1361 | { 1362 | printdbg("\r\n%s(" PID_T_FORMAT "): got fatal error when reading, there's nothing left to read from\r\n", me, getpid()); 1363 | break; 1364 | } 1365 | } 1366 | } 1367 | // we don't have a tty and use pipes 1368 | else 1369 | { 1370 | fd_set rfds; 1371 | int nfds = 0; 1372 | FD_ZERO(&rfds); 1373 | if (stdout_pipe_opened) 1374 | { 1375 | FD_SET(stdout_pipe[0], &rfds); 1376 | if (stdout_pipe[0] > nfds) 1377 | { 1378 | nfds = stdout_pipe[0]; 1379 | } 1380 | } 1381 | if (stderr_pipe_opened) 1382 | { 1383 | FD_SET(stderr_pipe[0], &rfds); 1384 | if (stderr_pipe[0] > nfds) 1385 | { 1386 | nfds = stderr_pipe[0]; 1387 | } 1388 | } 1389 | printdbg2("[select:%d:%d]", stdout_pipe_opened, stderr_pipe_opened); 1390 | int retval = select(nfds + 1, &rfds, NULL, NULL, NULL); 1391 | int current_fd = -1; 1392 | 1393 | cc = 0; 1394 | if (retval == -1) // select failed 1395 | { 1396 | if (errno != EINTR) 1397 | { 1398 | perror("select()"); 1399 | } 1400 | continue; 1401 | } 1402 | else if (retval) 1403 | { 1404 | const char *current_fd_name; 1405 | int *pipe_flag; 1406 | if (FD_ISSET(stderr_pipe[0], &rfds)) 1407 | { 1408 | current_fd = stderr_pipe[0]; 1409 | current_fd_name = "stderr"; 1410 | pipe_flag = &stderr_pipe_opened; 1411 | target_fd = 2; // stderr 1412 | if (opt_stealth_stderr) 1413 | { 1414 | dont_write = 1; // don't write buffer to ttyrec file, see below 1415 | } 1416 | } 1417 | else if (FD_ISSET(stdout_pipe[0], &rfds)) 1418 | { 1419 | current_fd = stdout_pipe[0]; 1420 | current_fd_name = "stdout"; 1421 | pipe_flag = &stdout_pipe_opened; 1422 | target_fd = 1; // stdout 1423 | if (opt_stealth_stdout) 1424 | { 1425 | dont_write = 1; // don't write buffer to ttyrec file, see below 1426 | } 1427 | } 1428 | else 1429 | { 1430 | perror("select() returned invalid fd"); 1431 | continue; 1432 | } 1433 | 1434 | cc = read(current_fd, obuf, BUFSIZ); 1435 | 1436 | // Handle EOF or error 1437 | if ((cc == 0) || ((cc < 0) && (errno != EINTR))) 1438 | { 1439 | if (cc == 0) 1440 | { 1441 | printdbg2("[%s:eof]", current_fd_name); 1442 | } 1443 | else 1444 | { 1445 | printdbg2("[%s:err=%d,%s]", current_fd_name, cc, strerror(errno)); 1446 | } 1447 | *pipe_flag = 0; 1448 | close(current_fd); 1449 | if ((stderr_pipe_opened == 0) && (stdout_pipe_opened == 0)) 1450 | { 1451 | printdbg2("[alleof]"); 1452 | break; 1453 | } 1454 | } 1455 | } 1456 | } 1457 | 1458 | // here, we have cc with the number of bytes read, from either the tty or the pipes 1459 | 1460 | printdbg2("[out:%d]", cc); 1461 | 1462 | if (!locked_since && (cc > 0)) 1463 | { 1464 | h.len = cc; 1465 | gettimeofday(&h.tv, NULL); 1466 | if (write(target_fd, obuf, cc) == -1) 1467 | { 1468 | if (errno != EINTR) 1469 | { 1470 | printdbg("write(child-stdout,len=%d): %s", cc, strerror(errno)); 1471 | if (stdout_pipe_opened) 1472 | { 1473 | close(stdout_pipe[0]); 1474 | } 1475 | if (stderr_pipe_opened) 1476 | { 1477 | close(stderr_pipe[0]); 1478 | } 1479 | break; 1480 | } 1481 | } 1482 | if (!dont_write) 1483 | { 1484 | (void)write_header(fscript, &h); 1485 | (void)fwrite_wrapper(obuf, 1, cc, fscript); 1486 | } 1487 | bytes_out += cc; 1488 | last_activity = time(NULL); 1489 | lock_warned = 0; 1490 | kill_warned = 0; 1491 | } 1492 | } 1493 | 1494 | printdbg("child: end dooutput, waiting can_exit (== %d)\r\n", can_exit); 1495 | 1496 | while (can_exit == 0) 1497 | { 1498 | waitedpid = waitpid(-1, &childexit, 0); 1499 | if (waitedpid < 0) // oops, all our children are already dead (ECHILD) 1500 | { 1501 | printdbg("child: oops, subchild is already dead!\r\n"); 1502 | can_exit = 1; 1503 | } 1504 | } 1505 | 1506 | printdbg("child: end dooutput, can_exit done, status %d, exiting\r\n", childexit); 1507 | if (opt_count_bytes) 1508 | { 1509 | fprintf(stderr, "\r\nTTY_BYTES_OUT=%llu\r\n", bytes_out); 1510 | } 1511 | done(childexit); 1512 | } 1513 | 1514 | 1515 | // called by subchild 1516 | void doshell(const char *command, char **params) 1517 | { 1518 | (void)fclose_wrapper(fscript); 1519 | if (use_tty) 1520 | { 1521 | getslave(); 1522 | print_termios_info(slave, "subchild slave"); 1523 | (void)close(master); 1524 | (void)dup2(slave, 0); 1525 | (void)dup2(slave, 1); 1526 | (void)dup2(slave, 2); 1527 | (void)close(slave); 1528 | } 1529 | else 1530 | { 1531 | (void)dup2(stdout_pipe[1], 1); 1532 | (void)dup2(stderr_pipe[1], 2); 1533 | (void)close(stdout_pipe[1]); 1534 | (void)close(stderr_pipe[1]); 1535 | } 1536 | 1537 | execvp(command, params); 1538 | 1539 | perror(command); 1540 | fail(); 1541 | } 1542 | 1543 | 1544 | void fixtty(void) 1545 | { 1546 | struct termios rtt; 1547 | 1548 | rtt = parent_stdin_termios; 1549 | #ifdef HAVE_cfmakeraw 1550 | cfmakeraw(&rtt); 1551 | rtt.c_lflag &= ~ECHO; 1552 | #else 1553 | rtt.c_iflag = 0; 1554 | rtt.c_lflag &= ~(ISIG | ICANON | XCASE | ECHO | ECHOE | ECHOK | ECHONL); 1555 | rtt.c_oflag = OPOST; 1556 | rtt.c_cc[VINTR] = CDEL; 1557 | rtt.c_cc[VQUIT] = CDEL; 1558 | rtt.c_cc[VERASE] = CDEL; 1559 | rtt.c_cc[VKILL] = CDEL; 1560 | rtt.c_cc[VEOF] = 1; 1561 | rtt.c_cc[VEOL] = 0; 1562 | #endif 1563 | if (tcsetattr(0, TCSAFLUSH, &rtt)) 1564 | { 1565 | perror("tcsetattr(0) in fixtty"); 1566 | } 1567 | } 1568 | 1569 | 1570 | void fail(void) 1571 | { 1572 | fprintf(stderr, "\r\nttyrec: aborting!\r\n"); 1573 | (void)kill(0, SIGTERM); 1574 | done(EXIT_FAILURE); 1575 | } 1576 | 1577 | 1578 | void sigterm_handler(int signal) 1579 | { 1580 | (void)signal; 1581 | if (subchild > 0) 1582 | { 1583 | // unlock_session() is also called in done(), but we need to do it first here 1584 | // or the below message won't be visible if we happen to be locked 1585 | unlock_session(SIGUSR2); 1586 | // terminate our subchild BEFORE printing the msg, to avoid an interlock case where 1587 | // our child is stuck writing to us, while we're writing to our stdout 1588 | // read by our caller, stuck writing to our child 1589 | kill(subchild, SIGTERM); 1590 | (void)puts("\r\nttyrec: ending your session, sorry (kill timeout expired, you manually typed the kill key sequence, or we got a SIGTERM).\r"); 1591 | } 1592 | done(EXIT_SUCCESS); 1593 | } 1594 | 1595 | 1596 | void sighup_handler(int signal) 1597 | { 1598 | (void)signal; 1599 | if (subchild > 0) 1600 | { 1601 | kill(subchild, SIGTERM); 1602 | } 1603 | done(EXIT_SUCCESS); 1604 | } 1605 | 1606 | 1607 | void done(int status) 1608 | { 1609 | // Sometimes (happens once every ~1 million executions in some environments), we might get a SIGHUP 1610 | // while we're calling exit() from this function. As the SIGHUP handler ends up calling this function 1611 | // again, we end up doing double-frees and calling exit() twice, which gets us a segfault. Hereby, ensure 1612 | // that once we've entered this function once, we'll never re-enter it through a sighandler. 1613 | static pthread_mutex_t entered_done = PTHREAD_MUTEX_INITIALIZER; 1614 | 1615 | if (pthread_mutex_trylock(&entered_done) != 0) 1616 | { 1617 | return; 1618 | } 1619 | 1620 | if (subchild) 1621 | { 1622 | printdbg("child: done, cleaning up and exiting with %d (child=%d subchild=%d)\r\n", WEXITSTATUS(status), child, subchild); 1623 | // if we were locked, unlock before exiting to avoid leaving the real terminal of our user stuck in altscreen 1624 | unlock_session(SIGUSR2); 1625 | (void)fclose_wrapper(fscript); 1626 | (void)close(master); 1627 | } 1628 | else 1629 | { 1630 | printdbg("parent: done, cleaning up and exiting with %d (child=%d subchild=%d)\r\n", WEXITSTATUS(status), child, subchild); 1631 | if (use_tty && parent_stdin_isatty) 1632 | { 1633 | // don't check for result because if it fails, we can't do anything interesting, 1634 | // not even printing an error (and actually; perror() stucks sometimes if we use it here) 1635 | tcsetattr(0, TCSAFLUSH, &parent_stdin_termios); 1636 | } 1637 | } 1638 | 1639 | free(dname); 1640 | free(uuid); 1641 | exit(WEXITSTATUS(status)); 1642 | } 1643 | 1644 | 1645 | void getmaster(void) 1646 | { 1647 | if (parent_stdin_isatty) 1648 | { 1649 | if (tcgetattr(0, &parent_stdin_termios)) 1650 | { 1651 | perror("tcgetattr(0, parent_stdin_termios)"); 1652 | } 1653 | if (ioctl(0, TIOCGWINSZ, (char *)&parent_stdin_winsize)) 1654 | { 1655 | perror("ioctl(0, TIOCGWINSZ)"); 1656 | } 1657 | } 1658 | 1659 | #ifdef HAVE_openpty 1660 | if (openpty_disable) 1661 | { 1662 | #endif 1663 | 1664 | #ifdef HAVE_grantpt 1665 | // with getpt, posix_openpt and ptmx, we need grantpt in getslave() 1666 | # if defined(HAVE_getpt) 1667 | if ((master = getpt()) < 0) 1668 | { 1669 | perror("getpt()"); 1670 | fail(); 1671 | } 1672 | return; 1673 | # elif defined(HAVE_posix_openpt) 1674 | if ((master = posix_openpt(O_RDWR | O_NOCTTY)) < 0) 1675 | { 1676 | perror("posix_openpt()"); 1677 | fail(); 1678 | } 1679 | return; 1680 | # else 1681 | if (access("/dev/ptmx", F_OK) == 0) 1682 | { 1683 | if ((master = open("/dev/ptmx", O_RDWR | O_NOCTTY)) < 0) 1684 | { 1685 | perror("open(\"/dev/ptmx\", O_RDWR)"); 1686 | fail(); 1687 | } 1688 | return; 1689 | } 1690 | # endif 1691 | #endif /* !HAVE_grantpt */ 1692 | 1693 | #ifdef HAVE_openpty 1694 | } 1695 | #endif 1696 | 1697 | //#if !defined(HAVE_grantpt) || ( !defined(HAVE_getpt) && !defined(HAVE_posix_openpt) ) 1698 | # ifdef HAVE_openpty 1699 | // BUG HERE: the getpt(), posix_openpt() and /dev/ptmx ways work correctly under Linux, 1700 | // where openpty() doesn't in the case of: ssh -T user@bastion -- -t user@remote -- 'pwd' = 0) 1791 | { 1792 | char *tp = &line[strlen("/dev/")]; 1793 | int ok; 1794 | 1795 | /* verify slave side is usable */ 1796 | *tp = 't'; 1797 | ok = access(line, R_OK | W_OK) == 0; 1798 | *tp = 'p'; 1799 | if (ok) 1800 | { 1801 | return; 1802 | } 1803 | (void)close(master); 1804 | } 1805 | } 1806 | } 1807 | if (tries == 0) 1808 | { 1809 | fprintf(stderr, "Found no way to allocate a pseudo-tty on your system!\r\n"); 1810 | } 1811 | else 1812 | { 1813 | fprintf(stderr, "Out of pty's (tried %u pty's)\r\n", tries); 1814 | } 1815 | fail(); 1816 | # endif 1817 | //#endif 1818 | } 1819 | 1820 | 1821 | void getslave(void) 1822 | { 1823 | #ifdef HAVE_openpty 1824 | printdbg("openpty_used == %d\r\n", openpty_used); 1825 | if (openpty_used) 1826 | { 1827 | if (setsid() < 0) 1828 | { 1829 | perror("setsid"); 1830 | fail(); 1831 | } 1832 | if (ioctl(slave, TIOCSCTTY, 0) < 0) 1833 | { 1834 | #ifndef SUN_OS 1835 | perror("ioctl"); 1836 | fail(); 1837 | #endif 1838 | } 1839 | return; 1840 | } 1841 | #endif 1842 | 1843 | #if defined(HAVE_grantpt) 1844 | if (setsid() < 0) 1845 | { 1846 | perror("setsid"); 1847 | fail(); 1848 | } 1849 | if (grantpt(master) != 0) 1850 | { 1851 | perror("grantpt"); 1852 | fail(); 1853 | } 1854 | if (unlockpt(master) != 0) 1855 | { 1856 | perror("unlockpt"); 1857 | fail(); 1858 | } 1859 | const char *slavename = ptsname(master); 1860 | printdbg("ptsname(master) is %s\r\n", slavename); 1861 | if ((slave = open(slavename, O_RDWR)) < 0) 1862 | { 1863 | perror("slave = open(fd, O_RDWR)"); 1864 | fail(); 1865 | } 1866 | if (ioctl(slave, TIOCSCTTY, 0) < 0) 1867 | { 1868 | #ifndef SUN_OS 1869 | perror("ioctl"); 1870 | fail(); 1871 | #endif 1872 | } 1873 | # ifdef HAVE_isastream 1874 | if (isastream(slave)) 1875 | { 1876 | if (ioctl(slave, I_PUSH, "ptem") < 0) 1877 | { 1878 | perror("ioctl(fd, I_PUSH, ptem)"); 1879 | fail(); 1880 | } 1881 | if (ioctl(slave, I_PUSH, "ldterm") < 0) 1882 | { 1883 | perror("ioctl(fd, I_PUSH, ldterm)"); 1884 | fail(); 1885 | } 1886 | # ifndef _HPUX_SOURCE 1887 | if (ioctl(slave, I_PUSH, "ttcompat") < 0) 1888 | { 1889 | perror("ioctl(fd, I_PUSH, ttcompat)"); 1890 | fail(); 1891 | } 1892 | # endif 1893 | if (parent_stdin_isatty) 1894 | { 1895 | if (ioctl(0, TIOCGWINSZ, (char *)&parent_stdin_winsize)) 1896 | { 1897 | perror("ioctl(0, TIOCGWINSZ)"); 1898 | } 1899 | } 1900 | } 1901 | # endif /* !HAVE_isastream */ 1902 | #elif !defined(HAVE_openpty) /* !HAVE_grantpt */ 1903 | line[strlen("/dev/")] = 't'; 1904 | slave = open(line, O_RDWR); 1905 | if (slave < 0) 1906 | { 1907 | perror(line); 1908 | fail(); 1909 | } 1910 | if (parent_stdin_isatty) 1911 | { 1912 | if (tcsetattr(slave, TCSAFLUSH, &parent_stdin_termios)) 1913 | { 1914 | perror("tcsetattr(slave, TCSAFLUSH, parent_stdin_termios)"); 1915 | } 1916 | if (ioctl(slave, TIOCSWINSZ, (char *)&parent_stdin_winsize)) 1917 | { 1918 | perror("ioctl(slave, TIOCSWINSZ, parent_stdin_winsize)"); 1919 | } 1920 | } 1921 | #endif /* HAVE_grantpt */ 1922 | } 1923 | 1924 | 1925 | void sigwinch_handler_parent(int signal) 1926 | { 1927 | (void)signal; 1928 | if (parent_stdin_isatty) 1929 | { 1930 | (void)ioctl(0, TIOCGWINSZ, (char *)&parent_stdin_winsize); 1931 | (void)ioctl(master, TIOCSWINSZ, (char *)&parent_stdin_winsize); 1932 | } 1933 | 1934 | kill(child, SIGWINCH); 1935 | } 1936 | 1937 | 1938 | void sigwinch_handler_child(int signal) 1939 | { 1940 | (void)signal; 1941 | kill(child, SIGWINCH); 1942 | } 1943 | 1944 | 1945 | void print_termios_info(int fd, const char *prefix) 1946 | { 1947 | struct termios t; 1948 | 1949 | if (opt_debug) 1950 | { 1951 | memset(&t, '\0', sizeof(t)); 1952 | if (tcgetattr(fd, &t)) 1953 | { 1954 | fprintf(stderr, "%25s: %s\r\n", prefix, strerror(errno)); 1955 | } 1956 | else 1957 | { 1958 | char dbgline[BUFSIZ]; 1959 | dbgline[0] = '\0'; 1960 | snprintf(dbgline, BUFSIZ, "%25s: i=%05lo o=%03lo c=%05lo l=%06lo, i: ", prefix, (unsigned long)t.c_iflag, (unsigned long)t.c_oflag, (unsigned long)t.c_cflag, (unsigned long)t.c_lflag); 1961 | #define IFLAG(f) \ 1962 | if (t.c_iflag & f) { strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); \ 1963 | } 1964 | #define OFLAG(f) if (t.c_oflag & f) { strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); } 1965 | #define CFLAG(f) if (t.c_cflag & f) { strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); } 1966 | #define LFLAG(f) if (t.c_lflag & f) { strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); } 1967 | 1968 | #define CSWITCH(f) \ 1969 | case f: \ 1970 | strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); break; 1971 | IFLAG(IGNBRK); 1972 | IFLAG(BRKINT); 1973 | IFLAG(IGNPAR); 1974 | IFLAG(PARMRK); 1975 | IFLAG(INPCK); 1976 | IFLAG(ISTRIP); 1977 | IFLAG(INLCR); 1978 | IFLAG(IGNCR); 1979 | IFLAG(ICRNL); 1980 | #ifdef IUCLC /* undef under NetBSD */ 1981 | IFLAG(IUCLC); 1982 | #endif 1983 | IFLAG(IXON); 1984 | #ifdef IXANY /* undef under NetBSD */ 1985 | IFLAG(IXANY); 1986 | #endif 1987 | IFLAG(IXOFF); 1988 | #ifdef IMAXBEL /* undef at least under Haiku */ 1989 | IFLAG(IMAXBEL); 1990 | #endif 1991 | #ifdef IUTF8 1992 | IFLAG(IUTF8); 1993 | #endif 1994 | strncat(dbgline + strlen(dbgline), "o: ", BUFSIZ - strlen(dbgline)); 1995 | OFLAG(OPOST); 1996 | #ifdef OLCUC /* undef under NetBSD */ 1997 | OFLAG(OLCUC); 1998 | #endif 1999 | OFLAG(ONLCR); 2000 | OFLAG(OCRNL); 2001 | OFLAG(ONLRET); 2002 | #ifdef OFILL /* undef under NetBSD */ 2003 | OFLAG(OFILL); 2004 | #endif 2005 | #ifdef OFDEL /* undef under NetBSD */ 2006 | OFLAG(OFDEL); 2007 | #endif 2008 | strncat(dbgline + strlen(dbgline), "c: ", BUFSIZ - strlen(dbgline)); 2009 | switch (t.c_cflag & B38400) 2010 | { 2011 | CSWITCH(B38400); 2012 | CSWITCH(B19200); 2013 | CSWITCH(B9600); 2014 | CSWITCH(B4800); 2015 | CSWITCH(B2400); 2016 | CSWITCH(B1200); 2017 | CSWITCH(B600); 2018 | CSWITCH(B300); 2019 | CSWITCH(B150); 2020 | CSWITCH(B75); 2021 | CSWITCH(B50); 2022 | CSWITCH(B0); 2023 | } 2024 | switch (t.c_cflag & CSIZE) 2025 | { 2026 | CSWITCH(CS8); 2027 | CSWITCH(CS7); 2028 | #if (CS6 != CS5) && (CS6 != CS7) && (CS6 != CS8) /* Haiku defines CS4 and CS5 to 0x00 */ 2029 | CSWITCH(CS6); 2030 | #endif 2031 | #if (CS5 != CS6) && (CS5 != CS7) && (CS5 != CS8) 2032 | CSWITCH(CS5); 2033 | #endif 2034 | } 2035 | CFLAG(CSTOPB); 2036 | CFLAG(CREAD); 2037 | CFLAG(PARENB); 2038 | CFLAG(PARODD); 2039 | CFLAG(CLOCAL); 2040 | strncat(dbgline + strlen(dbgline), "l: ", BUFSIZ - strlen(dbgline)); 2041 | LFLAG(ISIG); 2042 | LFLAG(ICANON); 2043 | #ifdef XCASE /* undef under NetBSD */ 2044 | LFLAG(XCASE); 2045 | #endif 2046 | LFLAG(ECHO); 2047 | LFLAG(ECHOE); 2048 | LFLAG(ECHOK); 2049 | LFLAG(ECHONL); 2050 | LFLAG(NOFLSH); 2051 | LFLAG(TOSTOP); 2052 | #ifdef ECHOCTL /* undef under NetBSD */ 2053 | LFLAG(ECHOCTL); 2054 | #endif 2055 | #ifdef ECHOPRT /* undef under NetBSD */ 2056 | LFLAG(ECHOPRT); 2057 | #endif 2058 | #ifdef ECHOKE /* undef under NetBSD */ 2059 | LFLAG(ECHOKE); 2060 | #endif 2061 | #ifdef FLUSH0 /* undef under NetBSD */ 2062 | LFLAG(FLUSHO); 2063 | #endif 2064 | #ifdef PENDIN /* undef under NetBSD */ 2065 | LFLAG(PENDIN); 2066 | #endif 2067 | LFLAG(IEXTEN); 2068 | #undef IFLAG 2069 | #undef OFLAG 2070 | #undef CFLAG 2071 | #undef LFLAG 2072 | #undef CSWITCH 2073 | strncat(dbgline + strlen(dbgline), "cc: ", BUFSIZ - strlen(dbgline)); 2074 | for (cc_t i = 0; i < NCCS; i++) 2075 | { 2076 | snprintf(dbgline + strlen(dbgline), BUFSIZ - strlen(dbgline), "%02x/", t.c_cc[i]); 2077 | } 2078 | fprintf(stderr, "%s\r\n", dbgline); 2079 | } 2080 | } 2081 | } 2082 | 2083 | 2084 | void help(void) 2085 | { 2086 | fprintf(stderr, \ 2087 | "Usage: ttyrec [options] -- [command options]\n" \ 2088 | "\n" \ 2089 | "Usage (legacy compatibility mode): ttyrec -e [options] [ttyrec file name]\n" \ 2090 | "\n" \ 2091 | "Options:\n" \ 2092 | " -z, --uuid UUID specify an UUID (can be any string) that will appear in the ttyrec output file names,\n" \ 2093 | " and kept with SIGUSR1 rotations (default: own PID)\n" \ 2094 | " -f, --output FILE full path of the first ttyrec file to write to (autogenerated if omitted)\n" \ 2095 | " -d, --dir FOLDER folder where to write the ttyrec files (taken from -f if omitted,\n" \ 2096 | " defaulting to working directory if both -f and -d are omitted)\n" \ 2097 | " -F, --name-format FMT custom strftime-compatible format string to qualify the full path of the output files,\n" \ 2098 | " including the SIGUSR1 rotated ones\n" \ 2099 | " -a, --append open the ttyrec output file in append mode instead of write-clobber mode\n"); 2100 | #ifdef HAVE_zstd 2101 | fprintf(stderr, \ 2102 | " -Z enable on-the-fly compression if available, silently fallback to no compression if not\n" \ 2103 | " --zstd force on-the-fly compression of output file using zstd,\n" \ 2104 | " the resulting file will have a '.ttyrec.zst' extension\n" \ 2105 | " --max-flush-time S specify the maximum number of seconds after which we'll force zstd to flush its output buffers\n" \ 2106 | " to ensure that even somewhat quiet sessions gets regularly written out to disk, default is %d\n" \ 2107 | " -l, --level LEVEL set compression level, must be between 1 and 19 for zstd, default is 3\n" \ 2108 | , ZSTD_MAX_FLUSH_SECONDS_DEFAULT); 2109 | #endif 2110 | fprintf(stderr, \ 2111 | " -n, --count-bytes count the number of bytes out and print it on termination (experimental)\n" \ 2112 | " -t, --lock-timeout S lock session on input timeout after S seconds\n" \ 2113 | " --warn-before-lock S warn S seconds before locking (see --lock-timeout)\n" \ 2114 | " -k, --kill-timeout S kill session on input timeout after S seconds\n" \ 2115 | " --warn-before-kill S warn S seconds before killing (see --kill-timeout)\n" \ 2116 | " -C, --no-cheatcodes disable cheat-codes (see below), this is the default\n" \ 2117 | " -c, --cheatcodes enable cheat-codes (see below)\n" \ 2118 | " -p, --no-openpty don't use openpty() even when it's available\n" \ 2119 | " -T, --term MODE MODE can be either 'never' (never allocate a pseudotty, even if stdin is a tty, and use pipes to\n" \ 2120 | " handle stdout/stderr instead), 'always' (always allocate a pseudotty, even if stdin is not a tty)\n" \ 2121 | " or 'auto' (default, allocate a pseudotty if stdin is a tty, uses pipes otherwise)\n" \ 2122 | " --stealth-stdout when no pseudotty is allocated, don't record stdout\n" \ 2123 | " --stealth-stderr when no pseudotty is allocated, don't record stderr\n" \ 2124 | " -v, --verbose verbose (debug) mode, use twice for more verbosity\n" \ 2125 | " -V, --version show version information\n" \ 2126 | " -e, --shell-cmd CMD enables legacy compatibility mode and specifies the command to be run under the user's $SHELL -c\n" \ 2127 | "\n" \ 2128 | "Examples:\n" \ 2129 | " Run some shell commands in legacy mode: ttyrec -e 'for i in a b c; do echo $i; done' outfile.ttyrec\n" \ 2130 | " Run some shell commands in normal mode: ttyrec -f /tmp/normal.ttyrec -- sh -c 'for i in a b c; do echo $i; done'\n" \ 2131 | " Connect to a remote machine interactively: ttyrec -t 60 -k 300 -- ssh remoteserver\n" \ 2132 | " Execute a local script remotely with the default remote shell: ttyrec -- ssh remoteserver < script.sh\n" \ 2133 | " Record a screen session: ttyrec screen\n" \ 2134 | "\n" \ 2135 | "Handled signals:\n" \ 2136 | " SIGUSR1 close current ttyrec file and reopen a new one (log rotation)\n" \ 2137 | " SIGURG lock session\n" \ 2138 | " SIGUSR2 unlock session\n" \ 2139 | "\n" \ 2140 | "Cheat-codes (magic keystrokes combinations):\n" \ 2141 | " ^L^L^L^L^L^L^L^L lock your session (that's 8 CTRL+L's)\n" \ 2142 | " ^K^I^L^L^K^I^L^L kill your session\n" \ 2143 | "\n" \ 2144 | "Remark about session lock and session kill:\n" \ 2145 | " If we don't have a tty, we can't lock, so -t will be ignored,\n" \ 2146 | " whereas -k will be applied without warning, as there's no tty to output a warning to.\n" \ 2147 | ); 2148 | } 2149 | -------------------------------------------------------------------------------- /ttyrec.h: -------------------------------------------------------------------------------- 1 | #ifndef __TTYREC_H__ 2 | #define __TTYREC_H__ 3 | 4 | #include 5 | 6 | typedef struct header 7 | { 8 | struct timeval tv; 9 | int len; 10 | } Header; 11 | 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /ttytime.c: -------------------------------------------------------------------------------- 1 | /* Use of this source code is governed by a BSD-style 2 | * license that can be found in the LICENSE file. 3 | * Copyright 2001-2019 The ovh-ttyrec Authors. All rights reserved. 4 | * 5 | * This work is based on the original ttyrec, whose license text 6 | * can be found below unmodified. 7 | * 8 | * Copyright (c) 1980 Regents of the University of California. 9 | * All rights reserved. 10 | * 11 | * Redistribution and use in source and binary forms, with or without 12 | * modification, are permitted provided that the following conditions 13 | * are met: 14 | * 1. Redistributions of source code must retain the above copyright 15 | * notice, this list of conditions and the following disclaimer. 16 | * 2. Redistributions in binary form must reproduce the above copyright 17 | * notice, this list of conditions and the following disclaimer in the 18 | * documentation and/or other materials provided with the distribution. 19 | * 3. All advertising materials mentioning features or use of this software 20 | * must display the following acknowledgement: 21 | * This product includes software developed by the University of 22 | * California, Berkeley and its contributors. 23 | * 4. Neither the name of the University nor the names of its contributors 24 | * may be used to endorse or promote products derived from this software 25 | * without specific prior written permission. 26 | * 27 | * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 28 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 30 | * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 31 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 32 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 33 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 34 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 35 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 36 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 37 | * SUCH DAMAGE. 38 | */ 39 | 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | #include "ttyrec.h" 46 | #include "io.h" 47 | 48 | int calc_time(const char *filename); 49 | 50 | int calc_time(const char *filename) 51 | { 52 | Header start, end; 53 | FILE *fp = efopen(filename, "r"); 54 | 55 | read_header(fp, &start); 56 | end.tv.tv_sec = start.tv.tv_sec; // to avoid can-be-uninit warning 57 | fseek(fp, start.len, SEEK_CUR); 58 | while (1) 59 | { 60 | Header h; 61 | if (read_header(fp, &h) == 0) 62 | { 63 | break; 64 | } 65 | end = h; 66 | fseek(fp, h.len, SEEK_CUR); 67 | } 68 | return end.tv.tv_sec - start.tv.tv_sec; 69 | } 70 | 71 | 72 | int main(int argc, char **argv) 73 | { 74 | int i; 75 | 76 | set_progname(argv[0]); 77 | for (i = 1; i < argc; i++) 78 | { 79 | char *filename = argv[i]; 80 | printf("%7d %s\n", calc_time(filename), filename); 81 | } 82 | return 0; 83 | } 84 | -------------------------------------------------------------------------------- /update-spec-file-changelog-from-debian.pl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/perl 2 | use strict; 3 | use warnings; 4 | use File::Copy; 5 | 6 | my $ver; 7 | my $latestver; 8 | my @changes; 9 | my $changelog_fd; 10 | my $oldspec_fd; 11 | my $newspec_fd; 12 | my $first = 1; 13 | 14 | open($oldspec_fd, '<', 'ovh-ttyrec.spec') or die $!; 15 | # first, copy all the old spec contents up to %changelog 16 | my @contents; 17 | while (<$oldspec_fd>) { 18 | push @contents, $_; 19 | if (/^%changelog/) { last; } 20 | } 21 | close($oldspec_fd); 22 | 23 | open($changelog_fd, '<', 'debian/changelog') or die $!; 24 | while (<$changelog_fd>) { 25 | if (m{^ovh-ttyrec \(([^)]+)\)}) { 26 | $ver = $1; 27 | $latestver = $ver if not defined $latestver; 28 | } 29 | elsif (m{^ -- (.+)\s+(...), (..) (...) (....)}) { 30 | my ($author,$wday,$day,$month,$year) = ($1,$2,$3,$4,$5); 31 | # from: Thu, 15 Sep 2020 10:59:22 +0200 32 | # to: Wed Nov 04 2020 33 | my $date = "$wday $month $day $year"; 34 | if (@changes) { s/^\*/-/ for @changes; } 35 | push @contents, "\n" if $first == 0; 36 | $first = 0; 37 | push @contents, "* $date $author $ver\n"; 38 | push @contents, join("\n", @changes); 39 | push @contents, "\n"; 40 | undef $ver; 41 | @changes = (); 42 | } 43 | elsif (m{^ (\* .+)}) { 44 | push @changes, $1; 45 | } 46 | elsif (m{^ (.+)}) { 47 | push @changes, $1; 48 | } 49 | } 50 | close($changelog_fd); 51 | 52 | s/^Version: .*/Version: $latestver/ for @contents; 53 | open($newspec_fd, '>', 'ovh-ttyrec.spec.tmp') or die $!; 54 | print $newspec_fd join("", @contents); 55 | close($newspec_fd); 56 | 57 | move("ovh-ttyrec.spec.tmp", "ovh-ttyrec.spec"); 58 | --------------------------------------------------------------------------------