Merge tag 'v22.10.01'

RaSCSI version 22.10.01
This commit is contained in:
Tony Kuker 2022-10-23 14:31:48 -05:00
commit 39d36cf78a
234 changed files with 20107 additions and 17436 deletions

32
.dockerignore Normal file
View File

@ -0,0 +1,32 @@
# Exclude all by default
/*
# Paths to include
!/docker/rascsi/rascsi_wrapper.sh
!/docker/rascsi/cfilesystem.patch
!/docker/rascsi-web/start.sh
!/doc
!/python
!/src
!/test
!/easyinstall.sh
!/LICENCE
!/lido-driver.img
!/README.md
# From .gitignore
venv
*.pyc
core
**/.idea
.DS_Store
*.swp
__pycache__
current
rascsi_interface_pb2.py
src/raspberrypi/hfdisk/
*~
messages.pot
messages.mo
s.sh
*-backups

54
.github/workflows/build_code.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Build C++ Packages
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Add armhf as architecture
run: sudo dpkg --add-architecture armhf
- name: Reconfigure apt for arch amd64
run: sudo sed -i "s/deb /deb [arch=amd64] /g" /etc/apt/sources.list
- name: Add armhf repos (jammy)
run: sudo bash -c "echo \"deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ jammy main multiverse restricted universe\" >> /etc/apt/sources.list"
- name: Add armhf repos (jammy-updates)
run: sudo bash -c "echo \"deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main multiverse restricted universe\" >> /etc/apt/sources.list"
- name: Update apt
run: sudo apt update
- name: Install cross compile toolchain
run: sudo apt-get --yes install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libspdlog-dev
- name: Install libraries
run: sudo apt-get --yes install libspdlog-dev:armhf libpcap-dev:armhf libevdev2:armhf libev-dev:armhf protobuf-compiler libprotobuf-dev:armhf
- name: make standard
run: make all -j6 CONNECT_TYPE=STANDARD CROSS_COMPILE=arm-linux-gnueabihf-
working-directory: ./src/raspberrypi
- name: make fullspec
run: make all -j6 CONNECT_TYPE=FULLSPEC CROSS_COMPILE=arm-linux-gnueabihf-
working-directory: ./src/raspberrypi
# We need to tar the binary outputs to retain the executable
# file permission. Currently, actions/upload-artifact only
# supports .ZIP files.
# This is workaround for https://github.com/actions/upload-artifact/issues/38
- name: tar binary outputs
run: tar -czvf rascsi.tar.gz ./bin
working-directory: ./src/raspberrypi
- name: upload artifacts
uses: actions/upload-artifact@v2
with:
name: arm-binaries
path: ./src/raspberrypi/rascsi.tar.gz

View File

@ -1,56 +0,0 @@
name: C/C++ CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Install cross compile toolchain
run: sudo apt-get install gcc-8-arm-linux-gnueabihf g++-8-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libspdlog-dev
- uses: actions/checkout@v2
- name: dump arm gcc version
run: arm-linux-gnueabihf-gcc -v
working-directory: ./src/raspberrypi
- name: dump native gcc version
run: gcc -v
working-directory: ./src/raspberrypi
- name: make standard
run: make all DEBUG=1 CONNECT_TYPE=STANDARD
working-directory: ./src/raspberrypi
- name: make fullspec
run: make all DEBUG=1 CONNECT_TYPE=FULLSPEC
working-directory: ./src/raspberrypi
# We need to tar the binary outputs to retain the executable
# file permission. Currently, actions/upload-artifact only
# supports .ZIP files.
# This is workaround for https://github.com/actions/upload-artifact/issues/38
- name: tar binary outputs
run: tar -czvf rascsi.tar.gz ./bin
working-directory: ./src/raspberrypi
- name: upload artifacts
uses: actions/upload-artifact@v2
with:
name: arm-binaries
path: ./src/raspberrypi/rascsi.tar.gz
# buildroot-image:
# runs-on: ubuntu-latest
# steps:
# - name: git-fetch buildroot
# run: git clone git://git.busybox.net/buildroot
# - name: make defconfig
# run: make raspberrypi4_defconfig
# working-directory: ./buildroot
# - name: make
# run: make all
# working-directory: ./buildroot

60
.github/workflows/run_tests.yml vendored Normal file
View File

@ -0,0 +1,60 @@
name: Run automated unit tests
on: [push, pull_request]
jobs:
Tests_and_SonarCloud_Analysis:
runs-on: ubuntu-22.04
env:
MAKEFLAGS: -j2 # Number of available processors
SOURCES: src/raspberrypi
SONAR_SCANNER_VERSION: 4.7.0.2747
SONAR_SERVER_URL: "https://sonarcloud.io"
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Install dependencies
run: sudo apt-get install libspdlog-dev libpcap-dev libevdev2 libev-dev protobuf-compiler libgtest-dev libgmock-dev
- name: Run unit tests and save log
run: $SOURCES/bin/fullspec/rascsi_test | tee test_log.txt
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: test log
path: test_log.txt
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
- name: Download and set up sonar-scanner
env:
SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip
run: |
mkdir -p $HOME/.sonar
curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }}
unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/
echo "$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin" >> $GITHUB_PATH
- name: Download and set up build-wrapper
env:
BUILD_WRAPPER_DOWNLOAD_URL: ${{ env.SONAR_SERVER_URL }}/static/cpp/build-wrapper-linux-x86.zip
run: |
curl -sSLo $HOME/.sonar/build-wrapper-linux-x86.zip ${{ env.BUILD_WRAPPER_DOWNLOAD_URL }}
unzip -o $HOME/.sonar/build-wrapper-linux-x86.zip -d $HOME/.sonar/
echo "$HOME/.sonar/build-wrapper-linux-x86" >> $GITHUB_PATH
- name: Run build-wrapper
run: |
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make -C $SOURCES coverage
- name: Run gcov
run: (cd $SOURCES ; gcov --preserve-paths $(find -name '*.gcno'))
- name: Run sonar-scanner
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
cd $SOURCES | sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" --define sonar.projectKey=akuker_RASCSI --define sonar.organization=rascsi --define sonar.cfamily.gcov.reportsPath=. --define sonar.cfamily.cache.enabled=false --define sonar.coverage.exclusions="**/test/**" --define sonar.cpd.exclusions="**test/**" --define sonar.python.version=3

8
.gitignore vendored
View File

@ -11,10 +11,16 @@ src/raspberrypi/hfdisk/
*~
messages.pot
messages.mo
report.xml
docker/docker-compose.override.yml
/docker/volumes/images/*
!/docker/volumes/images/.gitkeep
/docker/volumes/config/*
!/docker/volumes/config/.gitkeep
# temporary user files
s.sh
# temporary kicad files
*-backups

View File

@ -17,15 +17,18 @@ When you are ready to contribute code to RaSCSI Reloaded, follow the <a href="ht
If you want to add a new translation, or improve upon an existing one, please follow the <a href="https://github.com/akuker/RASCSI/blob/develop/python/web/README.md#localizing-the-web-interface">instructions in the Web Interface README</a>. Once the translation is complete, please use the same workflow as above to contribute it to the project.
<a href="https://www.tindie.com/stores/landogriffin/?ref=offsite_badges&utm_source=sellers_akuker&utm_medium=badges&utm_campaign=badge_large"><img src="https://d2ss6ovg47m0r5.cloudfront.net/badges/tindie-larges.png" alt="I sell on Tindie" width="200" height="104"></a>
<a href="https://www.tindie.com/stores/landogriffin/?ref=offsite_badges&utm_source=sellers_akuker&utm_medium=badges&utm_campaign=badge_large"><img src="https://d2ss6ovg47m0r5.cloudfront.net/badges/tindie-larges.png" alt="I sell on Tindie" width="200" height="104"></a>[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/summary/new_code?id=akuker_RASCSI)
# GitHub Sponsors
Thank you to all of the GitHub sponsors who support the development community!
Special thank you to the Gold level sponsors!
- <a href="https://github.com/mikelord68">@mikelord68</a>
- <a href="https://github.com/SamplerSpa-de">@samplerspa-de</a>
Special thank you to the Silver level sponsors!
- <a href="https://github.com/stinkerton18">@stinkerton18</a>
- <a href="https://github.com/hsiboy">@hsiboy</a>
- <a href="https://github.com/pendleton115">@pendleton115</a>
- <a href="https://github.com/Teufelhunden-0311">@Teufelhunden-0311</a>
- Private sponsor ;]

View File

@ -3,13 +3,13 @@
rascsi \- Emulates SCSI devices using the Raspberry Pi GPIO pins
.SH SYNOPSIS
.B rascsi
[\fB\-F\f® \fIFOLDER\fR]
[\fB\-L\f® \fILOG_LEVEL\fR]
[\fB\-P\f® \fIACCESS_TOKEN_FILE\fR]
[\fB\-F\fR \fIFOLDER\fR]
[\fB\-L\fR \fILOG_LEVEL\fR]
[\fB\-P\fR \fIACCESS_TOKEN_FILE\fR]
[\fB\-R\fR \fISCAN_DEPTH\fR]
[\fB\-h\fR]
[\fB\-n\fR \fIVENDOR:PRODUCT:REVISION\fR]
[\fB\-p\f® \fIPORT\fR]
[\fB\-p\fR \fIPORT\fR]
[\fB\-r\fR \fIRESERVED_IDS\fR]
[\fB\-n\fR \fITYPE\fR]
[\fB\-v\fR]
@ -18,22 +18,22 @@ rascsi \- Emulates SCSI devices using the Raspberry Pi GPIO pins
[\fB\-HDn[:u]\fR \fIFILE\fR]...
.SH DESCRIPTION
.B rascsi
Emulates SCSI devices using the Raspberry Pi GPIO pins.
emulates SCSI devices using the Raspberry Pi GPIO pins.
.PP
In the arguments to RaSCSI, one or more SCSI (-IDn[:u]) or SASI (-HDn[:u]) devices can be specified.
In the arguments to RaSCSI, one or more SCSI (-IDn[:u]) devices can be specified.
The number (n) after the ID or HD identifier specifies the ID number for that device. The optional number (u) specifies the LUN (logical unit) for that device. The default LUN is 0.
For SCSI: The ID is limited from 0-7. However, typically SCSI ID 7 is reserved for the "initiator" (the host computer). The LUN is limited from 0-31. Note that SASI is considered rare and only used on very early Sharp X68000 computers.
For SCSI: The ID is limited from 0-7. However, typically SCSI ID 7 is reserved for the "initiator" (the host computer). The LUN is limited from 0-31.
.PP
RaSCSI will determine the type of device based upon the file extension of the FILE argument.
hdf: SASI Hard Disk image (XM6 SASI HD image - typically only used with X68000)
hd1: SCSI Hard Disk image (generic, non-removable, SCSI-1)
hds: SCSI Hard Disk image (generic, non-removable)
hdr: SCSI Hard Disk image (generic, removable)
hdn: SCSI Hard Disk image (NEC GENUINE)
hdi: SCSI Hard Disk image (Anex86 HD image)
nhd: SCSI Hard Disk image (T98Next HD image)
hda: SCSI Hard Disk image (APPLE GENUINE - typically used with Mac SCSI emulation)
mos: SCSI Magneto-optical image (XM6 SCSI MO image - typically only used with X68000)
iso: SCSI CD-ROM image (ISO 9660 image)
hdn: SCSI Hard Disk image (NEC compatible - only used with PC-98 computers)
hdi: SCSI Hard Disk image (Anex86 proprietary - only used with PC-98 computers)
nhd: SCSI Hard Disk image (T98Next proprietary - only used with PC-98 computers)
hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers)
mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.)
iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image)
For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command:
sudo rascsi -ID0 /path/to/drive/hdimage.hda
@ -47,7 +47,7 @@ To quit RaSCSI, press Control + C. If it is running in the background, you can k
.SH OPTIONS
.TP
.BR \-b\fI " " \fIBLOCK_SIZE
The optional block size. For SCSI drives 512, 1024, 2048 or 4096 bytes, default size is 512 bytes. For SASI drives 256 or 1024 bytes, default is 256 bytes.
The optional block size, either 512, 1024, 2048 or 4096 bytes. Default size is 512 bytes.
.TP
.BR \-F\fI " " \fIFOLDER
The default folder for image files. For files in this folder no absolute path needs to be specified. The initial default folder is '~/images'.
@ -85,13 +85,9 @@ Overrides the default locale for client-faces error messages. The client can ove
n is the SCSI ID number (0-7). u (0-31) is the optional LUN (logical unit). The default LUN is 0.
.IP
FILE is the name of the image file to use for the SCSI device. For devices that do not support an image file (SCBR, SCDP, SCLP, SCHS) the filename may have a special meaning or a dummy name can be provided. For SCBR and SCDP it is an optioinal prioritized list of network interfaces, an optional IP address and netmask, e.g. "interfaces=eth0,eth1,wlan0:inet=10.10.20.1/24". For SCLP it is the print command to be used and a reservation timeout in seconds, e.g. "cmd=lp -oraw %f:timeout=60".
.TP
.BR \-HD\fIn[:u] " " \fIFILE
n is the SASI ID number (0-15). The effective SASI ID is calculated as n/2, the effective SASI LUN is calculated is the remainder of n/2. Alternatively the n:u syntax can be used, where ns is the SASI ID (0-7) and u the LUN (0-1).
.IP
FILE is the name of the image file to use for the SASI device.
FILE is the name of the image file to use for the SCSI device.
.IP
Note: SASI usage is rare, and is typically limited to early Unix workstations and Sharp X68000 systems.
.SH EXAMPLES
Launch RaSCSI with no emulated drives attached:
@ -103,13 +99,13 @@ Launch RaSCSI with an Apple hard drive image as ID 0 and a CD-ROM as ID 2
Launch RaSCSI with a removable SCSI drive image as ID 0 and the raw device file /dev/hdb (e.g. a USB stick) and a DaynaPort network adapter as ID 6:
rascsi -ID0 -t scrm /dev/hdb -ID6 -t scdp daynaport
To create an empty, 100MB HD image, use the following command:
To create an empty, 100MiB HD image, use the following command:
dd if=/dev/zero of=/path/to/newimage.hda bs=512 count=204800
In case the fallocate command is available a much faster alternative to the dd command is:
fallocate -l 104857600 /path/to/newimage.hda
.SH SEE ALSO
rasctl(1), scsimon(1), rasdump(1), sasidump(1)
rasctl(1), scsimon(1), rasdump(1)
Full documentation is available at: <https://www.github.com/akuker/RASCSI/wiki/>

View File

@ -1,157 +1,115 @@
!! ------ THIS FILE IS AUTO_GENERATED! DO NOT MANUALLY UPDATE!!!
!! ------ The native file is rascsi.1. Re-run 'make docs' after updating\n\n
rascsi(1) General Commands Manual rascsi(1)
!! ------ The native file is rascsi.1. Re-run 'make docs' after updating
rascsi(1) General Commands Manual rascsi(1)
NAME
rascsi - Emulates SCSI devices using the Raspberry Pi GPIO pins
SYNOPSIS
rascsi [-F[u00AE] FOLDER] [-L[u00AE] LOG_LEVEL] [-P[u00AE] ACCESS_TO
KEN_FILE] [-R SCAN_DEPTH] [-h] [-n VENDOR:PRODUCT:REVISION] [-p[u00AE]
PORT] [-r RESERVED_IDS] [-n TYPE] [-v] [-z LOCALE] [-IDn:[u] FILE]
[-HDn[:u] FILE]...
rascsi [-F FOLDER] [-L LOG_LEVEL] [-P ACCESS_TOKEN_FILE] [-R SCAN_DEPTH] [-h] [-n VENDOR:PRODUCT:REVISION] [-p PORT] [-r
RESERVED_IDS] [-n TYPE] [-v] [-z LOCALE] [-IDn:[u] FILE] [-HDn[:u] FILE]...
DESCRIPTION
rascsi Emulates SCSI devices using the Raspberry Pi GPIO pins.
rascsi emulates SCSI devices using the Raspberry Pi GPIO pins.
In the arguments to RaSCSI, one or more SCSI (-IDn[:u]) or SASI
(-HDn[:u]) devices can be specified. The number (n) after the ID or HD
identifier specifies the ID number for that device. The optional number
(u) specifies the LUN (logical unit) for that device. The default LUN
is 0. For SCSI: The ID is limited from 0-7. However, typically SCSI ID
7 is reserved for the "initiator" (the host computer). The LUN is lim
ited from 0-31. Note that SASI is considered rare and only used on very
early Sharp X68000 computers.
In the arguments to RaSCSI, one or more SCSI (-IDn[:u]) devices can be specified. The number (n) after the ID or HD iden
tifier specifies the ID number for that device. The optional number (u) specifies the LUN (logical unit) for that device.
The default LUN is 0. For SCSI: The ID is limited from 0-7. However, typically SCSI ID 7 is reserved for the "initiator"
(the host computer). The LUN is limited from 0-31.
RaSCSI will determine the type of device based upon the file extension
of the FILE argument.
hdf: SASI Hard Disk image (XM6 SASI HD image - typically only used
with X68000)
RaSCSI will determine the type of device based upon the file extension of the FILE argument.
hd1: SCSI Hard Disk image (generic, non-removable, SCSI-1)
hds: SCSI Hard Disk image (generic, non-removable)
hdr: SCSI Hard Disk image (generic, removable)
hdn: SCSI Hard Disk image (NEC GENUINE)
hdi: SCSI Hard Disk image (Anex86 HD image)
nhd: SCSI Hard Disk image (T98Next HD image)
hda: SCSI Hard Disk image (APPLE GENUINE - typically used with Mac
SCSI emulation)
mos: SCSI Magneto-optical image (XM6 SCSI MO image - typically only
used with X68000)
iso: SCSI CD-ROM image (ISO 9660 image)
hdn: SCSI Hard Disk image (NEC compatible - only used with PC-98 computers)
hdi: SCSI Hard Disk image (Anex86 proprietary - only used with PC-98 computers)
nhd: SCSI Hard Disk image (T98Next proprietary - only used with PC-98 computers)
hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers)
mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.)
iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image)
For example, if you want to specify an Apple-compatible HD image on ID
0, you can use the following command:
For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command:
sudo rascsi -ID0 /path/to/drive/hdimage.hda
Once RaSCSI starts, it will open a socket (default port is 6868) to al
low external management commands. If another process is using the
rascsi port, RaSCSI will terminate, since it is likely another instance
of RaSCSI. Once RaSCSI has initialized, the rasctl utility can be used
to send commands.
Once RaSCSI starts, it will open a socket (default port is 6868) to allow external management commands. If another process
is using the rascsi port, RaSCSI will terminate, since it is likely another instance of RaSCSI. Once RaSCSI has initial
ized, the rasctl utility can be used to send commands.
To quit RaSCSI, press Control + C. If it is running in the background,
you can kill it using an INT signal.
To quit RaSCSI, press Control + C. If it is running in the background, you can kill it using an INT signal.
OPTIONS
-b BLOCK_SIZE
The optional block size. For SCSI drives 512, 1024, 2048 or 4096
bytes, default size is 512 bytes. For SASI drives 256 or 1024
bytes, default is 256 bytes.
The optional block size, either 512, 1024, 2048 or 4096 bytes. Default size is 512 bytes.
-F FOLDER
The default folder for image files. For files in this folder no
absolute path needs to be specified. The initial default folder
is '~/images'.
The default folder for image files. For files in this folder no absolute path needs to be specified. The initial de
fault folder is '~/images'.
-L LOG_LEVEL
The rascsi log level (trace, debug, info, warn, err, critical,
off). The default log level is 'info'.
The rascsi log level (trace, debug, info, warn, err, critical, off). The default log level is 'info'.
-P ACCESS_TOKEN_FILE
Enable authentication and read the access token from the speci
fied file. The access token file must be owned by root and must
be readable by root only.
Enable authentication and read the access token from the specified file. The access token file must be owned by root
and must be readable by root only.
-R SCAN_DEPTH
Scan for image files recursively, up to a depth of SCAN_DEPTH.
Depth 0 means to ignore any folders within the default image
filder. Be careful when using this option with many sub-folders
in the default image folder. The default depth is 1.
Scan for image files recursively, up to a depth of SCAN_DEPTH. Depth 0 means to ignore any folders within the de
fault image filder. Be careful when using this option with many sub-folders in the default image folder. The default
depth is 1.
-h Show a help page.
-n VENDOR:PRODUCT:REVISION
Set the vendor, product and revision for the device, to be re
turned with the INQUIRY data. A complete set of name components
must be provided. VENDOR may have up to 8, PRODUCT up to 16, RE
VISION up to 4 characters. Padding with blanks to the maxium
length is automatically applied. Once set the name of a device
cannot be changed.
Set the vendor, product and revision for the device, to be returned with the INQUIRY data. A complete set of name
components must be provided. VENDOR may have up to 8, PRODUCT up to 16, REVISION up to 4 characters. Padding with
blanks to the maxium length is automatically applied. Once set the name of a device cannot be changed.
-p PORT
The rascsi server port, default is 6868.
-r RESERVED_IDS
Comma-separated list of IDs to reserve. Pass an empty list in
order to not reserve anything. -p TYPE The optional case-insen
sitive device type (SAHD, SCHD, SCRM, SCCD, SCMO, SCBR, SCDP,
SCLP, SCHS). If no type is specified for devices that support an
image file, rascsi tries to derive the type from the file exten
sion.
Comma-separated list of IDs to reserve. Pass an empty list in order to not reserve anything. -p TYPE The optional
case-insensitive device type (SAHD, SCHD, SCRM, SCCD, SCMO, SCBR, SCDP, SCLP, SCHS). If no type is specified for de
vices that support an image file, rascsi tries to derive the type from the file extension.
-v Display the rascsi version.
-z LOCALE
Overrides the default locale for client-faces error messages.
The client can override the locale.
Overrides the default locale for client-faces error messages. The client can override the locale.
-IDn[:u] FILE
n is the SCSI ID number (0-7). u (0-31) is the optional LUN
(logical unit). The default LUN is 0.
n is the SCSI ID number (0-7). u (0-31) is the optional LUN (logical unit). The default LUN is 0.
FILE is the name of the image file to use for the SCSI device.
For devices that do not support an image file (SCBR, SCDP, SCLP,
SCHS) the filename may have a special meaning or a dummy name
can be provided. For SCBR and SCDP it is an optioinal priori
tized list of network interfaces, an optional IP address and
netmask, e.g. "interfaces=eth0,eth1,wlan0:inet=10.10.20.1/24".
For SCLP it is the print command to be used and a reservation
timeout in seconds, e.g. "cmd=lp -oraw %f:timeout=60".
FILE is the name of the image file to use for the SCSI device. For devices that do not support an image file (SCBR,
SCDP, SCLP, SCHS) the filename may have a special meaning or a dummy name can be provided. For SCBR and SCDP it is
an optioinal prioritized list of network interfaces, an optional IP address and netmask, e.g. "inter
faces=eth0,eth1,wlan0:inet=10.10.20.1/24". For SCLP it is the print command to be used and a reservation timeout in
seconds, e.g. "cmd=lp -oraw %f:timeout=60".
-HDn[:u] FILE
n is the SASI ID number (0-15). The effective SASI ID is calcu
lated as n/2, the effective SASI LUN is calculated is the re
mainder of n/2. Alternatively the n:u syntax can be used, where
ns is the SASI ID (0-7) and u the LUN (0-1).
FILE is the name of the image file to use for the SASI device.
Note: SASI usage is rare, and is typically limited to early Unix
workstations and Sharp X68000 systems.
FILE is the name of the image file to use for the SCSI device.
EXAMPLES
Launch RaSCSI with no emulated drives attached:
rascsi
Launch RaSCSI with an Apple hard drive image as ID 0 and a CD-ROM as ID
2
Launch RaSCSI with an Apple hard drive image as ID 0 and a CD-ROM as ID 2
rascsi -ID0 /path/to/harddrive.hda -ID2 /path/to/cdimage.iso
Launch RaSCSI with a removable SCSI drive image as ID 0 and the raw de
vice file /dev/hdb (e.g. a USB stick) and a DaynaPort network adapter
as ID 6:
Launch RaSCSI with a removable SCSI drive image as ID 0 and the raw device file /dev/hdb (e.g. a USB stick) and a DaynaPort
network adapter as ID 6:
rascsi -ID0 -t scrm /dev/hdb -ID6 -t scdp daynaport
To create an empty, 100MB HD image, use the following command:
To create an empty, 100MiB HD image, use the following command:
dd if=/dev/zero of=/path/to/newimage.hda bs=512 count=204800
In case the fallocate command is available a much faster alternative to
the dd command is:
In case the fallocate command is available a much faster alternative to the dd command is:
fallocate -l 104857600 /path/to/newimage.hda
SEE ALSO
rasctl(1), scsimon(1), rasdump(1), sasidump(1)
rasctl(1), scsimon(1), rasdump(1)
Full documentation is available at:
<https://www.github.com/akuker/RASCSI/wiki/>
Full documentation is available at: <https://www.github.com/akuker/RASCSI/wiki/>
rascsi(1)
rascsi(1)

View File

@ -35,7 +35,7 @@ rasctl \- Sends management commands to the rascsi process
[\fB\-z\fR \fILOCALE\fR]
.SH DESCRIPTION
.B rasctl
Sends commands to the rascsi process to make configuration adjustments at runtime or to check the status of the devices.
sends commands to the rascsi process to make configuration adjustments at runtime or to check the status of the devices.
Either the -i or -l option should be specified at one time. Not both.
@ -136,7 +136,7 @@ Command is the operation being requested. Options are:
eject, protect and unprotect are idempotent.
.TP
.BR \-b\fI " " \fIBLOCK_SIZE
The optional block size. For SCSI drives 512, 1024, 2048 or 4096 bytes, default size is 512 bytes. For SASI drives 256 or 1024 bytes, default is 256 bytes.
The optional block size, either 512, 1024, 2048 or 4096 bytes. The default size is 512 bytes.
.TP
.BR \-f\fI " " \fIFILE|PARAM
Device-specific: Either a path to a disk image file, or a parameter for a non-disk device. See the rascsi(1) man page for permitted file types.
@ -174,6 +174,6 @@ Request the RaSCSI process to attach a disk (assumed) to SCSI ID 0 with the cont
rasctl -i 0 -f HDIIMAGE0.HDS
.SH SEE ALSO
rascsi(1), scsimon(1), rasdump(1), sasidump(1)
rascsi(1), scsimon(1), rasdump(1)
Full documentation is available at: <https://www.github.com/akuker/RASCSI/wiki/>

View File

@ -1,32 +1,31 @@
!! ------ THIS FILE IS AUTO_GENERATED! DO NOT MANUALLY UPDATE!!!
!! ------ The native file is rasctl.1. Re-run 'make docs' after updating\n\n
rascsi(1) General Commands Manual rascsi(1)
!! ------ The native file is rasctl.1. Re-run 'make docs' after updating
rascsi(1) General Commands Manual rascsi(1)
NAME
rasctl - Sends management commands to the rascsi process
SYNOPSIS
rasctl -e | -l | -m | -o | -s | -v | -D | -I | -L | -O | -P | -T | -V |
-X | [-C FILENAME:FILESIZE] [-E FILENAME] [-F IMAGE_FOLDER] [-R CUR
RENT_NAME:NEW_NAME] [-c CMD] [-f FILE|PARAM] [-g LOG_LEVEL] [-h HOST]
[-i ID [-n NAME] [-p PORT] [-r RESERVED_IDS] [-t TYPE] [-u UNIT] [-x
CURRENT_NAME:NEW_NAME] [-z LOCALE]
rasctl -e | -l | -m | -o | -s | -v | -D | -I | -L | -O | -P | -T | -V | -X | [-C FILENAME:FILESIZE] [-E FILENAME] [-F IM
AGE_FOLDER] [-R CURRENT_NAME:NEW_NAME] [-c CMD] [-f FILE|PARAM] [-g LOG_LEVEL] [-h HOST] [-i ID [-n NAME] [-p PORT] [-r RE
SERVED_IDS] [-t TYPE] [-u UNIT] [-x CURRENT_NAME:NEW_NAME] [-z LOCALE]
DESCRIPTION
rasctl Sends commands to the rascsi process to make configuration ad
justments at runtime or to check the status of the devices.
rasctl sends commands to the rascsi process to make configuration adjustments at runtime or to check the status of the de
vices.
Either the -i or -l option should be specified at one time. Not both.
You do NOT need root privileges to use rasctl.
Note: The command and type arguments are case insensitive. Only the
first letter of the command/type is evaluated by the tool.
Note: The command and type arguments are case insensitive. Only the first letter of the command/type is evaluated by the
tool.
OPTIONS
-C FILENAME:FILESIZE
Create an image file in the default image folder with the speci
fied name and size in bytes.
Create an image file in the default image folder with the specified name and size in bytes.
-D Detach all devices.
@ -39,28 +38,22 @@ OPTIONS
-I Gets the list of reserved device IDs.
-L LOG_LEVEL
Set the rascsi log level (trace, debug, info, warn, err, criti
cal, off).
Set the rascsi log level (trace, debug, info, warn, err, critical, off).
-h HOST
The rascsi host to connect to, default is 'localhost'.
-e List all images files in the default image folder.
-N Lists all available network interfaces provided that they are
up.
-N Lists all available network interfaces provided that they are up.
-O Display the available rascsi server log levels and the current
log level.
-O Display the available rascsi server log levels and the current log level.
-P Prompt for the access token in case rascsi requires authentica
tion.
-P Prompt for the access token in case rascsi requires authentication.
-l List all of the devices that are currently being emulated by
RaSCSI, as well as their current status.
-l List all of the devices that are currently being emulated by RaSCSI, as well as their current status.
-m List all file extensions recognized by RaSCSI and the device
types they map to.
-m List all file extensions recognized by RaSCSI and the device types they map to.
-o Display operation meta data information.
@ -71,11 +64,9 @@ OPTIONS
The rascsi port to connect to, default is 6868.
-r RESERVED_IDS
Comma-separated list of IDs to reserve. Pass an empty list in
order to not reserve anything.
Comma-separated list of IDs to reserve. Pass an empty list in order to not reserve anything.
-s Display server-side settings like available images or supported
device types.
-s Display server-side settings like available images or supported device types.
-T Display all device types and their properties.
@ -101,29 +92,23 @@ OPTIONS
d(etach): Detach disk
i(nsert): Insert media (removable media devices only)
e(ject): Eject media (removable media devices only)
p(rotect): Write protect the medium (not for CD-ROMs, which
are always read-only)
u(nprotect): Remove write protection from the medium (not for
CD-ROMs, which are always read-only)
p(rotect): Write protect the medium (not for CD-ROMs, which are always read-only)
u(nprotect): Remove write protection from the medium (not for CD-ROMs, which are always read-only)
s(how): Display device information
eject, protect and unprotect are idempotent.
-b BLOCK_SIZE
The optional block size. For SCSI drives 512, 1024, 2048 or 4096
bytes, default size is 512 bytes. For SASI drives 256 or 1024
bytes, default is 256 bytes.
The optional block size, either 512, 1024, 2048 or 4096 bytes. The default size is 512 bytes.
-f FILE|PARAM
Device-specific: Either a path to a disk image file, or a param
eter for a non-disk device. See the rascsi(1) man page for per
mitted file types.
Device-specific: Either a path to a disk image file, or a parameter for a non-disk device. See the rascsi(1) man
page for permitted file types.
-t TYPE
Specifies the device type. This type overrides the type derived
from the file extension of the specified image. See the
rascsi(1) man page for the available device types. For some
types there are shortcuts (only the first letter is required):
Specifies the device type. This type overrides the type derived from the file extension of the specified image. See
the rascsi(1) man page for the available device types. For some types there are shortcuts (only the first letter is
required):
hd: SCSI hard disk drive
rm: SCSI removable media drive
cd: CD-ROM
@ -134,17 +119,13 @@ OPTIONS
services: Host services device
-n VENDOR:PRODUCT:REVISION
The vendor, product and revision for the device, to be returned
with the INQUIRY data. A complete set of name components must be
provided. VENDOR may have up to 8, PRODUCT up to 16, REVISION up
to 4 characters. Padding with blanks to the maxium length is au
tomatically applied. Once set the name of a device cannot be
changed.
The vendor, product and revision for the device, to be returned with the INQUIRY data. A complete set of name compo
nents must be provided. VENDOR may have up to 8, PRODUCT up to 16, REVISION up to 4 characters. Padding with blanks
to the maxium length is automatically applied. Once set the name of a device cannot be changed.
-u UNIT
Unit number (0-31). This will default to 0. This option is only
used when there are multiple SCSI devices on a shared SCSI con
troller. (This is not common)
Unit number (0-31). This will default to 0. This option is only used when there are multiple SCSI devices on a
shared SCSI controller. (This is not common)
EXAMPLES
Show a listing of all of the SCSI devices and their current status.
@ -157,14 +138,13 @@ EXAMPLES
| 0 | 1 | SCHD | /home/pi/harddisk.hda
+----+-----+------+-------------------------------------
Request the RaSCSI process to attach a disk (assumed) to SCSI ID 0 with
the contents of the file system image "HDIIMAGE0.HDS".
Request the RaSCSI process to attach a disk (assumed) to SCSI ID 0 with the contents of the file system image "HDIIM
AGE0.HDS".
rasctl -i 0 -f HDIIMAGE0.HDS
SEE ALSO
rascsi(1), scsimon(1), rasdump(1), sasidump(1)
rascsi(1), scsimon(1), rasdump(1)
Full documentation is available at:
<https://www.github.com/akuker/RASCSI/wiki/>
Full documentation is available at: <https://www.github.com/akuker/RASCSI/wiki/>
rascsi(1)
rascsi(1)

View File

@ -9,7 +9,7 @@ rasdump \- SCSI disk dumping tool for RaSCSI
[\fB\-r\fR]
.SH DESCRIPTION
.B rasdump
Samples the data on physical SCSI storage media, including hard drives and MO drives, and stores it to an image file. It can also restore from a dumped file onto physical SCSI storage media. Can be connected directly, through a STANDARD RaSCSI board, or a FULLSPEC RaSCSI board.
samples the data on physical SCSI storage media, including hard drives and MO drives, and stores it to an image file. It can also restore from a dumped file onto physical SCSI storage media. Can be connected directly, through a STANDARD RaSCSI board, or a FULLSPEC RaSCSI board.
Set its own ID with the BID option. Defaults to 7 if ommitted.

View File

@ -1,41 +0,0 @@
.TH sasidump 1
.SH NAME
sasidump \- SASI disk dumping tool for RaSCSI
.SH SYNOPSIS
.B sasidump
\fB\-i\fR \fIID\fR
[\fB\-u\fR \fIUT\fR]
[\fB\-b\fR \fIBSIZE\fR]
\fB\-c\fR \fICOUNT\fR
\fB\-f\fR \fIFILE\fR
[\fB\-r\fR]
.SH DESCRIPTION
.B sasidump
Samples the data on physical SASI storage media, and stores it to an image file. It can also restore from a dumped file onto physical SASI storage media.
.SH OPTIONS
.TP
.BR \-i\fI " "\fIID
SASI ID of the target device
.TP
.BR \-u\fI " "\fIUD
Unit ID of the target device
.TP
.BR \-b\fI " "\fIBSIZE
Block size (default is 512)
.TP
.BR \-c\fI " "\fICOUNT
Block count
.TP
.BR \-f\fI " "\fIFILE
Path to the dump file
.TP
.BR \-r\fI
Restoration mode
.SH EXAMPLES
.SH SEE ALSO
rasctl(1), rascsi(1), scsimon(1), rasdump(1)
Full documentation is available at: <https://www.github.com/akuker/RASCSI/wiki/>

View File

@ -5,7 +5,7 @@ scsimon \- Acts as a data capture tool for all traffic on the SCSI bus. Data is
.B scsimon
.SH DESCRIPTION
.B scsimon
Monitors all of the traffic on the SCSI bus, using a RaSCSI device. The data is cached in memory while the tool is running. A circular buffer is used so that only the most recent 1,000,000 transactions are stored. The tool will continue to run until the user presses CTRL-C, or the process receives a SIGINT signal.
monitors all of the traffic on the SCSI bus, using a RaSCSI device. The data is cached in memory while the tool is running. A circular buffer is used so that only the most recent 1,000,000 transactions are stored. The tool will continue to run until the user presses CTRL-C, or the process receives a SIGINT signal.
.PP
The logged data is stored in a file called "log.vcd" in the current working directory from where scsimon was launched.
@ -22,6 +22,6 @@ Launch scsimon to capture all SCSI traffic available to the RaSCSI hardware:
scsimon
.SH SEE ALSO
rasctl(1), rascsi(1), rasdump(1), sasidump(1)
rasctl(1), rascsi(1), rasdump(1)
Full documentation is available at: <https://www.github.com/akuker/RASCSI/wiki/>

View File

@ -2,24 +2,20 @@
!! ------ The native file is scsimon.1. Re-run 'make docs' after updating
scsimon(1) General Commands Manual scsimon(1)
scsimon(1) General Commands Manual scsimon(1)
NAME
scsimon - Acts as a data capture tool for all traffic on the SCSI bus.
Data is stored in a Value Change Dump (VCD) file.
scsimon - Acts as a data capture tool for all traffic on the SCSI bus. Data is stored in a Value Change Dump (VCD) file.
SYNOPSIS
scsimon
DESCRIPTION
scsimon Monitors all of the traffic on the SCSI bus, using a RaSCSI de
vice. The data is cached in memory while the tool is running. A circu
lar buffer is used so that only the most recent 1,000,000 transactions
are stored. The tool will continue to run until the user presses CTRL-
C, or the process receives a SIGINT signal.
scsimon monitors all of the traffic on the SCSI bus, using a RaSCSI device. The data is cached in memory while the tool is
running. A circular buffer is used so that only the most recent 1,000,000 transactions are stored. The tool will continue
to run until the user presses CTRL-C, or the process receives a SIGINT signal.
The logged data is stored in a file called "log.vcd" in the current
working directory from where scsimon was launched.
The logged data is stored in a file called "log.vcd" in the current working directory from where scsimon was launched.
Currently, scsimon doesn't accept any arguments.
@ -29,14 +25,12 @@ OPTIONS
None
EXAMPLES
Launch scsimon to capture all SCSI traffic available to the RaSCSI
hardware:
Launch scsimon to capture all SCSI traffic available to the RaSCSI hardware:
scsimon
SEE ALSO
rasctl(1), rascsi(1), rasdump(1), sasidump(1)
rasctl(1), rascsi(1), rasdump(1)
Full documentation is available at:
<https://www.github.com/akuker/RASCSI/wiki/>
Full documentation is available at: <https://www.github.com/akuker/RASCSI/wiki/>
scsimon(1)
scsimon(1)

115
docker/README.md Normal file
View File

@ -0,0 +1,115 @@
# Docker Environment for Development and Testing
⚠️ **Important:** The Docker environment is unable to connect to the RaSCSI board and is
intended for development and testing purposes only. To setup RaSCSI on a Raspberry Pi
refer to the [setup instructions](https://github.com/akuker/RASCSI/wiki/Setup-Instructions)
on the wiki instead.
## Introduction
This documentation currently focuses on using Docker for developing and testing the web UI.
Additions, amendments and contributions for additional workflows are most welcome.
## Getting Started
The easiest way to launch a new environment is to use Docker Compose.
```
cd docker
docker compose up
```
Containers will be built and started for the RaSCSI server and the web UI.
The web UI can be accessed at:
* http://localhost:8080
* https://localhost:8443
To stop the containers, press *Ctrl + C*, or run `docker compose stop`
from another terminal.
## Environment Variables
The following environment variables are available when using Docker Compose:
| Environment Variable | Default |
| -------------------- | -------- |
| `OS_DISTRO` | debian |
| `OS_VERSION` | buster |
| `OS_ARCH` | amd64 |
| `WEB_HTTP_PORT` | 8080 |
| `WEB_HTTPS_PORT` | 8443 |
| `WEB_LOG_LEVEL` | info |
| `RASCSI_HOST` | rascsi |
| `RASCSI_PORT` | 6868 |
| `RASCSI_PASSWORD` | *[None]* |
| `RASCSI_LOG_LEVEL` | debug |
**Examples:**
Run Debian "bullseye":
```
OS_VERSION=bullseye docker compose up
```
Start the web UI with the log level set to debug:
```
WEB_LOG_LEVEL=debug docker compose up
```
## Volumes
When using Docker Compose the following volumes will be mounted automatically:
| Local Path | Container Path |
| ----------------------- | ------------------------ |
| docker/volumes/images/ | /home/pi/images/ |
| docker/volumes/config/ | /home/pi/.config/rascsi/ |
## How To
### Rebuild Containers
You should rebuild the container images after checking out a different version of
RaSCSI or making changes which affect the environment at build time, e.g.
`easyinstall.sh`.
```
docker compose up --build
```
### Open a Shell on a Running Container
Run the following command, replacing `[CONTAINER]` with `rascsi` or `rascsi_web`.
```
docker compose exec [CONTAINER] bash
```
### Setup Live Editing for the Web UI
Use a `docker-compose.override.yml` to mount the local `python` directory to
`/home/pi/RASCSI/python/` in the `rascsi_web` container.
Any changes to *.py files on the host computer (i.e. in your IDE) will trigger
the web UI process to be restarted in the container.
**Example:**
```
services:
rascsi_web:
volumes:
- ../python:/home/pi/RASCSI/python:delegated
```
### Connect the Web UI to a Real RaSCSI
This can be useful for testing, but there are some caveats, e.g. the RaSCSI and the
web UI will be accessing separate `images` directories.
```
RASCSI_HOST=foo RASCSI_PASSWORD=bar docker compose up
```

View File

@ -0,0 +1,4 @@
services:
rascsi_web:
volumes:
- ../python:/home/pi/RASCSI/python:delegated

71
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,71 @@
services:
rascsi:
container_name: rascsi
image: rascsi:develop-${OS_DISTRO:-debian}-${OS_VERSION:-buster}-${OS_ARCH:-amd64}-standalone
pull_policy: never
build:
context: ..
dockerfile: docker/rascsi/Dockerfile
args:
- OS_DISTRO=${OS_DISTRO:-debian}
- OS_VERSION=${OS_VERSION:-buster}
- OS_ARCH=${OS_ARCH:-amd64}
volumes:
- ./volumes/images:/home/pi/images:delegated
- ./volumes/config:/home/pi/.config/rascsi:delegated
ports:
- "127.0.0.1:${RASCSI_PORT:-6868}:6868"
environment:
- RASCSI_PASSWORD=${RASCSI_PASSWORD:-}
init: true
command: [
"/usr/local/bin/rascsi_wrapper.sh",
"-L",
"${RASCSI_LOG_LEVEL:-trace}",
"-r",
"7",
"-F",
"/home/pi/images"
]
rascsi_web:
container_name: rascsi_web
image: rascsi:develop-${OS_DISTRO:-debian}-${OS_VERSION:-buster}-${OS_ARCH:-amd64}-web
pull_policy: never
build:
context: ..
dockerfile: docker/rascsi-web/Dockerfile
args:
- OS_DISTRO=${OS_DISTRO:-debian}
- OS_VERSION=${OS_VERSION:-buster}
- OS_ARCH=${OS_ARCH:-amd64}
volumes:
- ./volumes/images:/home/pi/images:delegated
- ./volumes/config:/home/pi/.config/rascsi:delegated
ports:
- "127.0.0.1:${WEB_HTTP_PORT:-8080}:80"
- "127.0.0.1:${WEB_HTTPS_PORT:-8443}:443"
environment:
- RASCSI_PASSWORD=${RASCSI_PASSWORD:-}
init: true
command: [
"start.sh",
"--rascsi-host=${RASCSI_HOST:-rascsi}",
"--rascsi-port=${RASCSI_PORT:-6868}",
"--log-level=${WEB_LOG_LEVEL:-info}",
"--dev-mode"
]
pytest:
container_name: pytest
image: rascsi:pytest
pull_policy: never
profiles:
- webui-tests
build:
context: ..
dockerfile: docker/pytest/Dockerfile
volumes:
- ../python/web:/src:delegated
working_dir: /src
entrypoint: "pytest"

5
docker/pytest/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM python:3.7-bullseye
ENV DOCKER=1
COPY python/web/requirements-dev.txt /requirements-dev.txt
RUN pip install -r /requirements-dev.txt

View File

@ -0,0 +1,35 @@
ARG OS_DISTRO=debian
ARG OS_VERSION=buster
ARG OS_ARCH=amd64
FROM "${OS_ARCH}/${OS_DISTRO}:${OS_VERSION}"
EXPOSE 80 443
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends sudo systemd rsyslog procps man-db man2html
RUN groupadd pi
RUN useradd --create-home --shell /bin/bash -g pi pi
RUN echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
RUN echo "pi:rascsi" | chpasswd
RUN mkdir /home/pi/afpshare
RUN touch /etc/dhcpcd.conf
RUN mkdir -p /etc/network/interfaces.d/
WORKDIR /home/pi/RASCSI
USER pi
COPY --chown=pi:pi . .
# Standalone RaSCSI web UI
RUN ./easyinstall.sh --run_choice=11 --skip-token
# Wired network bridge
RUN ./easyinstall.sh --run_choice=6 --headless
USER root
WORKDIR /home/pi
RUN pip3 install watchdog
COPY docker/rascsi-web/start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh
CMD ["/usr/local/bin/start.sh"]

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
if ! [[ -f "/home/pi/RASCSI/python/common/src/rascsi_interface_pb2.py" ]]; then
# Build rascsi_interface_pb2.py with the protobuf compiler
protoc \
-I=/home/pi/RASCSI/src/raspberrypi \
--python_out=/home/pi/RASCSI/python/common/src \
rascsi_interface.proto
fi
# Start Nginx service
nginx
# Pass args to web UI start script
if [[ $RASCSI_PASSWORD ]]; then
/home/pi/RASCSI/python/web/start.sh "$@" --password=$RASCSI_PASSWORD
else
/home/pi/RASCSI/python/web/start.sh "$@"
fi

30
docker/rascsi/Dockerfile Normal file
View File

@ -0,0 +1,30 @@
ARG OS_DISTRO=debian
ARG OS_VERSION=buster
ARG OS_ARCH=amd64
FROM "${OS_ARCH}/${OS_DISTRO}:${OS_VERSION}"
EXPOSE 6868
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends sudo systemd rsyslog patch
RUN groupadd pi
RUN useradd --create-home --shell /bin/bash -g pi pi
RUN echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
WORKDIR /home/pi/RASCSI
USER pi
COPY --chown=pi:pi . .
# Workaround for Bullseye amd64 compilation error
# https://github.com/akuker/RASCSI/issues/821
RUN patch -p0 < docker/rascsi/cfilesystem.patch
# Install RaSCSI standalone
RUN ./easyinstall.sh --run_choice=10 --cores=`nproc` --skip-token
USER root
WORKDIR /home/pi
COPY docker/rascsi/rascsi_wrapper.sh /usr/local/bin/rascsi_wrapper.sh
RUN chmod +x /usr/local/bin/rascsi_wrapper.sh
CMD ["/usr/local/bin/rascsi_wrapper.sh", "-L", "trace", "-r", "7", "-F", "/home/pi/images"]

View File

@ -0,0 +1,24 @@
--- src/raspberrypi/devices/cfilesystem.cpp 2022-09-08 12:07:14.000000000 +0100
+++ src/raspberrypi/devices/cfilesystem.cpp.patched 2022-09-08 12:12:55.000000000 +0100
@@ -1075,12 +1075,15 @@
m_dirHuman.name[i] = ' ';
}
- for (i = 0; i < 10; i++) {
- if (p < m_pszHumanExt)
- m_dirHuman.add[i] = *p++;
- else
- m_dirHuman.add[i] = '\0';
- }
+ // This code causes a compilation error on Debian "bullseye" (amd64)
+ // https://github.com/akuker/RASCSI/issues/821
+ //
+ // for (i = 0; i < 10; i++) {
+ // if (p < m_pszHumanExt)
+ // m_dirHuman.add[i] = *p++;
+ // else
+ // m_dirHuman.add[i] = '\0';
+ // }
if (*p == '.')
p++;

View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
if [[ $RASCSI_PASSWORD ]]; then
TOKEN_FILE="/home/pi/.config/rascsi/rascsi_secret"
mkdir -p /home/pi/.config/rascsi || true
echo $RASCSI_PASSWORD > $TOKEN_FILE
chmod 700 $TOKEN_FILE
/usr/local/bin/rascsi "$@" -P $TOKEN_FILE
else
/usr/local/bin/rascsi "$@"
fi

View File

View File

@ -1 +0,0 @@
theme: jekyll-theme-dinky

View File

@ -1,37 +0,0 @@
## Welcome to GitHub Pages
You can use the [editor on GitHub](https://github.com/akuker/RASCSI/edit/master/docs/index.md) to maintain and preview the content for your website in Markdown files.
Whenever you commit to this repository, GitHub Pages will run [Jekyll](https://jekyllrb.com/) to rebuild the pages in your site, from the content in your Markdown files.
### Markdown
Markdown is a lightweight and easy-to-use syntax for styling your writing. It includes conventions for
```markdown
Syntax highlighted code block
# Header 1
## Header 2
### Header 3
- Bulleted
- List
1. Numbered
2. List
**Bold** and _Italic_ and `Code` text
[Link](url) and ![Image](src)
```
For more details see [GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/).
### Jekyll Themes
Your Pages site will use the layout and styles from the Jekyll theme you have selected in your [repository settings](https://github.com/akuker/RASCSI/settings). The name of this theme is saved in the Jekyll `_config.yml` configuration file.
### Support or Contact
Having trouble with Pages? Check out our [documentation](https://docs.github.com/categories/github-pages-basics/) or [contact support](https://support.github.com/contact) and well help you sort it out.

View File

@ -63,6 +63,7 @@ LIDO_DRIVER=$BASE/lido-driver.img
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
GIT_REMOTE=${GIT_REMOTE:-origin}
TOKEN=""
SECRET_FILE="$HOME/.config/rascsi/rascsi_secret"
set -e
@ -82,7 +83,42 @@ function sudoCheck() {
# install all dependency packages for RaSCSI Service
function installPackages() {
sudo apt-get update && sudo apt-get install git libspdlog-dev libpcap-dev genisoimage python3 python3-venv python3-dev python3-pip nginx libpcap-dev protobuf-compiler bridge-utils libev-dev libevdev2 unar -y </dev/null
sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y -qq \
build-essential \
git \
libspdlog-dev \
libpcap-dev \
libprotobuf-dev \
genisoimage \
python3 \
python3-dev \
python3-pip \
python3-venv \
python3-setuptools \
python3-wheel \
nginx-light \
protobuf-compiler \
bridge-utils \
libev-dev \
libevdev2 \
unzip \
unar \
disktype \
libgmock-dev \
man2html
}
# install Debian packges for RaSCSI standalone
function installPackagesStandalone() {
sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y -qq \
build-essential \
libspdlog-dev \
libpcap-dev \
libprotobuf-dev \
protobuf-compiler \
disktype \
libgmock-dev \
man2html
}
# cache the pip packages
@ -246,32 +282,42 @@ function backupRaScsiService() {
# Offers the choice of enabling token-based authentication for RaSCSI
function configureTokenAuth() {
echo ""
echo "Do you want to protect your RaSCSI installation with a password? [y/N]"
read REPLY
if [[ -f "$HOME/.rascsi_secret" ]]; then
sudo rm "$HOME/.rascsi_secret"
echo "Removed (legacy) RaSCSI token file"
fi
SECRET_FILE="$HOME/.config/rascsi/rascsi_secret"
if [[ -f $SECRET_FILE ]]; then
sudo rm "$SECRET_FILE"
echo "Removed RaSCSI token file"
fi
if [[ $SKIP_TOKEN ]]; then
echo "Skipping RaSCSI token setup"
return 0
fi
if [[ -z $TOKEN ]]; then
echo ""
echo "Do you want to protect your RaSCSI installation with a password? [y/N]"
read REPLY
if ! [[ $REPLY =~ ^[Yy]$ ]]; then
return 0
fi
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
echo -n "Enter the password that you want to use: "
read -r TOKEN
if [ -f "$HOME/.rascsi_secret" ]; then
sudo rm "$HOME/.rascsi_secret"
echo "Removed old RaSCSI token file"
fi
if [ -f "$SECRET_FILE" ]; then
sudo rm "$SECRET_FILE"
echo "Removed old RaSCSI token file"
fi
echo "$TOKEN" > "$SECRET_FILE"
fi
echo "$TOKEN" > "$SECRET_FILE"
# Make the secret file owned and only readable by root
sudo chown root:root "$SECRET_FILE"
sudo chmod 600 "$SECRET_FILE"
echo ""
echo "Configured RaSCSI to use $SECRET_FILE for authentication. This file is readable by root only."
echo "Make note of your password: you will need it to use rasctl and other RaSCSI clients."
fi
sudo chown root:root "$SECRET_FILE"
sudo chmod 600 "$SECRET_FILE"
echo ""
echo "Configured RaSCSI to use $SECRET_FILE for authentication. This file is readable by root only."
echo "Make note of your password: you will need it to use rasctl and other RaSCSI clients."
}
# Modifies and installs the rascsi service
@ -523,7 +569,7 @@ function showMacproxyStatus() {
}
# Creates a drive image file with specific parameters
function createDrive600MB() {
function createDrive600M() {
createDrive 600 "HD600"
}
@ -531,7 +577,7 @@ function createDrive600MB() {
function createDriveCustom() {
driveSize=-1
until [ $driveSize -ge "10" ] && [ $driveSize -le "4000" ]; do
echo "What drive size would you like (in MB) (10-4000)"
echo "What drive size would you like (in MiB) (10-4000)"
read driveSize
echo "How would you like to name that drive?"
@ -622,10 +668,10 @@ function createDrive() {
driveSize=$1
driveName=$2
mkdir -p "$VIRTUAL_DRIVER_PATH"
drivePath="${VIRTUAL_DRIVER_PATH}/${driveSize}MB.hda"
drivePath="${VIRTUAL_DRIVER_PATH}/${driveSize}M.hda"
if [ ! -f "$drivePath" ]; then
echo "Creating a ${driveSize}MB Drive"
echo "Creating a ${driveSize}MiB Drive"
truncate --size "${driveSize}m" "$drivePath"
echo "Formatting drive with HFS"
@ -647,23 +693,31 @@ function setupWiredNetworking() {
echo "WARNING: If you continue, the IP address of your Pi may change upon reboot."
echo "Please make sure you will not lose access to the Pi system."
echo ""
echo "Do you want to proceed with network configuration using the default settings? [Y/n]"
read REPLY
if [ "$REPLY" == "N" ] || [ "$REPLY" == "n" ]; then
echo "Available wired interfaces on this system:"
echo `ip -o addr show scope link | awk '{split($0, a); print $2}' | grep eth`
echo "Please type the wired interface you want to use and press Enter:"
read SELECTED
LAN_INTERFACE=$SELECTED
if [[ -z $HEADLESS ]]; then
echo "Do you want to proceed with network configuration using the default settings? [Y/n]"
read REPLY
if [ "$REPLY" == "N" ] || [ "$REPLY" == "n" ]; then
echo "Available wired interfaces on this system:"
echo `ip -o addr show scope link | awk '{split($0, a); print $2}' | grep eth`
echo "Please type the wired interface you want to use and press Enter:"
read SELECTED
LAN_INTERFACE=$SELECTED
fi
fi
if [ "$(grep -c "^denyinterfaces" /etc/dhcpcd.conf)" -ge 1 ]; then
echo "WARNING: Network forwarding may already have been configured. Proceeding will overwrite the configuration."
echo "Press enter to continue or CTRL-C to exit"
read REPLY
if [[ -z $HEADLESS ]]; then
echo "Press enter to continue or CTRL-C to exit"
read REPLY
fi
sudo sed -i /^denyinterfaces/d /etc/dhcpcd.conf
fi
sudo bash -c 'echo "denyinterfaces '$LAN_INTERFACE'" >> /etc/dhcpcd.conf'
echo "Modified /etc/dhcpcd.conf"
@ -676,6 +730,12 @@ function setupWiredNetworking() {
echo "Either use the Web UI, or do this on the command line (assuming SCSI ID 6):"
echo "rasctl -i 6 -c attach -t scdp -f $LAN_INTERFACE"
echo ""
if [[ $HEADLESS ]]; then
echo "Skipping reboot in headless mode"
return 0
fi
echo "We need to reboot your Pi"
echo "Press Enter to reboot or CTRL-C to exit"
read
@ -773,6 +833,20 @@ function installNetatalk() {
NETATALK_VERSION="2-220801"
AFP_SHARE_PATH="$HOME/afpshare"
AFP_SHARE_NAME="Pi File Server"
NETATALK_CONFIG_PATH="/etc/netatalk"
if [ -d "$NETATALK_CONFIG_PATH" ]; then
echo
echo "WARNING: Netatalk configuration dir $NETATALK_CONFIG_PATH already exists."
echo "This installation process will overwrite existing Netatalk applications and configurations."
echo "No shared files will be deleted, but you may have to manually restore your settings after the installation."
echo
echo "Do you want to proceed with the installation? [y/N]"
read -r REPLY
if ! [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
exit 0
fi
fi
echo "Downloading netatalk-$NETATALK_VERSION to $HOME"
cd $HOME || exit 1
@ -783,6 +857,39 @@ function installNetatalk() {
./debian_install.sh -j="${CORES:-1}" -n="$AFP_SHARE_NAME" -p="$AFP_SHARE_PATH" || exit 1
}
# Appends the images dir as a shared Netatalk volume
function shareImagesWithNetatalk() {
APPLEVOLUMES_PATH="/etc/netatalk/AppleVolumes.default"
if ! [ -f "$APPLEVOLUMES_PATH" ]; then
echo "Could not find $APPLEVOLUMES_PATH ... is Netatalk installed?"
exit 1
fi
if [ "$(grep -c "$VIRTUAL_DRIVER_PATH" "$APPLEVOLUMES_PATH")" -ge 1 ]; then
echo "The $VIRTUAL_DRIVER_PATH dir is already shared in $APPLEVOLUMES_PATH"
echo "Do you want to turn off the sharing? [y/N]"
read -r REPLY
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
sudo systemctl stop afpd
sudo sed -i '\,^'"$VIRTUAL_DRIVER_PATH"',d' "$APPLEVOLUMES_PATH"
echo "Sharing for $VIRTUAL_DRIVER_PATH disabled!"
sudo systemctl start afpd
exit 0
fi
exit 0
fi
sudo systemctl stop afpd
echo "Appended to AppleVolumes.default:"
echo "$VIRTUAL_DRIVER_PATH \"RaSCSI Images\"" | sudo tee -a "$APPLEVOLUMES_PATH"
sudo systemctl start afpd
echo
echo "WARNING: Do not inadvertently move or rename image files that are in use by RaSCSI."
echo "Doing so may lead to data loss."
echo
}
# Downloads, compiles, and installs Macproxy (web proxy)
function installMacproxy {
PORT=5000
@ -1146,9 +1253,9 @@ function runChoice() {
echo "Installing / Updating RaSCSI OLED Screen - Complete!"
;;
4)
echo "Creating an HFS formatted 600MB drive image with LIDO driver"
createDrive600MB
echo "Creating an HFS formatted 600MB drive image with LIDO driver - Complete!"
echo "Creating an HFS formatted 600 MiB drive image with LIDO driver"
createDrive600M
echo "Creating an HFS formatted 600 MiB drive image with LIDO driver - Complete!"
;;
5)
echo "Creating an HFS formatted drive image with LIDO driver"
@ -1199,8 +1306,9 @@ function runChoice() {
echo "- Install manpages to /usr/local/man"
sudoCheck
createImagesDir
configureTokenAuth
updateRaScsiGit
installPackages
installPackagesStandalone
stopRaScsi
compileRaScsi
installRaScsi
@ -1218,11 +1326,13 @@ function runChoice() {
echo "- Create a self-signed certificate in /etc/ssl"
sudoCheck
createCfgDir
configureTokenAuth
updateRaScsiGit
installPackages
preparePythonCommon
cachePipPackages
installRaScsiWebInterface
enableWebInterfaceAuth
echo "Configuring RaSCSI Web Interface stand-alone - Complete!"
echo "Launch the Web Interface with the 'start.sh' script. To use a custom port for the web server: 'start.sh --web-port=8081"
;;
@ -1239,6 +1349,10 @@ function runChoice() {
showRaScsiCtrlBoardStatus
echo "Installing / Updating RaSCSI Control Board UI - Complete!"
;;
13)
shareImagesWithNetatalk
echo "Configuring AppleShare File Server - Complete!"
;;
-h|--help|h|help)
showMenu
;;
@ -1252,8 +1366,8 @@ function runChoice() {
function readChoice() {
choice=-1
until [ $choice -ge "0" ] && [ $choice -le "12" ]; do
echo -n "Enter your choice (0-12) or CTRL-C to exit: "
until [ $choice -ge "0" ] && [ $choice -le "13" ]; do
echo -n "Enter your choice (0-13) or CTRL-C to exit: "
read -r choice
done
@ -1270,8 +1384,8 @@ function showMenu() {
echo " 3) install or update RaSCSI OLED Screen (requires hardware)"
echo "CREATE HFS FORMATTED (MAC) IMAGE WITH LIDO DRIVERS"
echo "** For the Mac Plus, it's better to create an image through the Web Interface **"
echo " 4) 600MB drive (suggested size)"
echo " 5) custom drive size (up to 4000MB)"
echo " 4) 600 MiB drive (suggested size)"
echo " 5) custom drive size (up to 4000 MiB)"
echo "NETWORK BRIDGE ASSISTANT"
echo " 6) configure network bridge for Ethernet (DHCP)"
echo " 7) configure network bridge for WiFi (static IP + NAT)"
@ -1283,6 +1397,7 @@ function showMenu() {
echo " 11) configure the RaSCSI Web Interface stand-alone"
echo "EXPERIMENTAL FEATURES"
echo " 12) install or update RaSCSI Control Board UI (requires hardware)"
echo " 13) share the images dir over AppleShare (requires Netatalk)"
}
# parse arguments passed to the script
@ -1291,24 +1406,41 @@ while [ "$1" != "" ]; do
VALUE=$(echo "$1" | awk -F= '{print $2}')
case $PARAM in
-c | --connect_type)
if ! [[ $VALUE =~ ^(FULLSPEC|STANDARD|AIBOM|GAMERNIUM)$ ]]; then
echo "ERROR: The connect type parameter must have a value of: FULLSPEC, STANDARD, AIBOM or GAMERNIUM"
exit 1
fi
CONNECT_TYPE=$VALUE
;;
-r | --run_choice)
if ! [[ $VALUE =~ ^[1-9][0-9]?$ && $VALUE -ge 1 && $VALUE -le 12 ]]; then
echo "ERROR: The run choice parameter must have a numeric value between 1 and 12"
exit 1
fi
RUN_CHOICE=$VALUE
;;
-j | --cores)
if ! [[ $VALUE =~ ^[1-9][0-9]?$ ]]; then
echo "ERROR: The cores parameter must have a numeric value of at least 1"
exit 1
fi
CORES=$VALUE
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
exit 1
-t | --token)
if [[ -z $VALUE ]]; then
echo "ERROR: The token parameter cannot be empty"
exit 1
fi
TOKEN=$VALUE
;;
esac
case $VALUE in
FULLSPEC | STANDARD | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12)
-s | --skip-token)
SKIP_TOKEN=1
;;
-h | --headless)
HEADLESS=1
;;
*)
echo "ERROR: unknown option \"$VALUE\""
echo "ERROR: Unknown parameter \"$PARAM\""
exit 1
;;
esac

View File

@ -1,2 +1,2 @@
protobuf==3.19.3
protobuf==3.19.5
requests==2.26.0

View File

@ -23,4 +23,7 @@ ARCHIVE_FILE_SUFFIXES = [
# The RESERVATIONS list is used to keep track of the reserved ID memos.
# Initialize with a list of 8 empty strings.
RESERVATIONS = ["" for x in range(0, 8)]
RESERVATIONS = ["" for _ in range(0, 8)]
# Standard error message for shell commands
SHELL_ERROR = "Shell command: \"%s\" led to error: %s"

View File

@ -6,22 +6,33 @@ import os
import logging
import asyncio
from functools import lru_cache
from pathlib import PurePath
from pathlib import PurePath, Path
from zipfile import ZipFile, is_zipfile
from time import time
from subprocess import run, CalledProcessError
from json import dump, load
from shutil import copyfile
from urllib.parse import quote
import requests
import rascsi_interface_pb2 as proto
from rascsi.common_settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, ARCHIVE_FILE_SUFFIXES, RESERVATIONS
from rascsi.common_settings import (
CFG_DIR,
CONFIG_FILE_SUFFIX,
PROPERTIES_SUFFIX,
ARCHIVE_FILE_SUFFIXES,
RESERVATIONS,
SHELL_ERROR,
)
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.return_codes import ReturnCodes
from rascsi.socket_cmds import SocketCmds
from util import unarchiver
FILE_READ_ERROR = "Unhandled exception when reading file: %s"
FILE_WRITE_ERROR = "Unhandled exception when writing to file: %s"
URL_SAFE = "/:?&"
class FileCmds:
"""
@ -94,7 +105,9 @@ class FileCmds:
for file in result.image_files_info.image_files:
# Add properties meta data for the image, if applicable
if file.name in prop_files:
process = self.read_drive_properties(f"{CFG_DIR}/{file.name}.{PROPERTIES_SUFFIX}")
process = self.read_drive_properties(
Path(CFG_DIR) / f"{file.name}.{PROPERTIES_SUFFIX}"
)
prop = process["conf"]
else:
prop = False
@ -148,7 +161,7 @@ class FileCmds:
command.params["token"] = self.token
command.params["locale"] = self.locale
command.params["file"] = file_name + "." + file_type
command.params["file"] = f"{file_name}.{file_type}"
command.params["size"] = str(size)
command.params["read_only"] = "false"
@ -216,14 +229,15 @@ class FileCmds:
# noinspection PyMethodMayBeStatic
def delete_file(self, file_path):
"""
Takes (str) file_path with the full path to the file to delete
Takes (Path) file_path for the file to delete
Returns (dict) with (bool) status and (str) msg
"""
parameters = {
"file_path": file_path
}
if os.path.exists(file_path):
os.remove(file_path)
if file_path.exists():
file_path.unlink()
return {
"status": True,
"return_code": ReturnCodes.DELETEFILE_SUCCESS,
@ -238,14 +252,16 @@ class FileCmds:
# noinspection PyMethodMayBeStatic
def rename_file(self, file_path, target_path):
"""
Takes (str) file_path and (str) target_path
Takes:
- (Path) file_path for the file to rename
- (Path) target_path for the name to rename
Returns (dict) with (bool) status and (str) msg
"""
parameters = {
"target_path": target_path
}
if os.path.exists(PurePath(target_path).parent):
os.rename(file_path, target_path)
if target_path.parent.exists:
file_path.rename(target_path)
return {
"status": True,
"return_code": ReturnCodes.RENAMEFILE_SUCCESS,
@ -260,14 +276,16 @@ class FileCmds:
# noinspection PyMethodMayBeStatic
def copy_file(self, file_path, target_path):
"""
Takes (str) file_path and (str) target_path
Takes:
- (Path) file_path for the file to copy from
- (Path) target_path for the name to copy to
Returns (dict) with (bool) status and (str) msg
"""
parameters = {
"target_path": target_path
}
if os.path.exists(PurePath(target_path).parent):
copyfile(file_path, target_path)
if target_path.parent.exists:
copyfile(str(file_path), str(target_path))
return {
"status": True,
"return_code": ReturnCodes.WRITEFILE_SUCCESS,
@ -305,21 +323,22 @@ class FileCmds:
properties_files_moved = []
if move_properties_files_to_config:
for file in extract_result["extracted"]:
if file.get("name").endswith(".properties"):
if file.get("name").endswith(f".{PROPERTIES_SUFFIX}"):
prop_path = Path(CFG_DIR) / file["name"]
if (self.rename_file(
file["absolute_path"],
f"{CFG_DIR}/{file['name']}"
Path(file["absolute_path"]),
prop_path,
)):
properties_files_moved.append({
"status": True,
"name": file["path"],
"path": f"{CFG_DIR}/{file['name']}",
"path": str(prop_path),
})
else:
properties_files_moved.append({
"status": False,
"name": file["path"],
"path": f"{CFG_DIR}/{file['name']}",
"path": str(prop_path),
})
return {
@ -362,7 +381,7 @@ class FileCmds:
tmp_full_path = tmp_dir + file_name
iso_filename = f"{server_info['image_dir']}/{file_name}.iso"
req_proc = self.download_to_dir(url, tmp_dir, file_name)
req_proc = self.download_to_dir(quote(url, safe=URL_SAFE), tmp_dir, file_name)
if not req_proc["status"]:
return {"status": False, "msg": req_proc["msg"]}
@ -386,7 +405,7 @@ class FileCmds:
"%s was successfully unzipped. Deleting the zipfile.",
tmp_full_path,
)
self.delete_file(tmp_full_path)
self.delete_file(Path(tmp_full_path))
try:
run(
@ -401,8 +420,7 @@ class FileCmds:
check=True,
)
except CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
return {"status": False, "msg": error.stderr.decode("utf-8")}
parameters = {
@ -424,7 +442,11 @@ class FileCmds:
logging.info("Making a request to download %s", url)
try:
with requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) as req:
with requests.get(
quote(url, safe=URL_SAFE),
stream=True,
headers={"User-Agent": "Mozilla/5.0"},
) as req:
req.raise_for_status()
with open(f"{save_dir}/{file_name}", "wb") as download:
for chunk in req.iter_content(chunk_size=8192):
@ -452,9 +474,9 @@ class FileCmds:
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
file_name = f"{CFG_DIR}/{file_name}"
file_path = f"{CFG_DIR}/{file_name}"
try:
with open(file_name, "w", encoding="ISO-8859-1") as json_file:
with open(file_path, "w", encoding="ISO-8859-1") as json_file:
version = self.ractl.get_server_info()["version"]
devices = self.ractl.list_devices()["device_list"]
for device in devices:
@ -485,7 +507,7 @@ class FileCmds:
indent=4
)
parameters = {
"target_path": file_name
"target_path": file_path
}
return {
"status": True,
@ -494,33 +516,28 @@ class FileCmds:
}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
self.delete_file(file_name)
self.delete_file(Path(file_path))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not write to file: %s", file_name)
self.delete_file(file_name)
parameters = {
"file_name": file_name
}
return {
"status": False,
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE,
"parameters": parameters,
}
logging.error(FILE_WRITE_ERROR, file_name)
self.delete_file(Path(file_path))
raise
def read_config(self, file_name):
"""
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
file_name = f"{CFG_DIR}/{file_name}"
file_path = Path(CFG_DIR) / file_name
try:
with open(file_name, encoding="ISO-8859-1") as json_file:
with open(file_path, encoding="ISO-8859-1") as json_file:
config = load(json_file)
# If the config file format changes again in the future,
# introduce more sophisticated format detection logic here.
if isinstance(config, dict):
self.ractl.detach_all()
for scsi_id in range(0, 8):
RESERVATIONS[scsi_id] = ""
ids_to_reserve = []
for item in config["reserved_ids"]:
ids_to_reserve.append(item["id"])
@ -575,15 +592,8 @@ class FileCmds:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not read file: %s", file_name)
parameters = {
"file_name": file_name
}
return {
"status": False,
"return_code": ReturnCodes.READCONFIG_COULD_NOT_READ,
"parameters": parameters
}
logging.error(FILE_READ_ERROR, str(file_path))
raise
def write_drive_properties(self, file_name, conf):
"""
@ -591,12 +601,12 @@ class FileCmds:
Takes file name base (str) and (list of dicts) conf as arguments
Returns (dict) with (bool) status and (str) msg
"""
file_path = f"{CFG_DIR}/{file_name}"
file_path = Path(CFG_DIR) / file_name
try:
with open(file_path, "w") as json_file:
dump(conf, json_file, indent=4)
parameters = {
"target_path": file_path
"target_path": str(file_path)
}
return {
"status": True,
@ -608,29 +618,22 @@ class FileCmds:
self.delete_file(file_path)
return {"status": False, "msg": str(error)}
except:
logging.error("Could not write to file: %s", file_path)
logging.error(FILE_WRITE_ERROR, str(file_path))
self.delete_file(file_path)
parameters = {
"target_path": file_path
}
return {
"status": False,
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE,
"parameters": parameters,
}
raise
# noinspection PyMethodMayBeStatic
def read_drive_properties(self, file_path):
"""
Reads drive properties from json formatted file.
Takes (str) file_path as argument.
Takes (Path) file_path as argument.
Returns (dict) with (bool) status, (str) msg, (dict) conf
"""
try:
with open(file_path) as json_file:
conf = load(json_file)
parameters = {
"file_path": file_path
"file_path": str(file_path)
}
return {
"status": True,
@ -642,15 +645,8 @@ class FileCmds:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error("Could not read file: %s", file_path)
parameters = {
"file_path": file_path
}
return {
"status": False,
"return_codes": ReturnCodes.READDRIVEPROPS_COULD_NOT_READ,
"parameters": parameters,
}
logging.error(FILE_READ_ERROR, str(file_path))
raise
# noinspection PyMethodMayBeStatic
async def run_async(self, program, args):
@ -685,5 +681,6 @@ class FileCmds:
"""
try:
return unarchiver.inspect_archive(file_path)
except (unarchiver.LsarCommandError, unarchiver.LsarOutputError):
except (unarchiver.LsarCommandError, unarchiver.LsarOutputError) as error:
logging.error(str(error))
raise

View File

@ -40,7 +40,7 @@ class RaCtlCmds:
version = (str(result.server_info.version_info.major_version) + "." +
str(result.server_info.version_info.minor_version) + "." +
str(result.server_info.version_info.patch_version))
log_levels = result.server_info.log_level_info.log_levels
log_levels = list(result.server_info.log_level_info.log_levels)
current_log_level = result.server_info.log_level_info.current_log_level
reserved_ids = list(result.server_info.reserved_ids_info.ids)
image_dir = result.server_info.image_files_info.default_image_folder
@ -48,15 +48,12 @@ class RaCtlCmds:
# Creates lists of file endings recognized by RaSCSI
mappings = result.server_info.mapping_info.mapping
sahd = []
schd = []
scrm = []
scmo = []
sccd = []
for dtype in mappings:
if mappings[dtype] == proto.PbDeviceType.SAHD:
sahd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCHD:
if mappings[dtype] == proto.PbDeviceType.SCHD:
schd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCRM:
scrm.append(dtype)
@ -73,7 +70,6 @@ class RaCtlCmds:
"reserved_ids": reserved_ids,
"image_dir": image_dir,
"scan_depth": scan_depth,
"sahd": sahd,
"schd": schd,
"scrm": scrm,
"scmo": scmo,
@ -117,7 +113,7 @@ class RaCtlCmds:
result = proto.PbResult()
result.ParseFromString(data)
ifs = result.network_interfaces_info.name
return {"status": result.status, "ifs": ifs}
return {"status": result.status, "ifs": list(ifs)}
def get_device_types(self):
"""
@ -144,7 +140,7 @@ class RaCtlCmds:
"removable": device.properties.removable,
"supports_file": device.properties.supports_file,
"params": params,
"block_sizes": device.properties.block_sizes,
"block_sizes": list(device.properties.block_sizes),
}
return {"status": result.status, "device_types": device_types}
@ -228,16 +224,13 @@ class RaCtlCmds:
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if "device_type" in kwargs.keys():
if kwargs["device_type"]:
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
if "unit" in kwargs.keys():
if kwargs["unit"]:
devices.unit = kwargs["unit"]
if "params" in kwargs.keys():
if isinstance(kwargs["params"], dict):
for param in kwargs["params"]:
devices.params[param] = kwargs["params"][param]
if kwargs.get("device_type"):
devices.type = proto.PbDeviceType.Value(str(kwargs["device_type"]))
if kwargs.get("unit"):
devices.unit = kwargs["unit"]
if kwargs.get("params") and isinstance(kwargs["params"], dict):
for param in kwargs["params"]:
devices.params[param] = kwargs["params"][param]
# Handling the inserting of media into an attached removable type device
device_type = kwargs.get("device_type", None)
@ -264,18 +257,14 @@ class RaCtlCmds:
# Handling attaching a new device
else:
command.operation = proto.PbOperation.ATTACH
if "vendor" in kwargs.keys():
if kwargs["vendor"]:
devices.vendor = kwargs["vendor"]
if "product" in kwargs.keys():
if kwargs["product"]:
devices.product = kwargs["product"]
if "revision" in kwargs.keys():
if kwargs["revision"]:
devices.revision = kwargs["revision"]
if "block_size" in kwargs.keys():
if kwargs["block_size"]:
devices.block_size = int(kwargs["block_size"])
if kwargs.get("vendor"):
devices.vendor = kwargs["vendor"]
if kwargs.get("product"):
devices.product = kwargs["product"]
if kwargs.get("revision"):
devices.revision = kwargs["revision"]
if kwargs.get("block_size"):
devices.block_size = int(kwargs["block_size"])
command.devices.append(devices)
@ -398,7 +387,7 @@ class RaCtlCmds:
dpath = result.devices_info.devices[i].file.name
dfile = dpath.replace(image_files_info["images_dir"] + "/", "")
dparam = result.devices_info.devices[i].params
dparam = dict(result.devices_info.devices[i].params)
dven = result.devices_info.devices[i].vendor
dprod = result.devices_info.devices[i].product
drev = result.devices_info.devices[i].revision

View File

@ -8,6 +8,7 @@ from shutil import disk_usage
from re import findall, match
from socket import socket, gethostname, AF_INET, SOCK_DGRAM
from rascsi.common_settings import SHELL_ERROR
class SysCmds:
"""
@ -32,8 +33,7 @@ class SysCmds:
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
logging.warning(SHELL_ERROR, error.cmd, error.stderr.decode("utf-8"))
ra_git_version = ""
try:
@ -47,9 +47,8 @@ class SysCmds:
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
pi_version = "Unknown"
logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
pi_version = "?"
return {"git": ra_git_version, "env": pi_version}
@ -70,8 +69,7 @@ class SysCmds:
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
logging.warning(SHELL_ERROR, error.cmd, error.stderr.decode("utf-8"))
processes = ""
matching_processes = findall(daemon, processes)
@ -93,8 +91,7 @@ class SysCmds:
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
logging.warning(SHELL_ERROR, error.cmd, error.stderr.decode("utf-8"))
bridges = ""
if "rascsi_bridge" in bridges:
@ -161,7 +158,36 @@ class SysCmds:
process = run(
["journalctl"] + line_param + scope_param,
capture_output=True,
check=True,
)
if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8")
return process.returncode, process.stderr.decode("utf-8")
@staticmethod
def get_diskinfo(file_path):
"""
Takes (str) file_path path to image file to inspect.
Returns either the disktype output, or the stderr output.
"""
process = run(
["disktype", file_path],
capture_output=True,
)
if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8")
return process.returncode, process.stderr.decode("utf-8")
@staticmethod
def get_manpage(file_path):
"""
Takes (str) file_path path to image file to generate manpage for.
Returns either the man2html output, or the stderr output.
"""
process = run(
["man2html", file_path, "-M", "/"],
capture_output=True,
)
if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8")

View File

@ -10,6 +10,7 @@ import pathlib
from tempfile import TemporaryDirectory
from re import escape, match
from json import loads, JSONDecodeError
from shutil import move
from util.run import run
FORK_OUTPUT_TYPE_VISIBLE = "visible"
@ -68,7 +69,7 @@ def extract_archive(file_path, **kwargs):
unar_result_success = r'^Successfully extracted to "(?P<destination>.+)".$'
unar_result_no_files = "No files extracted."
unar_file_extracted = \
r"^ (?P<path>.+). \(((?P<size>[0-9]+) B)?(?P<types>(dir)?(, )?(rsrc)?)\)\.\.\. (?P<status>[A-Z]+)\.$"
r"^ {2}(?P<path>.+). \(((?P<size>\d+) B)?(?P<types>(dir)?(, )?(rsrc)?)\)\.\.\. (?P<status>[A-Z]+)\.$"
lines = process["stdout"].rstrip("\n").split("\n")
@ -140,7 +141,7 @@ def extract_archive(file_path, **kwargs):
# The parent dir may not be specified as a member, so ensure it exists
target_path.parent.mkdir(parents=True, exist_ok=True)
logging.debug("Moving temp file: %s -> %s", source_path, target_path)
source_path.rename(target_path)
move(str(source_path), str(target_path))
moved.append(member)
return {
@ -151,7 +152,7 @@ def extract_archive(file_path, **kwargs):
raise UnarUnexpectedOutputError(lines[-1])
def inspect_archive(file_path, **kwargs):
def inspect_archive(file_path):
"""
Calls `lsar` to inspect the contents of an archive
Takes (str) file_path

View File

@ -4,7 +4,7 @@
luma-oled==3.8.1
Pillow==9.0.1
RPi.GPIO==0.7.0
protobuf==3.19.3
protobuf==3.19.5
unidecode==1.3.2
smbus==1.1.post2

View File

@ -247,7 +247,7 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
device_type = device_info["device_list"][0]["device_type"]
image = device_info["device_list"][0]["image"]
if device_type in ("SAHD", "SCHD", "SCBR", "SCDP", "SCLP", "SCHS"):
if device_type in ("SCHD", "SCBR", "SCDP", "SCLP", "SCHS"):
result = self.ractl_cmd.detach_by_id(scsi_id)
if result["status"] is True:
self.show_id_action_message(scsi_id, "detached")

View File

@ -12,5 +12,5 @@ pyusb==1.2.1
rpi-ws281x==4.3.0
RPi.GPIO==0.7.0
sysv-ipc==1.1.0
protobuf==3.19.1
protobuf==3.19.5
unidecode==1.3.2

2
python/web/.flake8 Normal file
View File

@ -0,0 +1,2 @@
[flake8]
max-line-length = 100

View File

@ -0,0 +1,8 @@
[tool.pytest.ini_options]
addopts = "--junitxml=report.xml"
log_cli = true
log_cli_level = "warn"
[tool.black]
line-length = 100
target-version = ['py37', 'py38', 'py39']

View File

@ -0,0 +1,6 @@
pytest==7.1.3
pytest-httpserver==1.0.6
black==22.8.0
flake8==5.0.4
watchdog==2.1.9
requests==2.28.1

View File

@ -4,7 +4,7 @@ Flask==2.0.1
itsdangerous==2.0.1
Jinja2==3.0.1
MarkupSafe==2.0.1
protobuf==3.17.3
protobuf==3.20.1
requests==2.26.0
simplepam==0.1.5
flask_babel==2.0.0

View File

@ -1,4 +1,5 @@
<html>
<!doctype html>
<html lang="en">
<head>
<title>RaSCSI-Web is Starting</title>
<meta http-equiv="refresh" content="2">

View File

@ -7,8 +7,8 @@
"block_size": 512,
"size": 52445184,
"name": "DEC RZ22",
"file_type": "hds",
"description": "Page/Swap drive for satellite workstations",
"file_type": "hd1",
"description": "Page/Swap drive for satellite workstations (SCSI-1)",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
@ -19,8 +19,8 @@
"block_size": 512,
"size": 104890368,
"name": "DEC RZ23",
"file_type": "hds",
"description": "Smallest usable drive for OpenVMS/VAX",
"file_type": "hd1",
"description": "Smallest usable drive for OpenVMS/VAX (SCSI-1)",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
@ -31,8 +31,8 @@
"block_size": 512,
"size": 209813504,
"name": "DEC RZ24",
"file_type": "hds",
"description": "Smallest usable drive for OpenVMS/VAX + Motif",
"file_type": "hd1",
"description": "Smallest usable drive for OpenVMS/VAX + Motif (SCSI-1)",
"url": "http://lastin.dti.supsi.ch/VET/disks/EK-RZXXD-PS.pdf"
},
{
@ -426,19 +426,19 @@
"revision": "1.0k",
"block_size": 2048,
"size": null,
"name": "Apple CD 600e",
"name": "AppleCD 600e (Matsushita CR-8005)",
"file_type": null,
"description": "Emulates Apple CD ROM drive for use with Macintosh computers.",
"description": "Emulates an Apple CD-ROM drive for use with Macintosh computers.",
"url": ""
},
{
"device_type": "SCCD",
"vendor": null,
"product": null,
"product": "SCSI CD-ROM 512",
"revision": null,
"block_size": 512,
"size": null,
"name": "Generic CD-ROM 512 block size",
"name": "Generic CD-ROM block size 512",
"file_type": null,
"description": "For use with host systems that expect the non-standard 512 byte block size for CD-ROM drives, such as Akai samplers.",
"url": ""

View File

@ -2,19 +2,7 @@ body {
color: black;
background-color: white;
font-family: Arial, Helvetica, sans-serif;
text-decoration:none;
}
h1 {
color: white;
font-size:20px;
background-color:black;
}
h2 {
color: black;
font-size:16px;
margin: 0px;
text-decoration: none;
}
a {
@ -27,90 +15,150 @@ form {
table, tr, td {
border: 1px solid black;
border-collapse:collapse;
border-collapse: collapse;
margin: none;
}
.error {
h1 {
color: white;
font-size:20px;
background-color:red;
white-space: pre-line;
font-size: 20px;
}
.message {
color: white;
font-size:20px;
background-color:green;
white-space: pre-line;
}
td.inactive {
text-align:center;
background-color:tan;
h2 {
color: black;
font-size: large;
font-weight: bold;
margin: 3px;
}
summary.heading {
color: black;
font-size: large;
font-weight: bold;
margin: 0px;
margin: 3px;
}
div.header {
color: white;
background-color: black;
}
div.footer {
font-family: monospace;
}
div.logged_in {
background-color: green;
}
div.logged_out {
background-color: red;
}
input.lun {
width: 36px;
}
div.flash {
margin-top: 5px;
margin-bottom: 5px;
}
div.flash div {
color: white;
font-size: 18px;
white-space: pre-line;
padding: 2px 5px;
}
div.flash div.success {
background-color: green;
}
div.flash div.warning {
background-color: orange;
color: black;
}
div.flash div.error {
background-color: red;
}
div.flash div.info {
background-color: #0d6efd;
}
td.inactive {
text-align: center;
background-color: tan;
}
ul.inline_list {
list-style: none;
}
.dropzone, .dropzone * {
box-sizing: border-box;
box-sizing: border-box;
}
.dropzone {
position: relative;
position: relative;
}
.dropzone .dz-button {
position: relative;
background-color: white;
color: black;
border: 2px dashed blue;
padding: 12px 28px;
}
.dropzone .dz-preview {
position: relative;
display: inline-block;
width: 120px;
margin: .5em;
position: relative;
display: inline-block;
width: 120px;
margin: .5em;
}
.dropzone .dz-preview .dz-progress {
display: block;
height: 15px;
border: 1px solid #aaa;
display: block;
height: 15px;
border: 1px solid #aaa;
}
.dropzone .dz-preview .dz-progress .dz-upload {
display: block;
height: 100%;
width: 0;
background: green;
display: block;
height: 100%;
width: 0;
background: green;
}
.dropzone .dz-preview .dz-error-message {
color: red;
display: none;
color: red;
display: none;
}
.dropzone .dz-preview.dz-error .dz-error-message {
display: block;
display: block;
}
.dropzone .dz-preview.dz-error .dz-error-mark {
display: block;
filter: drop-shadow(0px 0px 2px red);
display: block;
filter: drop-shadow(0px 0px 2px red);
opacity: 0;
}
.dropzone .dz-preview.dz-success .dz-success-mark {
display: block;
filter: drop-shadow(0px 0px 2px green);
display: block;
filter: drop-shadow(0px 0px 2px green);
}
.dropzone .dz-preview .dz-error-mark, .dropzone .dz-preview .dz-success-mark {
position: absolute;
display: none;
left: 30px;
top: 30px;
width: 54px;
height: 58px;
left: 50%;
margin-left: -27px;
position: absolute;
display: none;
top: 30px;
width: 54px;
height: 58px;
left: 50%;
margin-left: -27px;
}

View File

@ -1,80 +1,76 @@
<!doctype html>
<html>
<html lang="{{ env["locale"] }}">
<head>
<title>{{ _("RaSCSI Reloaded Control Page") }} [{{ host }}]</title>
<title>{{ _("RaSCSI Reloaded Control Page") }} [{{ env["host"] }}]</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="apple-touch-icon" sizes="57x57" href="/pwa/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/pwa/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/pwa/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/pwa/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/pwa/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/pwa/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/pwa/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/pwa/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/pwa/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/pwa/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/pwa/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/pwa/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/pwa/favicon-16x16.png">
<link rel="manifest" href="/pwa/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/pwa/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="apple-touch-icon" sizes="57x57" href="/pwa/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/pwa/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/pwa/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/pwa/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/pwa/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/pwa/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/pwa/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/pwa/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/pwa/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/pwa/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/pwa/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/pwa/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/pwa/favicon-16x16.png">
<link rel="manifest" href="/pwa/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/pwa/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script type="application/javascript">
var processNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class='message'>" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div>";
window.scrollTo(0,0);
}
<script type="application/javascript">
var processNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class=\"info\">" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div>";
window.scrollTo(0,0);
}
var shutdownNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class='message'>" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div>";
window.scrollTo(0,0);
}
</script>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
var shutdownNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class=\"info\">" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div>";
window.scrollTo(0,0);
}
</script>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
</head>
<body>
<div class="content">
<div class="header">
{% if auth_active %}
{% if username %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Logged in as <em>%(username)s</em>", username=username) }} &#8211; <a href="/logout">{{ _("Log Out") }}</a></span>
{% if env["auth_active"] %}
{% if env["username"] %}
<div align="center" class="logged_in">
{{ _("Logged in as <em>%(username)s</em>", username=env["username"]) }} - <a href="/logout">{{ _("Log Out") }}</a>
</div>
{% else %}
<span style="display: inline-block; width: 100%; color: white; background-color: red; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">
<div align="center" class="logged_out">
<form method="POST" action="/login">
<div>{{ _("Log In to Use Web Interface") }}</div>
<input type="text" name="username" placeholder="{{ _("Username") }}">
<input type="password" name="password" placeholder="{{ _("Password") }}">
<label for="username">{{ _("Username") }}</label>
<input type="text" name="username" id="username">
<label for="password">{{ _("Password") }}</label>
<input type="password" name="password" id="password">
<input type="submit" value="Login">
</form>
</span>
</div>
{% endif %}
{% else %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Web Interface Authentication Disabled") }} &#8211; {{ _("See <a href=\"%(url)s\" target=\"_blank\">Wiki</a> for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }}</span>
<div align="center" class="logged_out">
{{ _("Web Interface Authentication Disabled") }} - {{ _("See <a href=\"%(url)s\" target=\"_blank\">Wiki</a> for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }}
</div>
{% endif %}
<table width="100%" style="background-color: black;">
<tbody>
<tr align="center">
<td>
<a href="http://github.com/akuker/RASCSI" target="_blank">
<h1>{{ _("RaSCSI Reloaded Control Page") }}</h1>
</a>
</td>
</tr>
<tr>
<td style="color: white;">
hostname: {{ host }} ip: {{ ip_addr }}
</td>
</tr>
</tbody>
</table>
<div align="center">
<a href="/">
<h1>{{ _("RaSCSI Reloaded Control Page") }}</h1>
</a>
</div>
<div>
hostname: {{ env["host"] }} ip: {{ env["ip_addr"] }}
</div>
</div>
<div class="flash" id="flash">
{% for category, message in get_flashed_messages(with_categories=true) %}
@ -88,9 +84,27 @@
<div class="content">
{% block content %}{% endblock content %}
</div>
<div class="footer">
<center><tt>{{ _("RaSCSI Reloaded version: ") }}<strong>{{ version }} <a href="https://github.com/akuker/RASCSI/commit/{{ running_env['git'] }}" target="_blank">{{ running_env["git"][:7] }}</a></strong></tt></center>
<center><tt>{{ _("Pi environment: ") }}{{ running_env["env"] }}</tt></center>
<div align="center" class="footer">
<div>
{% if env["netatalk_configured"] == 1 %}
{{ _("The AppleShare server is running. No active connections.") }}
{% endif %}
{% if env["netatalk_configured"] == 2 %}
{{ _("%(value)d active AFP connection", value=(env["netatalk_configured"] - 1)) }}
{% elif env["netatalk_configured"] > 2 %}
{{ _("%(value)d active AFP connections", value=(env["netatalk_configured"] - 1)) }}
{% endif %}
</div>
<div>
{% if env["macproxy_configured"] %}
{{ _("Macproxy is running at %(ip_addr)s (default port 5000)", ip_addr=env['ip_addr']) }}
{% endif %}
</div>
<div>
{{ _("RaSCSI Reloaded version: ") }}<b>{{ env["version"] }} <a href="https://github.com/akuker/RASCSI/commit/{{ env["running_env"]["git"] }}" target="_blank">{{ env["running_env"]["git"][:7] }}</a></b>
</div>
<div>
{{ _("Pi environment: ") }}{{ env["running_env"]["env"] }}
</div>
</div>
</div>
</body>

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block content %}
<h2>{{ _("Detailed Info for Attached Devices") }}</h2>
{% for device in devices %}
<p>
<table border="black" cellpadding="3" summary="Detailed information for attached devices">
<tr>
<th scope="row">{{ _("SCSI ID") }}</th>
<td>{{ device["id"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("LUN") }}</th>
<td>{{ device["unit"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("Type") }}</th>
<td>{{ device["device_type"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("Status") }}</th>
<td>{{ device["status"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("File") }}</th>
<td>{{ device["image"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("Parameters") }}</th>
<td>{{ device["params"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("Vendor") }}</th>
<td>{{ device["vendor"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("Product") }}</th>
<td>{{ device["product"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("Revision") }}</th>
<td>{{ device["revision"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("Block Size") }}</th>
<td>{{ device["block_size"] }}</td>
</tr>
<tr>
<th scope="row">{{ _("Image Size") }}</th>
<td>{{ device["size"] }}</td>
</tr>
</table>
</p>
{% endfor %}
<p><a href="/">{{ _("Go to Home") }}</a></p>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<h2>{{ _("Disk Image Details: %(file_name)s", file_name=file_name) }}</h2>
<p><pre>{{ diskinfo }}</pre></p>
<p><a href="/">{{ _("Go to Home") }}</a></p>
{% endblock content %}

View File

@ -1,43 +1,34 @@
{% extends "base.html" %}
{% block content %}
<p><a href="/">{{ _("Cancel") }}</a></p>
<h2>{{ _("Disclaimer") }}</h2>
<p>{{ _("These device profiles are provided as-is with no guarantee to work equally to the actual physical device they are named after. You may need to provide appropirate device drivers and/or configuration parameters for them to function properly. If you would like to see data modified, or have additional devices to add to the list, please raise an issue ticket at <a href=\"%(url)s\">GitHub</a>.", url="https://github.com/akuker/RASCSI/issues") }}</p>
<h2>{{ _("Hard Drives") }}</h2>
<h2>{{ _("Hard Disk Drives") }}</h2>
<table cellpadding="3" border="black">
<table cellpadding="3" border="black" summary="List of hard drives">
<tbody>
<tr>
<td><b>{{ _("Name") }}</b></td>
<td><b>{{ _("Size (MB)") }}</b></td>
<td><b>{{ _("Description") }}</b></td>
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
<th scope="col">{{ _("Name") }}</th>
<th scope="col">{{ _("Size (MiB)") }}</th>
<th scope="col">{{ _("Description") }}</th>
<th scope="col">{{ _("Action") }}</th>
</tr>
{% for hd in hd_conf|sort(attribute='name') %}
{% for hd in env['drive_properties']['hd_conf']|sort(attribute='name') %}
<tr>
<td style="text-align:center">{{ hd.name }}</td>
<td style="text-align:center">{{ hd.size_mb }}</td>
<td style="text-align:left">{{ hd.description }}</td>
<td style="text-align:left">
{% if hd.url != "" %}
<a href="{{ hd.url }}">{{ _("Link") }}</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<td align="center">
{% if hd.url != "" %}
<a href="{{ hd.url }}">{{ hd.name }}</a>
{% else %}
{{ hd.name }}
{% endif %}
</td>
<td align="center">{{ hd.size_mb }}</td>
<td>{{ hd.description }}</td>
<td>
<form action="/drive/create" method="post">
<input type="hidden" name="vendor" value="{{ hd.vendor }}">
<input type="hidden" name="product" value="{{ hd.product }}">
<input type="hidden" name="revision" value="{{ hd.revision }}">
<input type="hidden" name="blocks" value="{{ hd.blocks }}">
<input type="hidden" name="block_size" value="{{ hd.block_size }}">
{{ _("Size:") }} <input type="number" name="size" min="512" max="274877906944" step="512" value="{{ hd.size }}">{{ _("B") }}
<input type="hidden" name="file_type" value="{{ hd.file_type }}">
<label for="file_name">{{ _("Save as:") }}</label>
<input type="text" name="file_name" value="{{ hd.secure_name }}" required />.{{ hd.file_type }}
<input type="hidden" name="drive_name" value="{{ hd.name }}">
<label for="file_name_{{ hd.name }}">{{ _("Save as:") }}</label>
<input type="text" name="file_name" id="file_name_{{ hd.name }}" value="{{ hd.secure_name }}" required />.{{ hd.file_type }}
<input type="submit" value="{{ _("Create") }}" />
</form>
</td>
@ -48,43 +39,36 @@
<hr/>
<h2>{{ _("CD-ROM Drives") }}</h2>
<p><em>{{ _("This will create a properties file for the given CD-ROM image. No new image file will be created.") }}</em></p>
<table cellpadding="3" border="black">
<h2>{{ _("CD/DVD Drives") }}</h2>
<p><em>{{ _("This will create a properties file for the given CD-ROM or DVD image. No new image file will be created.") }}</em></p>
<table cellpadding="3" border="black" summary="List of CD-ROM or DVD drives">
<tbody>
<tr>
<td><b>{{ _("Name") }}</b></td>
<td><b>{{ _("Size (MB)") }}</b></td>
<td><b>{{ _("Description") }}</b></td>
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
<th scope="col">{{ _("Name") }}</th>
<th scope="col">{{ _("Description") }}</th>
<th scope="col">{{ _("Action") }}</th>
</tr>
{% for cd in cd_conf|sort(attribute='name') %}
{% for cd in env['drive_properties']['cd_conf']|sort(attribute='name') %}
<tr>
<td style="text-align:center">{{ cd.name }}</td>
<td style="text-align:center">{{ cd.size_mb }}</td>
<td style="text-align:left">{{ cd.description }}</td>
<td style="text-align:left">
{% if cd.url != "" %}
<a href="{{ cd.url }}">{{ _("Link") }}</a>
{% else %}
-
{% endif %}
<td align="center">
{% if cd.url != "" %}
<a href="{{ cd.url }}">{{ cd.name }}</a>
{% else %}
{{ cd.name }}
{% endif %}
</td>
<td style="text-align:left">
<td>{{ cd.description }}</td>
<td>
<form action="/drive/cdrom" method="post">
<input type="hidden" name="vendor" value="{{ cd.vendor }}">
<input type="hidden" name="product" value="{{ cd.product }}">
<input type="hidden" name="revision" value="{{ cd.revision }}">
<input type="hidden" name="block_size" value="{{ cd.block_size }}">
<label for="file_name">{{ _("Create for:") }}</label>
<select type="select" name="file_name">
<input type="hidden" name="drive_name" value="{{ cd.name }}">
<label for="file_name_{{ cd.name }}">{{ _("Create for:") }}</label>
<select type="select" name="file_name" id="file_name_{{ cd.name }}">
{% for file in files|sort(attribute='name') %}
{% if file["name"].lower().endswith(cdrom_file_suffix) %}
<option value="{{ file["name"] }}">{{ file["name"].replace(base_dir, '') }}</option>
{% endif %}
{% endfor %}
</select>
{% if file["name"].lower().endswith(env['cd_suffixes']) %}
<option value="{{ file["name"] }}">{{ file["name"].replace(env["image_dir"], '') }}</option>
{% endif %}
{% endfor %}
</select>
<input type="submit" value="{{ _("Create") }}" />
</form>
</td>
@ -95,39 +79,31 @@
<hr/>
<h2>{{ _("Removable Drives") }}</h2>
<table cellpadding="3" border="black">
<h2>{{ _("Removable Disk Drives") }}</h2>
<table cellpadding="3" border="black" summary="List of removable disk drives">
<tbody>
<tr>
<td><b>{{ _("Name") }}</b></td>
<td><b>{{ _("Size (MB)") }}</b></td>
<td><b>{{ _("Description") }}</b></td>
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
<th scope="col">{{ _("Name") }}</th>
<th scope="col">{{ _("Size (MiB)") }}</th>
<th scope="col">{{ _("Description") }}</th>
<th scope="col">{{ _("Action") }}</th>
</tr>
{% for rm in rm_conf|sort(attribute='name') %}
{% for rm in env['drive_properties']['rm_conf']|sort(attribute='name') %}
<tr>
<td style="text-align:center">{{ rm.name }}</td>
<td style="text-align:center">{{ rm.size_mb }}</td>
<td style="text-align:left">{{ rm.description }}</td>
<td style="text-align:left">
{% if rm.url != "" %}
<a href="{{ rm.url }}">{{ _("Link") }}</a>
{% else %}
-
{% endif %}
<td align="center">
{% if rm.url != "" %}
<a href="{{ rm.url }}">{{ rm.name }}</a>
{% else %}
{{ rm.name }}
{% endif %}
</td>
<td style="text-align:left">
<td align="center">{{ rm.size_mb }}</td>
<td>{{ rm.description }}</td>
<td>
<form action="/drive/create" method="post">
<input type="hidden" name="vendor" value="{{ rm.vendor }}">
<input type="hidden" name="product" value="{{ rm.product }}">
<input type="hidden" name="revision" value="{{ rm.revision }}">
<input type="hidden" name="blocks" value="{{ rm.blocks }}">
<input type="hidden" name="block_size" value="{{ rm.block_size }}">
{{ _("Size:") }} <input type="number" name="size" min="512" max="274877906944" step="512" value="{{ rm.size }}">{{ _("B") }}
<input type="hidden" name="file_type" value="{{ rm.file_type }}">
<label for="file_name">{{ _("Save as:") }}</label>
<input type="text" name="file_name" value="{{ rm.secure_name }}" required />.{{ rm.file_type }}
<input type="hidden" name="drive_name" value="{{ rm.name }}">
<label for="file_name_{{ rm.name }}">{{ _("Save as:") }}</label>
<input type="text" name="file_name" id="file_name_{{ rm.name }}" value="{{ rm.secure_name }}" required />.{{ rm.file_type }}
<input type="submit" value="{{ _("Create") }}" />
</form>
</td>
@ -135,7 +111,7 @@
{% endfor %}
</tbody>
</table>
<p><small>{{ _("%(disk_space)s MB disk space remaining on the Pi", disk_space=free_disk) }}</small></p>
<p><a href="/">{{ _("Cancel") }}</a></p>
<p><small>{{ _("%(disk_space)s MiB disk space remaining on the Pi", disk_space=env["free_disk_space"]) }}</small></p>
<p><a href="/">{{ _("Go to Home") }}</a></p>
{% endblock content %}

View File

@ -6,18 +6,19 @@
{{ _("Current RaSCSI Configuration") }}
</summary>
<ul>
<li>{{ _("Displays the currently attached devices for each available SCSI ID.") }}</li>
<li>{{ _("Save and load device configurations, stored as json files in <tt>%(config_dir)s</tt>", config_dir=CFG_DIR) }}</tt></li>
<li>{{ _("Save and load device configurations, stored as json files in <tt>%(config_dir)s</tt>", config_dir=CFG_DIR) }}</li>
<li>{{ _("To have a particular device configuration load when RaSCSI starts, save it as <em>default</em>.") }}</li>
</ul>
</details>
<p><form action="/config/load" method="post">
<select name="name" required="" width="14">
<p>
<form action="/config/load" method="post">
<label for="config_load_name">{{ _("File name") }}</label>
<select name="name" id="config_load_name" required="" width="14">
{% if config_files %}
{% for config in config_files|sort %}
<option value="{{ config }}">
{{ config.replace(".json", '') }}
{{ config }}
</option>
{% endfor %}
{% else %}
@ -28,55 +29,59 @@
</select>
<input name="load" type="submit" value="{{ _("Load") }}" onclick="return confirm('{{ _("Detach all current device and Load configuration?") }}')">
<input name="delete" type="submit" value="{{ _("Delete") }}" onclick="return confirm('{{ _("Delete configuration file?") }}')">
</form></p>
</form>
</p>
<p><form action="/config/save" method="post">
<input name="name" placeholder="default" size="20">
<p>
<form action="/config/save" method="post">
<label for="config_save_name">{{ _("File name") }}</label>
<input name="name" id="config_save_name" placeholder="default" size="20">
.{{ CONFIG_FILE_SUFFIX }}
<input type="submit" value="{{ _("Save") }}">
</form></p>
</form>
</p>
<table border="black" cellpadding="3">
<table border="black" cellpadding="3" summary="List of attached devices">
<tbody>
<tr>
<td><b>{{ _("ID") }}</b></td>
<th scope="col">{{ _("ID") }}</th>
{% if units %}
<td><b>{{ _("LUN") }}</b></td>
<th scope="col">{{ _("LUN") }}</th>
{% endif %}
<td><b>{{ _("Type") }}</b></td>
<td><b>{{ _("Status") }}</b></td>
<td><b>{{ _("File") }}</b></td>
<td><b>{{ _("Product") }}</b></td>
<td><b>{{ _("Actions") }}</b></td>
<th scope="col">{{ _("Device") }}</th>
<th scope="col">{{ _("Parameters") }}</th>
<th scope="col">{{ _("Product") }}</th>
<th scope="col">{{ _("Actions") }}</th>
</tr>
{% for device in devices %}
{% for device in devices | sort(attribute='id') %}
<tr>
{% if device["id"] not in reserved_scsi_ids %}
<td style="text-align:center">{{ device.id }}</td>
<td align="center">{{ device.id }}</td>
{% if units %}
<td style="text-align:center">{{ device.unit }}</td>
<td align="center">{{ device.unit }}</td>
{% endif %}
<td style="text-align:center">{{ device.device_type }}</td>
<td style="text-align:center">{{ device.status }}</td>
<td style="text-align:left">
<td align="center">{{ device.device_name }}</td>
<td>
{% if "No Media" in device.status %}
<form action="/scsi/attach" method="post">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}">
<input name="type" type="hidden" value="{{ device.device_type }}">
<input name="file_size" type="hidden" value="{{ device.size }}">
<select type="select" name="file_name">
<label for="device_list_file_name_{{ device.id }}_{{ device.unit }}">{{ _("File name") }}</label>
<select type="select" name="file_name" id="device_list_file_name_{{ device.id }}_{{ device.unit }}">
{% for f in files|sort(attribute='name') %}
{% if device.device_type == "SCCD" %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
{% if f["name"].lower().endswith(env['cd_suffixes']) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(env["image_dir"], '') }}</option>
{% endif %}
{% elif device.device_type == "SCRM" %}
{% if f["name"].lower().endswith(removable_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
{% if f["name"].lower().endswith(env['rm_suffixes']) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(env["image_dir"], '') }}</option>
{% endif %}
{% elif device.device_type == "SCMO" %}
{% if f["name"].lower().endswith(mo_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
{% if f["name"].lower().endswith(env['mo_suffixes']) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(env["image_dir"], '') }}</option>
{% endif %}
{% endif %}
{% endfor %}
@ -84,48 +89,57 @@
<input type="submit" value="{{ _("Attach") }}">
</form>
{% else %}
{% if device.params %}
{% for key in device.params %}
{% if key == "interface" %}
({{device.params[key]}})
{% elif key == "timeout" %}
({{key}}:{{device.params[key]}})
{% else %}
{{device.params[key]}}
{% endif %}
{% endfor %}
{% elif device.file %}
{{ device.file }}
{% endif %}
</td>
{% if device.vendor == "RaSCSI" %}
<td style="text-align:center">{{ device.product }}</td>
{% else %}
<td style="text-align:center">{{ device.vendor }} {{ device.product }}</td>
{% endif %}
<td style="text-align:center">
{% if device.device_type != "-" %}
</td>
<td align="center">
{% if device.vendor != "RaSCSI" %}
{{ device.vendor }}
{% endif %}
{{ device.product }}
{% if device.vendor != "RaSCSI" %}
{{ device.revision }}
{% endif %}
</td>
<td align="center">
{% if device.id in scsi_ids["occupied_ids"] %}
{% if device.device_type in REMOVABLE_DEVICE_TYPES and "No Media" not in device.status %}
<form action="/scsi/eject" method="post" onsubmit="return confirm('{{ _("Eject Disk? WARNING: On Mac OS, eject the Disk in the Finder instead!") }}')">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="{{ _("Eject") }}">
</form>
{% else %}
{% endif %}
<form action="/scsi/detach" method="post" onsubmit="return confirm('{{ _("Detach Device?") }}')">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="{{ _("Detach") }}">
</form>
{% endif %}
<form action="/scsi/info" method="post">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="{{ _("Info") }}">
</form>
{% else %}
<form action="/scsi/reserve" method="post" onsubmit="var memo = prompt('{{ _("Enter a memo for this reservation") }}'); if (memo === null) event.preventDefault(); document.getElementById('memo_{{ device.id }}').value = memo;">
{% else %}
<form action="/scsi/reserve" method="post" onsubmit="var memo = prompt('{{ _("Enter a memo for this reservation") }}'); if (memo === null) event.preventDefault(); document.getElementById('memo_{{ device.id }}').value = memo;">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="memo" id="memo_{{ device.id }}" type="hidden" value="">
<input type="submit" value="{{ _("Reserve") }}">
</form>
{% endif %}
{% endif %}
</td>
{% else %}
<td class="inactive">{{ device.id }}</td>
{% if units %}
<td class="inactive"></td>
{% endif %}
<td class="inactive"></td>
<td class="inactive">{{ _("Reserved ID") }}</td>
<td class="inactive">{{ RESERVATIONS[device.id] }}</td>
<td class="inactive"></td>
@ -141,9 +155,14 @@
</tbody>
</table>
<p><form action="/scsi/detach_all" method="post" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
<input type="submit" value="{{ _("Detach All Devices") }}">
</form></p>
<p>
<form action="/scsi/detach_all" method="post" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
<input type="submit" value="{{ _("Detach All Devices") }}">
</form>
<form action="/scsi/info" method="post">
<input type="submit" value="{{ _("Show Device Info") }}">
</form>
</p>
<hr/>
@ -152,19 +171,28 @@
{{ _("Image File Management") }}
</summary>
<ul>
<li>{{ _("Manage image files in the active RaSCSI image directory: <tt>%(directory)s</tt> with a scan depth of %(scan_depth)s.", directory=base_dir, scan_depth=scan_depth) }}</li>
<li>{{ _("Select a valid SCSI ID and <a href=\"%(url)s\">LUN</a> to attach to. Unless you know what you're doing, always use LUN 0.", url="https://en.wikipedia.org/wiki/Logical_unit_number") }}
<li>{{ _("Manage image files in the active RaSCSI image directory: <tt>%(directory)s</tt> with a scan depth of %(scan_depth)s.", directory=env["image_dir"], scan_depth=scan_depth) }}</li>
<li>{{ _("Select a valid SCSI ID and <a href=\"%(url)s\" target=\"_blank\">LUN</a> to attach to. Unless you know what you're doing, always use LUN 0.", url="https://en.wikipedia.org/wiki/Logical_unit_number") }}
</li>
<li>
{{ _("Recognized image file types:") }}
{% set comma = joiner(", ") %}
{% for extension in valid_image_suffixes %}{{ comma() }}.{{ extension}}{% endfor %}
</li>
<li>
{{ _("Recognized archive file types:") }}
{% set comma = joiner(", ") %}
{% for extension in ARCHIVE_FILE_SUFFIXES %}{{ comma() }}.{{ extension}}{% endfor %}
</li>
<li>{{ _("If RaSCSI was unable to detect the media type associated with the image, you get to choose the type from the dropdown.") }}</li>
</ul>
</details>
<table border="black" cellpadding="3">
<table border="black" cellpadding="3" summary="List of files in the image directory">
<tbody>
<tr style="font-weight: bold;">
<td>{{ _("File") }}</td>
<td>{{ _("Size") }}</td>
<td>{{ _("Parameters and Actions") }}</td>
<tr>
<th scope="col">{{ _("File") }}</th>
<th scope="col">{{ _("Size") }}</th>
<th scope="col">{{ _("Actions") }}</th>
</tr>
{% for file in files|sort(attribute='name') %}
<tr>
@ -174,12 +202,12 @@
<summary>
{{ file["name"] }}
</summary>
<ul style="list-style: none;">
<ul class="inline_list">
{% for key in file["prop"] %}
<li>{{ key }}: {{ file['prop'][key] }}</li>
{% endfor %}
<form action="/files/download" method="post">
<input name="file" type="hidden" value="{{ CFG_DIR }}/{{ file['name'].replace(base_dir, '') }}.{{ PROPERTIES_SUFFIX }}">
<input name="file" type="hidden" value="{{ CFG_DIR }}/{{ file['name'].replace(env['image_dir'], '') }}.{{ PROPERTIES_SUFFIX }}">
<input type="submit" value="{{ _("Properties File") }} &#8595;">
</form>
</ul>
@ -191,7 +219,7 @@
<summary>
{{ file["name"] }}
</summary>
<ul style="list-style: none;">
<ul class="inline_list">
{% for member in file["archive_contents"] %}
{% if not member["is_properties_file"] %}
<li>
@ -205,7 +233,7 @@
<input type="submit" value="{{ _("Extract") }}" onclick="processNotify('{{ _("Extracting a single file...") }}')">
</form>
</summary>
<ul style="list-style: none;">
<ul class="inline_list">
<li>{{ member["related_properties_file"] }}</li>
</ul>
</details>
@ -226,17 +254,15 @@
{% else %}
<td>{{ file["name"] }}</td>
{% endif %}
<td style="text-align:center">
<td align="center">
<form action="/files/download" method="post">
<input name="file" type="hidden" value="{{ base_dir }}/{{ file['name'] }}">
<input type="submit" value="{{ file['size_mb'] }} {{ _("MB") }} &#8595;">
<input name="file" type="hidden" value="{{ file['name'] }}">
<input type="submit" value="{{ file['size_mb'] }} {{ _("MiB") }} &#8595;">
</form>
</td>
<td>
{% if file["name"] in attached_images %}
<center>
{{ _("Attached!") }}
</center>
{{ _("In use") }}
{% else %}
{% if file["archive_contents"] %}
<form action="/files/extract_image" method="post">
@ -249,23 +275,24 @@
<form action="/scsi/attach" method="post">
<input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="file_size" type="hidden" value="{{ file['size'] }}">
<label for="id">{{ _("ID") }}</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option name="id" value="{{id}}"{% if id == recommended_id %} selected{% endif %}>
<label for="image_list_scsi_id_{{ file["name"] }}">{{ _("ID") }}</label>
<select name="scsi_id" id="image_list_scsi_id_{{ file["name"] }}">
{% for id in scsi_ids["valid_ids"] %}
<option name="id" value="{{id}}"{% if id == scsi_ids["recommended_id"] %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<label for="unit">{{ _("LUN") }}</label>
<input name="unit" type="number" value="0" min="0" max="31" step="1">
<label for="image_list_unit_{{ file["name"] }}">{{ _("LUN") }}</label>
<input class="lun" name="unit" id="image_list_unit_{{ file["name"] }}" type="number" value="0" min="0" max="31" step="1" size="3">
{% if file["detected_type"] != "UNDEFINED" %}
<input name="type" type="hidden" value="{{ file['detected_type'] }}">
{{ file['detected_type_name'] }}
{% else %}
<select name="type">
<label for="image_list_type_{{ file["name"] }}">{{ _("Type") }}</label>
<select name="type" id="image_list_type_{{ file["name"] }}">
<option selected disabled value="">
{{ _("Select media type") }}
{{ _("Unknown") }}
</option>
{% for key, value in device_types.items() %}
{% if key in DISK_DEVICE_TYPES %}
@ -294,12 +321,18 @@
<input type="submit" value="{{ _("Delete") }}">
</form>
{% endif %}
{% if not file["archive_contents"] %}
<form action="/files/diskinfo" method="post">
<input name="file_name" type="hidden" value="{{ file['name'] }}">
<input type="submit" value="{{ _("?") }}">
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><small>{{ _("%(disk_space)s MB disk space remaining on the Pi", disk_space=free_disk) }}</small></p>
<p><small>{{ _("%(disk_space)s MiB disk space remaining on the Pi", disk_space=env["free_disk_space"]) }}</small></p>
<hr/>
<details>
@ -307,39 +340,41 @@
{{ _("Attach Peripheral Device") }}
</summary>
<ul>
<li>{{ _("<a href=\"%(url1)s\">DaynaPORT SCSI/Link</a> and <a href=\"%(url2)s\">X68000 Host Bridge</a> are network devices.", url1="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link", url2="https://github.com/akuker/RASCSI/wiki/X68000#Host_File_System_driver") }}
</li>
<ul>
<li>{{ _("If you have a DHCP setup, choose only the interface you have configured the bridge with. You can ignore the inet field when attaching.") }}</li>
<li>{{ _("Configure the network bridge by running easyinstall.sh, or follow the <a href=\"%(url)s\">manual steps in the wiki</a>.", url="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#manual-setup") }}
{% if bridge_configured %}
<li>{{ _("The <tt>rascsi_bridge</tt> network bridge is active and ready to be used by an emulated network adapter!") }}</li>
{% else %}
<li>{{ _("Please configure the <tt>rascsi_bridge</tt> network bridge before attaching an emulated network adapter!") }}</li>
{% endif %}
<li>{{ _("To browse the modern web, install a vintage web proxy such as <a href=\"%(url)s\" target=\"_blank\">Macproxy</a>.", url="https://github.com/akuker/RASCSI/wiki/Vintage-Web-Proxy#macproxy") }}</li>
</li>
</ul>
<li>{{ _("The Printer and Host Services device are currently supported on compatible Atari systems, and require <a href=\"%(url)s\">driver software</a> to be installed on the host system.", url="https://www.hddriver.net/en/rascsi_tools.html") }}
<li>{{ _("Read more about <a href=\"%(url)s\" target=\"_blank\">supported device types</a> on the wiki.", url="https://github.com/akuker/RASCSI/wiki/Supported-Device-Types") }}
</li>
</ul>
</details>
<table border="black" cellpadding="3">
<tr style="font-weight: bold;">
<td>{{ _("Peripheral") }}</td>
<td>{{ _("Parameters and Actions") }}</td>
<table border="black" cellpadding="3" summary="List of peripheral devices">
<tr>
<th scope="col">{{ _("Device") }}</th>
<th scope="col">{{ _("Key") }}</th>
<th scope="col">{{ _("Parameters and Actions") }}</th>
</tr>
{% for type in PERIPHERAL_DEVICE_TYPES %}
{% for type in REMOVABLE_DEVICE_TYPES + PERIPHERAL_DEVICE_TYPES %}
<tr>
<td>
<div>{{ device_types[type]["name"] }}</div>
</td>
<td>
<div>{{ type }}</div>
</td>
<td>
<form action="/scsi/attach_device" method="post">
<input name="type" type="hidden" value="{{ type }}">
{% for key, value in device_types[type]["params"].items() %}
<label for="{{ key }}">{{ key }}:</label>
{% for key, value in device_types[type]["params"] | dictsort %}
<label for="param_{{ type }}_{{ key }}">{{ key }}:</label>
{% if value.isnumeric() %}
<input name="{{ key }}" type="number" value="{{ value }}">
<input name="param_{{ key }}" id="param_{{ type }}_{{ key }}" type="number" value="{{ value }}">
{% elif key == "interface" %}
<select name="interface">
<select name="param_{{ key }}" id="param_{{ type }}_{{ key }}">
{% for if in netinfo["ifs"] %}
<option value="{{ if }}">
{{ if }}
@ -347,63 +382,89 @@
{% endfor %}
</select>
{% else %}
<input name="{{ key }}" type="text" size="{{ value|length }}" placeholder="{{ value }}">
<input name="param_{{ key }}" id="param_{{ type }}_{{ key }}" type="text" size="{{ value|length }}" placeholder="{{ value }}">
{% endif %}
{% endfor %}
<label for="scsi_id">{{ _("SCSI ID:") }}</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}>
{% if type in REMOVABLE_DEVICE_TYPES %}
<label for="{{ type }}_drive_name">{{ _("Masquerade as:") }}</label>
<select name="drive_name" id="{{ type }}_drive_name">
<option value="">
{{ _("None") }}
</option>
{% if type == "SCCD" %}
{% for drive in env["drive_properties"]["cd_conf"] | sort(attribute='name') %}
<option value="{{ drive.name }}">
{{ drive.name }}
</option>
{% endfor %}
{% endif %}
{% if type == "SCRM" %}
{% for drive in env["drive_properties"]["rm_conf"] | sort(attribute='name') %}
<option value="{{ drive.name }}">
{{ drive.name }}
</option>
{% endfor %}
{% endif %}
{% if type == "SCMO" %}
{% for drive in env["drive_properties"]["mo_conf"] | sort(attribute='name') %}
<option value="{{ drive.name }}">
{{ drive.name }}
</option>
{% endfor %}
{% endif %}
</select>
{% endif %}
<label for="{{ type }}_scsi_id">{{ _("ID") }}</label>
<select name="scsi_id" id="{{ type }}_scsi_id">
{% for id in scsi_ids["valid_ids"] %}
<option value="{{ id }}"{% if id == scsi_ids["recommended_id"] %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<label for="unit">{{ _("LUN") }}</label>
<input name="unit" type="number" value="0" min="0" max="31" step="1">
<label for="{{ type }}_unit">{{ _("LUN") }}</label>
<input class="lun" name="unit" id="{{ type }}_unit" type="number" value="0" min="0" max="31" step="1" size="3">
<input type="submit" value="{{ _("Attach") }}">
</form>
</td>
</tr>
{% endfor %}
</table>
{% if macproxy_configured %}
<p><small>{{ _("Macproxy is running at %(ip_addr)s (default port 5000)", ip_addr=ip_addr) }}</small></p>
{% else %}
<p><small>{{ _("Install <a href=\"%(url)s\">Macproxy</a> to browse the Web with any vintage browser. It's not just for Macs!", url="https://github.com/akuker/RASCSI/wiki/Vintage-Web-Proxy#macproxy") }}</small></p>
{% endif %}
<hr/>
<details>
<summary class="heading">
{{ _("Upload File") }}
{{ _("Upload File from Local Computer") }}
</summary>
<ul>
<li>{{ _("Uploads file to <tt>%(directory)s</tt>. The largest file size accepted is %(max_file_size)s MB.", directory=base_dir, max_file_size=max_file_size) }}</li>
<li>{{ _("For unrecognized file types, try renaming hard drive images to '.hds', CD-ROM images to '.iso', and removable drive images to '.hdr' before uploading.") }}</li>
<li>{{ _("Recognized file types: %(valid_file_suffix)s", valid_file_suffix=valid_file_suffix) }}</li>
<li>{{ _("The largest file size accepted in this form is %(max_file_size)s MiB. Use other file transfer means for larger files.", max_file_size=max_file_size) }}</li>
<li>{{ _("File uploads will progress only if you stay on this page. If you navigate away before the transfer is completed, you will end up with an incomplete file.") }}</li>
<li>{{ _("Install <a href=\"%(url)s\" target=\"_blank\">Netatalk</a> to use the AFP File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper"></form>
</td>
</tr>
</table>
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper">
<p>
<label for="upload_destination">{{ _("Target directory:") }}</label>
<select name="destination" id="upload_destination">
<option value="images">Images - {{ env["image_dir"] }}</option>
<option value="afp">AppleShare - {{ AFP_DIR }}</option>
</select>
</p>
</form>
<script type="application/javascript">
Dropzone.options.dropper = {
paramName: 'file',
acceptedFiles: '{{ valid_file_suffix }}',
chunking: true,
forceChunking: true,
url: '/files/upload',
maxFilesize: {{ max_file_size }}, // MB
maxFilesize: {{ max_file_size }}, // MiB
chunkSize: 1000000, // bytes
dictDefaultMessage: "{{ _("Drop files here to upload") }}",
dictFallbackMessage: "{{ _("Your browser does not support drag'n'drop file uploads.") }}",
dictFallbackText: "{{ _("Please use the fallback form below to upload your files like in the olden days.") }}",
dictFileTooBig: "{{ _("File is too big: {{filesize}}MB. Max filesize: {{maxFilesize}}MB.") }}",
dictFileTooBig: "{{ _("File is too big: {{filesize}}MiB. Max filesize: {{maxFilesize}}MiB.") }}",
dictInvalidFileType: "{{ _("You can't upload files of this type.") }}",
dictResponseError: "{{ _("Server responded with code: {{statusCode}}") }}",
dictCancelUpload:" {{ _("Cancel upload") }}",
@ -412,10 +473,10 @@
dictRemoveFile: "{{ _("Remove file") }}",
dictMaxFilesExceeded: "{{ _("You can not upload any more files.") }}",
dictFileSizeUnits: {
tb: "{{ _("TB") }}",
gb: "{{ _("GB") }}",
mb: "{{ _("MB") }}",
kb: "{{ _("KB") }}",
tb: "{{ _("TiB") }}",
gb: "{{ _("GiB") }}",
mb: "{{ _("MiB") }}",
kb: "{{ _("KiB") }}",
b: "{{ _("B") }}"
}
}
@ -425,61 +486,23 @@
<details>
<summary class="heading">
{{ _("Download File to Images") }}
{{ _("Download File from the Web") }}
</summary>
<ul>
<li>{{ _("Given a URL, download that file to the <tt>%(directory)s</tt> directory.", directory=base_dir) }}</li>
<li>{{ _("Install <a href=\"%(url)s\" target=\"_blank\">Netatalk</a> to use the AFP File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_images" method="post">
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File to Images...") }}')">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">
{{ _("Download File to AppleShare") }}
</summary>
<ul>
<li>{{ _("Given a URL, download that file to the <tt>%(directory)s</tt> directory and share it over AFP.", directory=AFP_DIR) }}</li>
<li>{{ _("Manage the files you download here through AppleShare on your vintage Mac.") }}</li>
<li>{{ _("Requires <a href=\"%(url)s\">Netatalk</a> to be installed and configured correctly for your network.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</li>
</ul>
</details>
{% if netatalk_configured %}
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_afp" method="post">
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File to AppleShare...") }}')">
</form>
</td>
</tr>
</table>
{% if netatalk_configured == 1 %}
<p><small>{{ _("The AppleShare server is running. No active connections.") }}</small></p>
{% elif netatalk_configured == 2 %}
<p><small>{{ _("%(value)d active AFP connection", value=(netatalk_configured - 1)) }}</small></p>
{% elif netatalk_configured > 2 %}
<p><small>{{ _("%(value)d active AFP connections", value=(netatalk_configured - 1)) }}</small></p>
{% endif %}
{% else %}
<p>{{ _("Install <a href=\"%(url)s\">Netatalk</a> to use the AppleShare File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</p>
{% endif %}
<form action="/files/download_url" method="post">
<label for="download_destination">{{ _("Target directory:") }}</label>
<select name="destination" id="download_destination">
<option value="images">Images - {{ env["image_dir"] }}</option>
<option value="afp">AppleShare - {{ AFP_DIR }}</option>
</select>
<label for="download_url">{{ _("URL:") }}</label>
<input name="url" id="download_url" required="" type="url">
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File...") }}')">
</form>
<hr/>
@ -490,50 +513,44 @@
<ul>
<li>{{ _("Create an ISO file system CD-ROM image with the downloaded file, and mount it on the given SCSI ID.") }}</li>
<li>{{ _("HFS is for Mac OS, Joliet for Windows, and Rock Ridge for POSIX.") }}</li>
<li>{{ _("On Mac OS, a <a href=\"%(url)s\">compatible CD-ROM driver</a> is required.", url="https://github.com/akuker/RASCSI/wiki/Drive-Setup#Mounting_CD_ISO_or_MO_images") }}</li>
<li>{{ _("If the downloaded file is a zip archive, we will attempt to unzip it and store the resulting files.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<label for="scsi_id">{{ _("SCSI ID:") }}</label>
<form action="/files/download_to_iso" method="post">
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<label for="type">{{ _("Type:") }}</label>
<select name="type">
<option value="-hfs">
HFS
</option>
<option value="-iso-level 1">
ISO-9660 Level 1
</option>
<option value="-iso-level 2">
ISO-9660 Level 2
</option>
<option value="-iso-level 3">
ISO-9660 Level 3
</option>
<option value="-J">
Joliet
</option>
<option value="-r">
Rock Ridge
</option>
</select>
<input type="submit" value="{{ _("Download and Mount CD-ROM image") }}" onclick="processNotify('{{ _("Downloading File and generating CD-ROM image...") }}')">
</form>
</td>
</tr>
</table>
<form action="/files/download_to_iso" method="post">
<label for="iso_url">{{ _("URL:") }}</label>
<input name="url" id="iso_url" required="" type="url">
<label for="iso_type">{{ _("Type:") }}</label>
<select name="type" id="iso_type">
<option value="-hfs">
HFS
</option>
<option value="-iso-level 1">
ISO-9660 Level 1
</option>
<option value="-iso-level 2">
ISO-9660 Level 2
</option>
<option value="-iso-level 3">
ISO-9660 Level 3
</option>
<option value="-J">
Joliet
</option>
<option value="-r">
Rock Ridge
</option>
</select>
<label for="iso_scsi_id">{{ _("ID") }}</label>
<select name="scsi_id" id="iso_scsi_id">
{% for id in scsi_ids["valid_ids"] %}
<option value="{{ id }}"{% if id == scsi_ids["recommended_id"] %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Download and Mount CD-ROM image") }}" onclick="processNotify('{{ _("Downloading File and generating CD-ROM image...") }}')">
</form>
<hr/>
@ -542,54 +559,37 @@
{{ _("Create Empty Disk Image File") }}
</summary>
<ul>
<li>{{ _("The Generic image type is recommended for most computer platforms.") }}</li>
<li>{{ _("APPLE GENUINE (.hda) and NEC GENUINE (.hdn) image types will make RaSCSI behave as a particular drive type that are recognized by Mac and PC98 systems, respectively.") }}</li>
<li>{{ _("SASI images should only be used on the original Sharp X68000, or other legacy systems that utilize this pre-SCSI standard.") }}</li>
<li>{{ _("Please refer to <a href=\"%(url)s\" target=\"_blank\">wiki documentation</a> to learn more about the supported image file types.", url="https://github.com/akuker/RASCSI/wiki/Supported-Device-Types#image-types") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/create" method="post">
<label for="file_name">{{ _("File Name:") }}</label>
<input name="file_name" placeholder="{{ _("File Name") }}" required="" type="text">
<label for="type">{{ _("Type:") }}</label>
<select name="type">
<option value="hds">
{{ _("SCSI Hard Disk image (Generic) [.hds]") }}
</option>
<option value="hda">
{{ _("SCSI Hard Disk image (APPLE GENUINE) [.hda]") }}
</option>
<option value="hdn">
{{ _("SCSI Hard Disk image (NEC GENUINE) [.hdn]") }}
</option>
<option value="hdr">
{{ _("SCSI Removable Media Disk image (Generic) [.hdr]") }}
</option>
<option value="hdf">
{{ _("SASI Hard Disk image (Legacy) [.hdf]") }}
</option>
</select>
<label for="size">{{ _("Size:") }}</label>
<input name="size" type="number" placeholder="{{ _("MB") }}" min="1" max="262144" required>
<input type="submit" value="{{ _("Create") }}">
</form>
</td>
</tr>
</table>
<hr/>
<form action="/files/create" method="post">
<label for="image_create_file_name">{{ _("File Name:") }}</label>
<input name="file_name" id="image_create_file_name" required="" type="text">
<label for="image_create_type">{{ _("Type:") }}</label>
<select name="type" id="image_create_type">
{% for key, value in image_suffixes_to_create.items() %}
<option value="{{ key }}">
{{ value }} [.{{ key }}]
</option>
{% endfor %}
</select>
<label for="image_create_size">{{ _("Size:") }}</label>
<input name="size" id="image_create_size" type="number" placeholder="{{ _("MiB") }}" min="1" max="262144" required>
<label for="image_create_drive_name">{{ _("Masquerade as:") }}</label>
<select name="drive_name" id="image_create_drive_name">
<option value="">
{{ _("None") }}
</option>
{% for drive in env["drive_properties"]["hd_conf"] | sort(attribute='name') %}
<option value="{{ drive.name }}">
{{ drive.name }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Create") }}">
</form>
<details>
<summary class="heading">
{{ _("Create Named Drive") }}
</summary>
<ul>
<li>{{ _("Create pairs of images and properties files from a list of real-life drives.") }}</li>
<li>{{ _("This will make RaSCSI use certain vendor strings and block sizes that may improve compatibility with certain systems.") }}</li>
</ul>
</details>
<p><a href="/drive/list">{{ _("Create a named disk image that mimics real-life drives") }}</a></p>
<hr/>
@ -599,67 +599,49 @@
{{ _("Logging") }}
</summary>
<ul>
<li>{{ _("Fetch a certain number of lines of system logs with the given scope.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/logs/show" method="post">
<label for="lines">{{ _("Log Lines:") }}</label>
<input name="lines" type="number" value="200" min="0" max="99999" step="100">
<label for="scope">{{ _("Scope:") }}</label>
<select name="scope">
<option value="">
{{ _("All logs") }}
</option>
<option value="rascsi">
rascsi
</option>
<option value="rascsi-web">
rascsi-web
</option>
<option value="rascsi-oled">
rascsi-oled
</option>
<option value="rascsi-ctrlboard">
rascsi-ctrlboard
</option>
</select>
<input type="submit" value="{{ _("Show Logs") }}">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">
{{ _("Server Log Level") }}
</summary>
<ul>
<li>{{ _("Change the log level of the RaSCSI backend process.") }}</li>
<li>{{ _("The current dropdown selection indicates the active log level.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/logs/level" method="post">
<label for="level">{{ _("Log Level:") }}</label>
<select name="level">
{% for level in log_levels %}
<option value="{{ level }}"{% if level == current_log_level %} selected{% endif %}>
{{ level }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Set Log Level") }}">
</form>
</td>
</tr>
</table>
<div>
<form action="/logs/show" method="post">
<label for="log_lines">{{ _("Log Lines:") }}</label>
<input name="lines" id="log_lines" type="number" value="200" min="0" max="99999" step="100">
<label for="log_scope">{{ _("Scope:") }}</label>
<select name="scope" id="log_scope">
<option value="">
{{ _("All logs") }}
</option>
<option value="rascsi">
rascsi
</option>
<option value="rascsi-web">
rascsi-web
</option>
<option value="rascsi-oled">
rascsi-oled
</option>
<option value="rascsi-ctrlboard">
rascsi-ctrlboard
</option>
</select>
<input type="submit" value="{{ _("Show Logs") }}">
</form>
</div>
<div>
<form action="/logs/level" method="post">
<label for="log_level">{{ _("Log Level:") }}</label>
<select name="level" id="log_level">
{% for level in log_levels %}
<option value="{{ level }}"{% if level == current_log_level %} selected{% endif %}>
{{ level }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Set Log Level") }}">
</form>
</div>
<hr/>
@ -671,23 +653,18 @@
<li>{{ _("Change the Web Interface language.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/language" method="post">
<label for="language">{{ _("Language:") }}</label>
<select name="locale">
{% for locale in locales %}
<option value="{{ locale.language }}">
{{ locale.language }} - {{ locale.display_name }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Change Language") }}">
</form>
</td>
</tr>
</table>
<form action="/language" method="post">
<label for="locale">{{ _("Language:") }}</label>
<select name="locale" id="locale">
{% for locale in locales %}
<option value="{{ locale.language }}">
{{ locale.language }} - {{ locale.display_name }}
</option>
{% endfor %}
</select>
<input type="submit" value="{{ _("Change Language") }}">
</form>
<hr/>
@ -696,23 +673,19 @@
{{ _("Raspberry Pi Operations") }}
</summary>
<ul>
<li>{{ _("Reboot or shut down the Raspberry Pi that RaSCSI is running on.") }}</li>
<li>{{ _("IMPORTANT: Always shut down the Pi before turning off the power. Failing to do so may lead to data loss.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/pi/reboot" method="post" onclick="if (confirm('{{ _("Reboot the Raspberry Pi?") }}')) shutdownNotify('{{ _("Rebooting the Raspberry Pi...") }}'); else event.preventDefault();">
<input type="submit" value="{{ _("Reboot Raspberry Pi") }}">
</form>
</td>
<td style="border: none; vertical-align:top;">
<form action="/pi/shutdown" method="post" onclick="if (confirm('{{ _("Shut down the Raspberry Pi?") }}')) shutdownNotify('{{ _("Shutting down the Raspberry Pi...") }}'); else event.preventDefault();">
<input type="submit" value="{{ _("Shut Down Raspberry Pi") }}">
</form>
</td>
</tr>
</table>
<form action="/pi/reboot" method="post" onclick="if (confirm('{{ _("Reboot the Raspberry Pi?") }}')) shutdownNotify('{{ _("Rebooting the Raspberry Pi...") }}'); else event.preventDefault();">
<input type="submit" value="{{ _("Reboot Raspberry Pi") }}">
</form>
<form action="/pi/shutdown" method="post" onclick="if (confirm('{{ _("Shut down the Raspberry Pi?") }}')) shutdownNotify('{{ _("Shutting down the Raspberry Pi...") }}'); else event.preventDefault();">
<input type="submit" value="{{ _("Shut Down Raspberry Pi") }}">
</form>
<hr/>
<a href="/sys/manpage?app=rascsi"><p>{{ _("Read the RaSCSI Manual") }}</p></a>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<h2>{{ _("System Logs: %(scope)s %(lines)s lines", scope=scope, lines=lines) }}</h2>
<p><pre>{{ logs }}</pre></p>
<p><a href="/">{{ _("Go to Home") }}</a></p>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<h2>{{ _("Manual for %(app)s", app=app) }}</h2>
{{ manpage | safe }}
<p><a href="/">{{ _("Go to Home") }}</a></p>
{% endblock content %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@ def get_valid_scsi_ids(devices, reserved_ids):
Takes a list of (dict)s devices, and list of (int)s reserved_ids.
Returns:
- (list) of (int)s valid_ids, which are the SCSI ids that are not reserved
- (list) of (int)s occupied_ids, which are the SCSI ids were a device is attached
- (int) recommended_id, which is the id that the Web UI should default to recommend
"""
occupied_ids = []
@ -33,11 +34,15 @@ def get_valid_scsi_ids(devices, reserved_ids):
recommended_id = unoccupied_ids[-1]
else:
if occupied_ids:
recommended_id = occupied_ids.pop(0)
recommended_id = occupied_ids[0]
else:
recommended_id = 0
return valid_ids, recommended_id
return {
"valid_ids": valid_ids,
"occupied_ids": occupied_ids,
"recommended_id": recommended_id,
}
def sort_and_format_devices(devices):
@ -47,18 +52,25 @@ def sort_and_format_devices(devices):
For SCSI IDs where no device is attached, inject a (dict) with placeholder text.
"""
occupied_ids = []
formatted_devices = []
for device in devices:
occupied_ids.append(device["id"])
device["device_name"] = get_device_name(device["device_type"])
formatted_devices.append(device)
formatted_devices = devices
# Add padding devices and sort the list
for i in range(8):
if i not in occupied_ids:
formatted_devices.append({"id": i, "device_type": "-", \
"status": "-", "file": "-", "product": "-"})
# Sort list of devices by id
formatted_devices.sort(key=lambda dic: str(dic["id"]))
# Add placeholder data for non-occupied IDs
for scsi_id in range(8):
if scsi_id not in occupied_ids:
formatted_devices.append(
{
"id": scsi_id,
"unit": "-",
"device_name": "-",
"status": "-",
"file": "-",
"product": "-",
}
)
return formatted_devices
@ -80,20 +92,18 @@ def get_device_name(device_type):
Takes a four letter device acronym (str) device_type.
Returns the human-readable name for the device type.
"""
if device_type == "SAHD":
return _("SASI Hard Disk")
if device_type == "SCHD":
return _("SCSI Hard Disk")
return _("Hard Disk Drive")
if device_type == "SCRM":
return _("Removable Disk")
return _("Removable Disk Drive")
if device_type == "SCMO":
return _("Magneto-Optical")
return _("Magneto-Optical Drive")
if device_type == "SCCD":
return _("CD / DVD")
return _("CD/DVD Drive")
if device_type == "SCBR":
return _("X68000 Host Bridge")
return _("Host Bridge")
if device_type == "SCDP":
return _("DaynaPORT SCSI/Link")
return _("Ethernet Adapter")
if device_type == "SCLP":
return _("Printer")
if device_type == "SCHS":
@ -101,6 +111,115 @@ def get_device_name(device_type):
return device_type
def map_image_file_descriptions(file_suffixes):
"""
Takes a (list) of (str) file suffixes for images file types.
Returns a (dict) with file suffix and description pairs, both (str)
"""
supported_image_types = {}
for suffix in file_suffixes:
supported_image_types[suffix] = get_image_description(suffix)
return supported_image_types
# pylint: disable=too-many-return-statements
def get_image_description(file_suffix):
"""
Takes a three char file suffix (str) file_suffix.
Returns the help text description for said file suffix.
"""
if file_suffix == "hds":
return _("Hard Disk Image (Generic)")
if file_suffix == "hda":
return _("Hard Disk Image (Apple)")
if file_suffix == "hdn":
return _("Hard Disk Image (NEC)")
if file_suffix == "hd1":
return _("Hard Disk Image (SCSI-1)")
if file_suffix == "hdr":
return _("Removable Disk Image")
if file_suffix == "mos":
return _("Magneto-Optical Disk Image")
return file_suffix
def format_drive_properties(drive_properties):
"""
Takes a (dict) with structured drive properties data
Returns a (dict) with the formatted properties, one (list) per device type
"""
hd_conf = []
cd_conf = []
rm_conf = []
mo_conf = []
FORMAT_FILTER = "{:,.2f}"
for device in drive_properties:
# Add fallback device names, since other code relies on this data for display
if not device["name"]:
if device["product"]:
device["name"] = device["product"]
else:
device["name"] = "Unknown Device"
if device["device_type"] == "SCHD":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = FORMAT_FILTER.format(device["size"] / 1024 / 1024)
hd_conf.append(device)
elif device["device_type"] == "SCCD":
device["size_mb"] = _("N/A")
cd_conf.append(device)
elif device["device_type"] == "SCRM":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = FORMAT_FILTER.format(device["size"] / 1024 / 1024)
rm_conf.append(device)
elif device["device_type"] == "SCMO":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = FORMAT_FILTER.format(device["size"] / 1024 / 1024)
mo_conf.append(device)
return {
"hd_conf": hd_conf,
"cd_conf": cd_conf,
"rm_conf": rm_conf,
"mo_conf": mo_conf,
}
def get_properties_by_drive_name(drives, drive_name):
"""
Takes (list) of (dict) drives, and (str) drive_name
Returns (dict) with the collection of drive properties that matches drive_name
"""
drives.sort(key=lambda item: item.get("name"))
drive_props = None
prev_drive = {"name": ""}
for drive in drives:
# TODO: Make this check into an integration test
if "name" not in drive:
logging.warning(
"Device without a name exists in the drive properties database. This is a bug."
)
break
# TODO: Make this check into an integration test
if drive["name"] == prev_drive["name"]:
logging.warning(
"Device with duplicate name \"%s\" in drive properties database. This is a bug.",
drive["name"],
)
prev_drive = drive
if drive["name"] == drive_name:
drive_props = drive
return {
"file_type": drive_props["file_type"],
"vendor": drive_props["vendor"],
"product": drive_props["product"],
"revision": drive_props["revision"],
"block_size": drive_props["block_size"],
"size": drive_props["size"],
}
def auth_active(group):
"""
Inspects if the group defined in (str) group exists on the system.
@ -119,24 +238,31 @@ def auth_active(group):
def is_bridge_configured(interface):
"""
Takes (str) interface of a network device being attached.
Returns (bool) False if the network bridge is configured.
Returns (str) with an error message if the network bridge is not configured.
Returns a (dict) with (bool) status and (str) msg
"""
# TODO: Reduce the nesting of these checks, and streamline how the results are notified
status = True
return_msg = ""
sys_cmd = SysCmds()
if interface.startswith("wlan"):
if not sys_cmd.introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"):
return _("Configure IPv4 forwarding before using a wireless network device.")
if not Path("/etc/iptables/rules.v4").is_file():
return _("Configure NAT before using a wireless network device.")
status = False
return_msg = _("Configure IPv4 forwarding before using a wireless network device.")
elif not Path("/etc/iptables/rules.v4").is_file():
status = False
return_msg = _("Configure NAT before using a wireless network device.")
else:
if not sys_cmd.introspect_file(
"/etc/dhcpcd.conf",
r"^denyinterfaces " + interface + r"$",
):
return _("Configure the network bridge before using a wired network device.")
if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file():
return _("Configure the network bridge before using a wired network device.")
return False
status = False
return_msg = _("Configure the network bridge before using a wired network device.")
elif not Path("/etc/network/interfaces.d/rascsi_bridge").is_file():
status = False
return_msg = _("Configure the network bridge before using a wired network device.")
return {"status": status, "msg": return_msg + f" ({interface})"}
def upload_with_dropzonejs(image_dir):
@ -169,18 +295,7 @@ def upload_with_dropzonejs(image_dir):
if current_chunk + 1 == total_chunks:
# Validate the resulting file size after writing the last chunk
if path.getsize(save_path) != int(request.form["dztotalfilesize"]):
log.error(
"Finished transferring %s, "
"but it has a size mismatch with the original file. "
"Got %s but we expected %s.",
file_object.filename,
path.getsize(save_path),
request.form['dztotalfilesize'],
)
log.error("File size mismatch between the original file and transferred file.")
return make_response(_("Transferred file corrupted!"), 500)
log.info("File %s has been uploaded successfully", file_object.filename)
log.debug("Chunk %s of %s for file %s completed.",
current_chunk + 1, total_chunks, file_object.filename)
return make_response(_("File upload successful!"), 200)

View File

@ -62,8 +62,7 @@ if ! test -e venv; then
pip3 install wheel
pip3 install -r requirements.txt
git rev-parse --is-inside-work-tree &> /dev/null
if [[ $? -eq 0 ]]; then
if git rev-parse --is-inside-work-tree &> /dev/null; then
git rev-parse HEAD > current
fi
fi
@ -110,6 +109,9 @@ while [ "$1" != "" ]; do
-l | --log-level)
ARG_LOG_LEVEL="--log-level $VALUE"
;;
-d | --dev-mode)
ARG_DEV_MODE="--dev-mode"
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
exit 1
@ -122,4 +124,10 @@ PYTHON_COMMON_PATH=$(dirname $PWD)/common/src
echo "Starting web server for RaSCSI Web Interface..."
export PYTHONPATH=$PWD/src:${PYTHON_COMMON_PATH}
cd src
python3 web.py ${ARG_PORT} ${ARG_PASSWORD} ${ARG_RASCSI_HOST} ${ARG_RASCSI_PORT} ${ARG_LOG_LEVEL}
if [[ $ARG_DEV_MODE ]]; then
watchmedo auto-restart --directory=../../ --pattern=*.py --recursive -- \
python3 web.py ${ARG_PORT} ${ARG_PASSWORD} ${ARG_RASCSI_HOST} ${ARG_RASCSI_PORT} ${ARG_LOG_LEVEL} ${ARG_DEV_MODE}
else
python3 web.py ${ARG_PORT} ${ARG_PASSWORD} ${ARG_RASCSI_HOST} ${ARG_RASCSI_PORT} ${ARG_LOG_LEVEL} ${ARG_DEV_MODE}
fi

View File

@ -0,0 +1,82 @@
import pytest
import uuid
import warnings
SCSI_ID = 6
FILE_SIZE_1_MIB = 1048576
STATUS_SUCCESS = "success"
STATUS_ERROR = "error"
@pytest.fixture(scope="function")
def create_test_image(request, http_client):
images = []
def create(image_type="hds", size=1, auto_delete=True):
file_prefix = str(uuid.uuid4())
file_name = f"{file_prefix}.{image_type}"
response = http_client.post(
"/files/create",
data={
"file_name": file_prefix,
"type": image_type,
"size": size,
},
)
if response.json()["status"] != STATUS_SUCCESS:
raise Exception("Failed to create temporary image")
if auto_delete:
images.append(file_name)
return file_name
def delete():
for image in images:
response = http_client.post("/files/delete", data={"file_name": image})
if response.status_code != 200 or response.json()["status"] != STATUS_SUCCESS:
warnings.warn(
f"Failed to auto-delete file created with create_test_image fixture: {image}"
)
request.addfinalizer(delete)
return create
@pytest.fixture(scope="function")
def list_files(http_client):
def files():
return [f["name"] for f in http_client.get("/").json()["data"]["files"]]
return files
@pytest.fixture(scope="function")
def list_attached_images(http_client):
def files():
return http_client.get("/").json()["data"]["attached_images"]
return files
@pytest.fixture(scope="function")
def delete_file(http_client):
def delete(file_name):
response = http_client.post("/files/delete", data={"file_name": file_name})
if response.status_code != 200 or response.json()["status"] != STATUS_SUCCESS:
warnings.warn(f"Failed to delete file via delete_file fixture: {file_name}")
return delete
@pytest.fixture(scope="function")
def detach_devices(http_client):
def detach():
response = http_client.post("/scsi/detach_all")
if response.json()["status"] == STATUS_SUCCESS:
return True
raise Exception("Failed to detach SCSI devices")
return detach

View File

@ -0,0 +1,44 @@
from conftest import STATUS_SUCCESS, STATUS_ERROR
# route("/login", methods=["POST"])
def test_login_with_valid_credentials(pytestconfig, http_client_unauthenticated):
# Note: This test depends on the rascsi group existing and 'username' a member the group
response = http_client_unauthenticated.post(
"/login",
data={
"username": pytestconfig.getoption("rascsi_username"),
"password": pytestconfig.getoption("rascsi_password"),
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert "env" in response_data["data"]
# route("/login", methods=["POST"])
def test_login_with_invalid_credentials(http_client_unauthenticated):
response = http_client_unauthenticated.post(
"/login",
data={
"username": "__INVALID_USER__",
"password": "__INVALID_PASS__",
},
)
response_data = response.json()
assert response.status_code == 401
assert response_data["status"] == STATUS_ERROR
assert response_data["messages"][0]["message"] == (
"You must log in with valid credentials for a user in the 'rascsi' group"
)
# route("/logout")
def test_logout(http_client):
response = http_client.get("/logout")
assert response.status_code == 200

View File

@ -0,0 +1,263 @@
import pytest
from conftest import (
SCSI_ID,
FILE_SIZE_1_MIB,
STATUS_SUCCESS,
)
# route("/scsi/attach", methods=["POST"])
def test_attach_image(http_client, create_test_image, detach_devices):
test_image = create_test_image()
response = http_client.post(
"/scsi/attach",
data={
"file_name": test_image,
"file_size": FILE_SIZE_1_MIB,
"scsi_id": SCSI_ID,
"unit": 0,
"type": "SCHD",
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == (
f"Attached {test_image} as Hard Disk Drive to SCSI ID {SCSI_ID} LUN 0"
)
# Cleanup
detach_devices()
# route("/scsi/attach_device", methods=["POST"])
@pytest.mark.parametrize(
"device_name,device_config",
[
(
"Removable Disk Drive",
{
"type": "SCRM",
"drive_props": {
"vendor": "HD VENDOR",
"product": "HD PRODUCT",
"revision": "0123",
"block_size": "512",
},
},
),
(
"Magneto-Optical Drive",
{
"type": "SCMO",
"drive_props": {
"vendor": "MO VENDOR",
"product": "MO PRODUCT",
"revision": "0123",
"block_size": "512",
},
},
),
(
"CD/DVD Drive",
{
"type": "SCCD",
"drive_props": {
"vendor": "CD VENDOR",
"product": "CD PRODUCT",
"revision": "0123",
"block_size": "512",
},
},
),
# TODO: Find a portable way to detect network interfaces for testing
("Host Bridge", {"type": "SCBR", "param_inet": "192.168.0.1/24"}),
# TODO: Find a portable way to detect network interfaces for testing
("Ethernet Adapter", {"type": "SCDP", "param_inet": "192.168.0.1/24"}),
("Host Services", {"type": "SCHS"}),
("Printer", {"type": "SCLP", "param_timeout": 60, "param_cmd": "lp -fart %f"}),
],
)
def test_attach_device(env, http_client, detach_devices, device_name, device_config):
if env["is_docker"] and device_name == "Host Bridge":
pytest.skip("Test not supported in Docker environment.")
device_config["scsi_id"] = SCSI_ID
device_config["unit"] = 0
response = http_client.post(
"/scsi/attach_device",
data=device_config,
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == (
f"Attached {device_name} to SCSI ID {SCSI_ID} LUN 0"
)
# Cleanup
detach_devices()
# route("/scsi/detach", methods=["POST"])
def test_detach_device(http_client, create_test_image):
test_image = create_test_image()
http_client.post(
"/scsi/attach",
data={
"file_name": test_image,
"file_size": FILE_SIZE_1_MIB,
"scsi_id": SCSI_ID,
"unit": 0,
"type": "SCHD",
},
)
response = http_client.post(
"/scsi/detach",
data={
"scsi_id": SCSI_ID,
"unit": 0,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == f"Detached SCSI ID {SCSI_ID} LUN 0"
# route("/scsi/detach_all", methods=["POST"])
def test_detach_all_devices(http_client, create_test_image, list_attached_images):
test_images = []
scsi_ids = [4, 5, 6]
for scsi_id in scsi_ids:
test_image = create_test_image()
test_images.append(test_image)
http_client.post(
"/scsi/attach",
data={
"file_name": test_image,
"file_size": FILE_SIZE_1_MIB,
"scsi_id": scsi_id,
"unit": 0,
"type": "SCHD",
},
)
assert list_attached_images() == test_images
response = http_client.post("/scsi/detach_all")
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert list_attached_images() == []
# route("/scsi/eject", methods=["POST"])
def test_eject_device(http_client, create_test_image, detach_devices):
test_image = create_test_image()
http_client.post(
"/scsi/attach",
data={
"file_name": test_image,
"file_size": FILE_SIZE_1_MIB,
"scsi_id": SCSI_ID,
"unit": 0,
"type": "SCCD", # CD-ROM
},
)
response = http_client.post(
"/scsi/eject",
data={
"scsi_id": SCSI_ID,
"unit": 0,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == f"Ejected SCSI ID {SCSI_ID} LUN 0"
# Cleanup
detach_devices()
# route("/scsi/info", methods=["POST"])
def test_show_device_info(http_client, create_test_image, detach_devices):
test_image = create_test_image()
http_client.post(
"/scsi/attach",
data={
"file_name": test_image,
"file_size": FILE_SIZE_1_MIB,
"scsi_id": SCSI_ID,
"unit": 0,
"type": "SCHD",
},
)
response = http_client.post(
"/scsi/info",
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert "devices" in response_data["data"]
assert response_data["data"]["devices"][0]["file"] == test_image
# Cleanup
detach_devices()
# route("/scsi/reserve", methods=["POST"])
# route("/scsi/release", methods=["POST"])
def test_reserve_and_release_device(http_client):
scsi_id = 0
response = http_client.post(
"/scsi/reserve",
data={
"scsi_id": scsi_id,
"memo": "TEST",
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == f"Reserved SCSI ID {scsi_id}"
response = http_client.post(
"/scsi/release",
data={
"scsi_id": scsi_id,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == (
f"Released the reservation for SCSI ID {scsi_id}"
)

View File

@ -0,0 +1,310 @@
import pytest
import uuid
import os
from conftest import (
SCSI_ID,
FILE_SIZE_1_MIB,
STATUS_SUCCESS,
)
# route("/files/create", methods=["POST"])
def test_create_file(http_client, list_files, delete_file):
file_prefix = str(uuid.uuid4())
file_name = f"{file_prefix}.hds"
response = http_client.post(
"/files/create",
data={
"file_name": file_prefix,
"type": "hds",
"size": 1,
"drive_name": "DEC RZ22",
},
)
response_data = response.json()
assert response.status_code == 201
assert response_data["status"] == STATUS_SUCCESS
assert response_data["data"]["image"] == file_name
assert response_data["messages"][0]["message"] == f"Image file created: {file_name}"
assert file_name in list_files()
# Cleanup
delete_file(file_name)
# route("/files/rename", methods=["POST"])
def test_rename_file(http_client, create_test_image, list_files, delete_file):
original_file = create_test_image(auto_delete=False)
renamed_file = f"{uuid.uuid4()}.rename"
response = http_client.post(
"/files/rename",
data={"file_name": original_file, "new_file_name": renamed_file},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == f"Image file renamed to: {renamed_file}"
assert renamed_file in list_files()
# Cleanup
delete_file(renamed_file)
# route("/files/copy", methods=["POST"])
def test_copy_file(http_client, create_test_image, list_files, delete_file):
original_file = create_test_image()
copy_file = f"{uuid.uuid4()}.copy"
response = http_client.post(
"/files/copy",
data={
"file_name": original_file,
"copy_file_name": copy_file,
},
)
response_data = response.json()
files = list_files()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == f"Copy of image file saved as: {copy_file}"
assert original_file in files
assert copy_file in files
# Cleanup
delete_file(copy_file)
# route("/files/delete", methods=["POST"])
def test_delete_file(http_client, create_test_image, list_files):
file_name = create_test_image(auto_delete=False)
response = http_client.post("/files/delete", data={"file_name": file_name})
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == f"Image file deleted: {file_name}"
assert file_name not in list_files()
# route("/files/extract_image", methods=["POST"])
@pytest.mark.parametrize(
"archive_file_name,image_file_name",
[
("test_image.zip", "test_image_from_zip.hds"),
("test_image.sit", "test_image_from_sit.hds"),
("test_image.7z", "test_image_from_7z.hds"),
],
)
def test_extract_file(
httpserver, http_client, list_files, delete_file, archive_file_name, image_file_name
):
http_path = f"/images/{archive_file_name}"
url = httpserver.url_for(http_path)
with open(f"tests/assets/{archive_file_name}", mode="rb") as file:
zip_file_data = file.read()
httpserver.expect_request(http_path).respond_with_data(
zip_file_data,
mimetype="application/octet-stream",
)
http_client.post(
"/files/download_url",
data={
"destination": "images",
"url": url,
},
)
response = http_client.post(
"/files/extract_image",
data={
"archive_file": archive_file_name,
"archive_members": image_file_name,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == "Extracted 1 file(s)"
assert image_file_name in list_files()
# Cleanup
delete_file(archive_file_name)
delete_file(image_file_name)
# route("/files/upload", methods=["POST"])
def test_upload_file(http_client, delete_file):
file_name = f"{uuid.uuid4()}.test"
with open("tests/assets/test_image.hds", mode="rb") as file:
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0, 0)
number_of_chunks = 4
# Note: The test file needs to be cleanly divisible by the chunk size
chunk_size = int(file_size / number_of_chunks)
for chunk_number in range(0, 4):
if chunk_number == 0:
chunk_byte_offset = 0
else:
chunk_byte_offset = chunk_number * chunk_size
form_data = {
"dzuuid": str(uuid.uuid4()),
"dzchunkindex": chunk_number,
"dzchunksize": chunk_size,
"dzchunkbyteoffset": chunk_byte_offset,
"dztotalfilesize": file_size,
"dztotalchunkcount": number_of_chunks,
}
file_data = {"file": (file_name, file.read(chunk_size))}
response = http_client.post(
"/files/upload",
data=form_data,
files=file_data,
)
assert response.status_code == 200
assert response.text == "File upload successful!"
file = [f for f in http_client.get("/").json()["data"]["files"] if f["name"] == file_name][0]
assert file["size"] == file_size
# Cleanup
delete_file(file_name)
# route("/files/download", methods=["POST"])
def test_download_file(http_client, create_test_image):
file_name = create_test_image()
response = http_client.post("/files/download", data={"file": file_name})
assert response.status_code == 200
assert response.headers["content-type"] == "application/octet-stream"
assert response.headers["content-disposition"] == f"attachment; filename={file_name}"
assert response.headers["content-length"] == str(FILE_SIZE_1_MIB)
# route("/files/download_url", methods=["POST"])
def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_file):
file_name = str(uuid.uuid4())
http_path = f"/images/{file_name}"
url = httpserver.url_for(http_path)
with open("tests/assets/test_image.hds", mode="rb") as file:
test_file_data = file.read()
httpserver.expect_request(http_path).respond_with_data(
test_file_data,
mimetype="application/octet-stream",
)
response = http_client.post(
"/files/download_url",
data={
"destination": "images",
"url": url,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert file_name in list_files()
assert (
response_data["messages"][0]["message"] == f"{file_name} downloaded to {env['images_dir']}"
)
# Cleanup
delete_file(file_name)
# route("/files/download_to_iso", methods=["POST"])
def test_download_url_to_iso(
env,
httpserver,
http_client,
list_files,
list_attached_images,
detach_devices,
delete_file,
):
test_file_name = str(uuid.uuid4())
iso_file_name = f"{test_file_name}.iso"
http_path = f"/images/{test_file_name}"
url = httpserver.url_for(http_path)
with open("tests/assets/test_image.hds", mode="rb") as file:
test_file_data = file.read()
httpserver.expect_request(http_path).respond_with_data(
test_file_data,
mimetype="application/octet-stream",
)
response = http_client.post(
"/files/download_to_iso",
data={
"scsi_id": SCSI_ID,
"type": "-hfs",
"url": url,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert iso_file_name in list_files()
assert iso_file_name in list_attached_images()
m = response_data["messages"]
assert m[0]["message"] == 'Created CD-ROM ISO image with arguments "-hfs"'
assert m[1]["message"] == f"Saved image as: {env['images_dir']}/{iso_file_name}"
assert m[2]["message"] == f"Attached to SCSI ID {SCSI_ID}"
# Cleanup
detach_devices()
delete_file(iso_file_name)
# route("/files/diskinfo", methods=["POST"])
def test_show_diskinfo(http_client, create_test_image):
test_image = create_test_image()
response = http_client.post(
"/files/diskinfo",
data={
"file_name": test_image,
},
)
response_data = response.json()
assert response.status_code == 200
assert "Regular file" in response_data["data"]["diskinfo"]

View File

@ -0,0 +1,98 @@
import uuid
from conftest import (
FILE_SIZE_1_MIB,
STATUS_SUCCESS,
)
# route("/")
def test_index(http_client):
response = http_client.get("/")
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert "devices" in response_data["data"]
# route("/env")
def test_get_env_info(http_client):
response = http_client.get("/env")
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert "running_env" in response_data["data"]
# route("/pwa/<path:pwa_path>")
def test_pwa_route(http_client):
response = http_client.get("/pwa/favicon.ico")
assert response.status_code == 200
assert response.headers["content-disposition"] == "inline; filename=favicon.ico"
# route("/drive/list", methods=["GET"])
def test_show_named_drive_presets(http_client):
response = http_client.get("/drive/list")
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert "files" in response_data["data"]
# route("/drive/cdrom", methods=["POST"])
def test_create_cdrom_properties_file(env, http_client):
file_name = f"{uuid.uuid4()}.iso"
response = http_client.post(
"/drive/cdrom",
data={
"drive_name": "Sony CDU-8012",
"file_name": file_name,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == (
f"File created: {env['cfg_dir']}/{file_name}.properties"
)
# route("/drive/create", methods=["POST"])
def test_create_image_with_properties_file(http_client, delete_file):
file_prefix = str(uuid.uuid4())
file_name = f"{file_prefix}.hds"
response = http_client.post(
"/drive/create",
data={
"drive_name": "Miniscribe M8425",
"size": FILE_SIZE_1_MIB,
"file_name": file_prefix,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == f"Image file created: {file_name}"
# Cleanup
delete_file(file_name)
# route("/sys/manpage", methods=["POST"])
def test_show_manpage(http_client):
response = http_client.get("/sys/manpage?app=rascsi")
response_data = response.json()
assert response.status_code == 200
assert "rascsi" in response_data["data"]["manpage"]

View File

@ -0,0 +1,152 @@
import pytest
import uuid
from conftest import STATUS_SUCCESS
# route("/language", methods=["POST"])
@pytest.mark.parametrize(
"locale,confirm_message",
[
("de", "Webinterface-Sprache auf Deutsch geändert"),
("es", "Se ha cambiado el lenguaje de la Interfaz Web a español"),
("fr", "Langue de linterface web changée pour français"),
("sv", "Bytte webbgränssnittets språk till svenska"),
("en", "Changed Web Interface language to English"),
],
)
def test_set_language(http_client, locale, confirm_message):
response = http_client.post(
"/language",
data={
"locale": locale,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == confirm_message
# route("/logs/level", methods=["POST"])
@pytest.mark.parametrize("level", ["trace", "debug", "info", "warn", "err", "critical", "off"])
def test_set_log_level(http_client, level):
response = http_client.post(
"/logs/level",
data={
"level": level,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == f"Log level set to {level}"
# Cleanup
http_client.post(
"/logs/level",
data={
"level": "debug",
},
)
# route("/logs/show", methods=["POST"])
def test_show_logs(http_client):
response = http_client.post(
"/logs/show",
data={
"lines": 100,
"scope": "rascsi",
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["data"]["lines"] == "100"
assert response_data["data"]["scope"] == "rascsi"
# route("/config/save", methods=["POST"])
# route("/config/load", methods=["POST"])
def test_save_load_and_delete_configs(env, http_client):
config_name = str(uuid.uuid4())
config_json_file = f"{config_name}.json"
reserved_scsi_id = 0
reservation_memo = str(uuid.uuid4())
# Confirm the initial state
assert http_client.get("/").json()["data"]["RESERVATIONS"][0] == ""
# Save the initial state to a config
response = http_client.post(
"/config/save",
data={
"name": config_name,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == (
f"File created: {env['cfg_dir']}/{config_json_file}"
)
assert config_json_file in http_client.get("/").json()["data"]["config_files"]
# Modify the state
http_client.post(
"/scsi/reserve",
data={
"scsi_id": reserved_scsi_id,
"memo": reservation_memo,
},
)
assert http_client.get("/").json()["data"]["RESERVATIONS"][0] == reservation_memo
# Load the saved config
response = http_client.post(
"/config/load",
data={
"name": config_json_file,
"load": True,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == (
f"Loaded configurations from: {config_json_file}"
)
# Confirm the application has returned to its initial state
assert http_client.get("/").json()["data"]["RESERVATIONS"][0] == ""
# Delete the saved config
response = http_client.post(
"/config/load",
data={
"name": config_json_file,
"delete": True,
},
)
response_data = response.json()
assert response.status_code == 200
assert response_data["status"] == STATUS_SUCCESS
assert response_data["messages"][0]["message"] == (
f"File deleted: {env['cfg_dir']}/{config_json_file}"
)
assert config_json_file not in http_client.get("/").json()["data"]["config_files"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,82 @@
import pytest
import requests
import socket
import os
def pytest_addoption(parser):
default_base_url = "http://rascsi_web" if os.getenv("DOCKER") else "http://localhost:8080"
parser.addoption("--home_dir", action="store", default="/home/pi")
parser.addoption("--base_url", action="store", default=default_base_url)
parser.addoption("--httpserver_host", action="store", default=socket.gethostname())
parser.addoption("--httpserver_listen_address", action="store", default="0.0.0.0")
parser.addoption("--rascsi_username", action="store", default="pi")
parser.addoption("--rascsi_password", action="store", default="rascsi")
@pytest.fixture(scope="session")
def env(pytestconfig):
home_dir = pytestconfig.getoption("home_dir")
return {
"is_docker": bool(os.getenv("DOCKER")),
"home_dir": home_dir,
"cfg_dir": f"{home_dir}/.config/rascsi",
"images_dir": f"{home_dir}/images",
"afp_dir": f"{home_dir}/afpshare",
}
@pytest.fixture(scope="session")
def httpserver_listen_address(pytestconfig):
return (pytestconfig.getoption("httpserver_listen_address"), 0)
@pytest.fixture(scope="function", autouse=True)
def set_httpserver_hostname(pytestconfig, httpserver):
# We need httpserver.url_for() to generate URLs pointing to the correct host
httpserver.host = pytestconfig.getoption("httpserver_host")
@pytest.fixture(scope="session", autouse=True)
def ensure_all_devices_detached(create_http_client):
http_client = create_http_client()
http_client.post("/scsi/detach_all")
@pytest.fixture(scope="session")
def create_http_client(pytestconfig):
def create(authenticate=True):
session = requests.Session()
session.headers.update({"Accept": "application/json"})
session.original_request = session.request
def relative_request(method, url, *args, **kwargs):
if url[:4] != "http":
url = pytestconfig.getoption("base_url") + url
return session.original_request(method, url, *args, **kwargs)
session.request = relative_request
if authenticate:
session.post(
"/login",
data={
"username": pytestconfig.getoption("rascsi_username"),
"password": pytestconfig.getoption("rascsi_password"),
},
)
return session
return create
@pytest.fixture(scope="function")
def http_client(create_http_client):
return create_http_client(authenticate=True)
@pytest.fixture(scope="function")
def http_client_unauthenticated(create_http_client):
return create_http_client(authenticate=False)

View File

@ -7,14 +7,10 @@
*.vcd
*.json
*.html
rascsi
scsimon
rasctl
sasidump
rasdump
scisparse
rascsi.dat
obj
bin
coverage
/rascsi_interface.pb.cpp
/rascsi_interface.pb.h
.project

View File

@ -1,5 +1,8 @@
.DEFAULT_GOAL: all
# Depending on the GCC version the compilation flags differ
GCCVERSION10 := $(shell expr `gcc -dumpversion` \>= 10)
## Optional build flags:
## CROSS_COMPILE : Specify which compiler toolchain to use.
## To cross compile set this accordingly, e.g. to:
@ -14,14 +17,12 @@ CXX = $(CROSS_COMPILE)g++
## this is only used by developers.
DEBUG ?= 0
ifeq ($(DEBUG), 1)
# Debug CFLAGS
CFLAGS += -O0 -g -Wall -DDEBUG
CXXFLAGS += -O0 -g -Wall -DDEBUG
# Debug compiler flags
CXXFLAGS += -O0 -g -Wall -Wextra -DDEBUG
BUILD_TYPE = Debug
else
# Release CFLAGS
CFLAGS += -O3 -Wall -Werror -DNDEBUG
CXXFLAGS += -O3 -Wall -Werror -DNDEBUG
# Release compiler flags
CXXFLAGS += -O3 -Wall -Werror -Wextra -DNDEBUG
BUILD_TYPE = Release
endif
ifeq ("$(shell uname -s)","Linux")
@ -29,23 +30,17 @@ ifeq ("$(shell uname -s)","Linux")
CXXFLAGS += -Wno-psabi
endif
CFLAGS += -iquote . -D_FILE_OFFSET_BITS=64 -MD -MP
CXXFLAGS += -std=c++17 -iquote . -D_FILE_OFFSET_BITS=64 -MD -MP
CXXFLAGS += -std=c++17 -iquote . -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -MD -MP
## EXTRA_FLAGS : Can be used to pass special purpose flags
CFLAGS += $(EXTRA_FLAGS)
CXXFLAGS += $(EXTRA_FLAGS)
# If we're using GCC version 10 or later, we need to add the FMT_HEADER_ONLY definition
GCCVERSION10 := $(shell expr `gcc -dumpversion` \>= 10)
ifeq "$(GCCVERSION10)" "1"
CFLAGS += -DFMT_HEADER_ONLY
CXXFLAGS += -DFMT_HEADER_ONLY
endif
## CONNECT_TYPE=FULLSPEC : Specify the type of RaSCSI board type
## that you are using. The typical options are
## STANDARD or FULLSPEC. The default is FULLSPEC
@ -55,14 +50,12 @@ endif
CONNECT_TYPE ?= FULLSPEC
ifdef CONNECT_TYPE
CFLAGS += -DCONNECT_TYPE_$(CONNECT_TYPE)
CXXFLAGS += -DCONNECT_TYPE_$(CONNECT_TYPE)
endif
RASCSI = rascsi
RASCTL = rasctl
RASDUMP = rasdump
SASIDUMP = sasidump
SCSIMON = scsimon
RASCSI_TEST = rascsi_test
@ -73,6 +66,8 @@ RSYSLOG_LOG = /var/log/rascsi.log
USR_LOCAL_BIN = /usr/local/bin
MAN_PAGE_DIR = /usr/local/man/man1
DOC_DIR = ../../doc
COVERAGE_DIR = ./coverage
COVERAGE_FILE = rascsi.dat
OS_FILES = ./os_integration
OBJDIR := ./obj/$(shell echo $(CONNECT_TYPE) | tr '[:upper:]' '[:lower:]')
@ -82,8 +77,7 @@ BIN_ALL = \
$(BINDIR)/$(RASCSI) \
$(BINDIR)/$(RASCTL) \
$(BINDIR)/$(SCSIMON) \
$(BINDIR)/$(RASDUMP) \
$(BINDIR)/$(SASIDUMP)
$(BINDIR)/$(RASDUMP)
SRC_PROTOC = \
rascsi_interface.proto
@ -91,80 +85,67 @@ SRC_PROTOC = \
SRC_PROTOBUF = \
rascsi_interface.pb.cpp
SRC_RASCSI_CORE = scsi.cpp \
gpiobus.cpp \
filepath.cpp \
fileio.cpp \
SRC_SHARED = \
rascsi_version.cpp \
rascsi_image.cpp \
rascsi_response.cpp \
rasutil.cpp \
protobuf_util.cpp \
localizer.cpp
protobuf_serializer.cpp
SRC_RASCSI_CORE = \
bus.cpp \
filepath.cpp \
fileio.cpp
SRC_RASCSI_CORE += $(shell find ./rascsi -name '*.cpp')
SRC_RASCSI_CORE += $(shell find ./controllers -name '*.cpp')
SRC_RASCSI_CORE += $(shell find ./devices -name '*.cpp')
SRC_RASCSI_CORE += $(SRC_PROTOBUF)
SRC_RASCSI_CORE += $(shell find ./hal -name '*.cpp')
SRC_RASCSI = rascsi.cpp
SRC_RASCSI += $(SRC_RASCSI_CORE)
SRC_SCSIMON = \
scsimon.cpp \
scsi.cpp \
gpiobus.cpp \
bus.cpp \
rascsi_version.cpp
SRC_SCSIMON += $(shell find ./monitor -name '*.cpp')
SRC_SCSIMON += $(shell find ./hal -name '*.cpp')
SRC_RASCTL = \
rasctl.cpp\
rasctl_commands.cpp \
rasctl_display.cpp \
rascsi_version.cpp \
rasutil.cpp \
protobuf_util.cpp \
localizer.cpp
SRC_RASCTL += $(SRC_PROTOBUF)
SRC_RASCTL_CORE = $(shell find ./rasctl -name '*.cpp')
SRC_RASCTL = rasctl.cpp
SRC_RASDUMP = \
rasdump.cpp \
scsi.cpp \
gpiobus.cpp \
bus.cpp \
filepath.cpp \
fileio.cpp \
rascsi_version.cpp
SRC_SASIDUMP = \
sasidump.cpp \
scsi.cpp \
gpiobus.cpp \
filepath.cpp \
fileio.cpp \
rascsi_version.cpp
SRC_RASDUMP += $(shell find ./hal -name '*.cpp')
SRC_RASCSI_TEST = $(shell find ./test -name '*.cpp')
SRC_RASCSI_TEST += $(SRC_RASCSI_CORE)
vpath %.h ./ ./controllers ./devices ./monitor
vpath %.cpp ./ ./controllers ./devices ./monitor ./test
vpath %.h ./ ./controllers ./devices ./monitor ./hal ./rascsi ./rasctl
vpath %.cpp ./ ./controllers ./devices ./monitor ./test ./hal ./rascsi ./rasctl
vpath %.o ./$(OBJDIR)
vpath ./$(BINDIR)
OBJ_RASCSI_CORE := $(addprefix $(OBJDIR)/,$(notdir $(SRC_RASCSI_CORE:%.cpp=%.o)))
OBJ_RASCSI := $(addprefix $(OBJDIR)/,$(notdir $(SRC_RASCSI:%.cpp=%.o)))
OBJ_RASCTL_CORE := $(addprefix $(OBJDIR)/,$(notdir $(SRC_RASCTL_CORE:%.cpp=%.o)))
OBJ_RASCTL := $(addprefix $(OBJDIR)/,$(notdir $(SRC_RASCTL:%.cpp=%.o)))
OBJ_RASDUMP := $(addprefix $(OBJDIR)/,$(notdir $(SRC_RASDUMP:%.cpp=%.o)))
OBJ_SASIDUMP := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SASIDUMP:%.cpp=%.o)))
OBJ_SCSIMON := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SCSIMON:%.cpp=%.o)))
OBJ_RASCSI_TEST := $(addprefix $(OBJDIR)/,$(notdir $(SRC_RASCSI_TEST:%.cpp=%.o)))
OBJ_SHARED := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SHARED:%.cpp=%.o)))
OBJ_PROTOBUF := $(addprefix $(OBJDIR)/,$(notdir $(SRC_PROTOBUF:%.cpp=%.o)))
GEN_PROTOBUF := $(SRC_PROTOBUF) rascsi_interface.pb.h
# The following will include all of the auto-generated dependency files (*.d)
# if they exist. This will trigger a rebuild of a source file if a header changes
ALL_DEPS := $(patsubst %.o,%.d,$(OBJ_RASCSI) $(OBJ_RASCTL) $(OBJ_SCSIMON) $(OBJ_RASCSI_TEST))
ALL_DEPS := $(patsubst %.o,%.d,$(OBJ_RASCSI_CORE) $(OBJ_RASCTL_CORE) $(OBJ_RASCSI) $(OBJ_RASCTL) $(OBJ_SCSIMON) $(OBJ_RASCSI_TEST))
-include $(ALL_DEPS)
$(OBJDIR) $(BINDIR):
@ -183,41 +164,51 @@ $(SRC_PROTOBUF): $(SRC_PROTOC)
## all : Rebuild all of the executable files and re-generate
## the text versions of the manpages
## docs : Re-generate the text versions of the man pages
## test : Build and run unit tests
## coverage : Build and run unit tests and create coverage SonarQube files.
## lcov : Build and run unit tests and create coverage HTML files.
## Note that you have to run 'make clean' before switching
## between coverage and non-coverage builds.
.DEFAULT_GOAL := all
.PHONY: all ALL docs
.PHONY: all ALL docs test coverage lcov
all: $(BIN_ALL) docs
ALL: all
test: $(BINDIR)/$(RASCSI_TEST)
$(BINDIR)/$(RASCSI_TEST)
coverage: CXXFLAGS += --coverage
coverage: test
lcov: CXXFLAGS += --coverage
lcov: test
lcov -q -c -d . --include '*/raspberrypi/*' -o $(COVERAGE_FILE) --exclude '*/test/*' --exclude '*/interfaces/*' --exclude '*/rascsi_interface.pb*'
genhtml -q -o $(COVERAGE_DIR) --legend $(COVERAGE_FILE)
docs: $(DOC_DIR)/rascsi_man_page.txt $(DOC_DIR)/rasctl_man_page.txt $(DOC_DIR)/scsimon_man_page.txt
$(BINDIR)/$(RASCSI): $(SRC_PROTOBUF) $(OBJ_RASCSI) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCSI) -lpthread -lpcap -lprotobuf -lstdc++fs
$(SRC_SHARED): $(SRC_PROTOBUF)
$(BINDIR)/$(RASCTL): $(SRC_PROTOBUF) $(OBJ_RASCTL) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCTL) -lpthread -lprotobuf -lstdc++fs
$(BINDIR)/$(RASCSI): $(SRC_PROTOBUF) $(OBJ_RASCSI_CORE) $(OBJ_RASCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCSI_CORE) $(OBJ_RASCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) -lpthread -lpcap -lprotobuf -lstdc++fs
$(BINDIR)/$(RASCTL): $(SRC_PROTOBUF) $(OBJ_RASCTL_CORE) $(OBJ_RASCTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCTL_CORE) $(OBJ_RASCTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) -lpthread -lprotobuf
$(BINDIR)/$(RASDUMP): $(OBJ_RASDUMP) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASDUMP)
$(BINDIR)/$(SASIDUMP): $(OBJ_SASIDUMP) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SASIDUMP)
$(BINDIR)/$(SCSIMON): $(OBJ_SCSIMON) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SCSIMON) -lpthread
$(BINDIR)/$(RASCSI_TEST): $(SRC_PROTOBUF) $(OBJ_RASCSI_TEST) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCSI_TEST) -lpcap -lprotobuf -lgmock -lgtest -lgtest_main
$(BINDIR)/$(RASCSI_TEST): $(SRC_PROTOBUF) $(OBJ_RASCSI_CORE) $(OBJ_RASCTL_CORE) $(OBJ_RASCSI_TEST) $(OBJ_RASCTL_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCSI_CORE) $(OBJ_RASCTL_CORE) $(OBJ_RASCSI_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) -lpthread -lpcap -lprotobuf -lstdc++fs -lgmock -lgtest
# Phony rules for building individual utilities
.PHONY: $(RASCSI) $(RASCTL) $(RASDUMP) $(SASIDUMP) $(SCSIMON)
.PHONY: $(RASCSI) $(RASCTL) $(RASDUMP) $(SCSIMON)
$(RASCSI) : $(BINDIR)/$(RASCSI)
$(RASCTL) : $(BINDIR)/$(RASCTL)
$(RASDUMP) : $(BINDIR)/$(RASDUMP)
$(SASIDUMP): $(BINDIR)/$(SASIDUMP)
$(SCSIMON) : $(BINDIR)/$(SCSIMON)
@ -225,7 +216,7 @@ $(SCSIMON) : $(BINDIR)/$(SCSIMON)
## compiler files and executable files
.PHONY: clean
clean:
rm -rf $(OBJDIR) $(BINDIR) $(GEN_PROTOBUF)
rm -rf $(OBJDIR) $(BINDIR) $(GEN_PROTOBUF) $(COVERAGE_DIR) $(COVERAGE_FILE)
## install : Copies all of the man pages to the correct location
## Copies the binaries to a global install location
@ -245,12 +236,10 @@ install: \
$(MAN_PAGE_DIR)/rasctl.1 \
$(MAN_PAGE_DIR)/scsimon.1 \
$(MAN_PAGE_DIR)/rasdump.1 \
$(MAN_PAGE_DIR)/sasidump.1 \
$(USR_LOCAL_BIN)/$(RASCTL) \
$(USR_LOCAL_BIN)/$(RASCSI) \
$(USR_LOCAL_BIN)/$(SCSIMON) \
$(USR_LOCAL_BIN)/$(RASDUMP) \
$(USR_LOCAL_BIN)/$(SASIDUMP) \
$(SYSTEMD_CONF) \
$(RSYSLOG_CONF) \
$(RSYSLOG_LOG)

85
src/raspberrypi/bus.cpp Normal file
View File

@ -0,0 +1,85 @@
//---------------------------------------------------------------------------
//
// X68000 EMULATOR "XM6"
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
// Copyright (C) 2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "bus.h"
using namespace std;
//---------------------------------------------------------------------------
//
// Phase Acquisition
//
//---------------------------------------------------------------------------
BUS::phase_t BUS::GetPhase()
{
// Selection Phase
if (GetSEL()) {
return phase_t::selection;
}
// Bus busy phase
if (!GetBSY()) {
return phase_t::busfree;
}
// Get target phase from bus signal line
int mci = GetMSG() ? 0b100 : 0b000;
mci |= GetCD() ? 0b010 : 0b000;
mci |= GetIO() ? 0b001 : 0b000;
return GetPhase(mci);
}
//---------------------------------------------------------------------------
//
// Determine Phase String phase enum
//
//---------------------------------------------------------------------------
const char* BUS::GetPhaseStrRaw(phase_t current_phase) {
const auto& it = phase_str_mapping.find(current_phase);
return it != phase_str_mapping.end() ? it->second : "INVALID";
}
//---------------------------------------------------------------------------
//
// Phase Table
// Reference Table 8: https://www.staff.uni-mainz.de/tacke/scsi/SCSI2-06.html
// This determines the phase based upon the Msg, C/D and I/O signals.
//
//---------------------------------------------------------------------------
const array<BUS::phase_t, 8> BUS::phase_table = {
// | MSG|C/D|I/O |
phase_t::dataout, // | 0 | 0 | 0 |
phase_t::datain, // | 0 | 0 | 1 |
phase_t::command, // | 0 | 1 | 0 |
phase_t::status, // | 0 | 1 | 1 |
phase_t::reserved, // | 1 | 0 | 0 |
phase_t::reserved, // | 1 | 0 | 1 |
phase_t::msgout, // | 1 | 1 | 0 |
phase_t::msgin // | 1 | 1 | 1 |
};
//---------------------------------------------------------------------------
//
// Phase string to phase mapping
//
//---------------------------------------------------------------------------
const unordered_map<BUS::phase_t, const char*> BUS::phase_str_mapping = {
{ phase_t::busfree, "busfree" },
{ phase_t::arbitration, "arbitration" },
{ phase_t::selection, "selection" },
{ phase_t::reselection, "reselection" },
{ phase_t::command, "command" },
{ phase_t::datain, "datain" },
{ phase_t::dataout, "dataout" },
{ phase_t::status, "status" },
{ phase_t::msgin, "msgin" },
{ phase_t::msgout, "msgout" },
{ phase_t::reserved, "reserved" }
};

113
src/raspberrypi/bus.h Normal file
View File

@ -0,0 +1,113 @@
//---------------------------------------------------------------------------
//
// X68000 EMULATOR "XM6"
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
//
//---------------------------------------------------------------------------
#pragma once
#include "os.h"
#include "scsi.h"
#include <array>
#include <unordered_map>
using namespace std;
class BUS
{
public:
// Operation modes definition
enum class mode_e {
TARGET = 0,
INITIATOR = 1,
MONITOR = 2,
};
// Phase definitions
enum class phase_t : int {
busfree,
arbitration,
selection,
reselection,
command,
datain,
dataout,
status,
msgin,
msgout,
reserved
};
BUS() = default;
virtual ~BUS() = default;
// Basic Functions
virtual bool Init(mode_e mode) = 0;
virtual void Reset() = 0;
virtual void Cleanup() = 0;
phase_t GetPhase();
static phase_t GetPhase(int mci)
{
return phase_table[mci];
}
// Get the string phase name, based upon the raw data
static const char* GetPhaseStrRaw(phase_t current_phase);
// Extract as specific pin field from a raw data capture
static inline uint32_t GetPinRaw(uint32_t raw_data, uint32_t pin_num)
{
return ((raw_data >> pin_num) & 1);
}
virtual bool GetBSY() const = 0;
virtual void SetBSY(bool ast) = 0;
virtual bool GetSEL() const = 0;
virtual void SetSEL(bool ast) = 0;
virtual bool GetATN() const = 0;
virtual void SetATN(bool ast) = 0;
virtual bool GetACK() const = 0;
virtual void SetACK(bool ast) = 0;
virtual bool GetRST() const = 0;
virtual void SetRST(bool ast) = 0;
virtual bool GetMSG() const = 0;
virtual void SetMSG(bool ast) = 0;
virtual bool GetCD() const = 0;
virtual void SetCD(bool ast) = 0;
virtual bool GetIO() = 0;
virtual void SetIO(bool ast) = 0;
virtual bool GetREQ() const = 0;
virtual void SetREQ(bool ast) = 0;
virtual BYTE GetDAT() = 0;
virtual void SetDAT(BYTE dat) = 0;
virtual bool GetDP() const = 0; // Get parity signal
virtual uint32_t Acquire() = 0;
virtual int CommandHandShake(BYTE *buf) = 0;
virtual int ReceiveHandShake(BYTE *buf, int count) = 0;
virtual int SendHandShake(BYTE *buf, int count, int delay_after_bytes) = 0;
virtual bool GetSignal(int pin) const = 0;
// Get SCSI input signal value
virtual void SetSignal(int pin, bool ast) = 0;
// Set SCSI output signal value
static const int SEND_NO_DELAY = -1;
// Passed into SendHandShake when we don't want to delay
private:
static const array<phase_t, 8> phase_table;
static const unordered_map<phase_t, const char *> phase_str_mapping;
};

View File

@ -0,0 +1,87 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "log.h"
#include "rascsi_interface.pb.h"
#include "protobuf_serializer.h"
#include "command_util.h"
#include <sstream>
using namespace std;
using namespace rascsi_interface;
#define FPRT(fp, ...) fprintf(fp, __VA_ARGS__ )
static const char COMPONENT_SEPARATOR = ':';
static const char KEY_VALUE_SEPARATOR = '=';
void command_util::ParseParameters(PbDeviceDefinition& device, const string& params)
{
if (params.empty()) {
return;
}
// Old style parameters, for backwards compatibility only.
// Only one of these parameters will be used by rascsi, depending on the device type.
if (params.find(KEY_VALUE_SEPARATOR) == string::npos) {
AddParam(device, "file", params);
if (params != "bridge" && params != "daynaport" && params != "printer" && params != "services") {
AddParam(device, "interfaces", params);
}
return;
}
stringstream ss(params);
string p;
while (getline(ss, p, COMPONENT_SEPARATOR)) {
if (!p.empty()) {
size_t separator_pos = p.find(KEY_VALUE_SEPARATOR);
if (separator_pos != string::npos) {
AddParam(device, p.substr(0, separator_pos), string_view(p).substr(separator_pos + 1));
}
}
}
}
string command_util::GetParam(const PbCommand& command, const string& key)
{
const auto& it = command.params().find(key);
return it != command.params().end() ? it->second : "";
}
string command_util::GetParam(const PbDeviceDefinition& device, const string& key)
{
const auto& it = device.params().find(key);
return it != device.params().end() ? it->second : "";
}
void command_util::AddParam(PbCommand& command, const string& key, string_view value)
{
if (!key.empty() && !value.empty()) {
auto& map = *command.mutable_params();
map[key] = value;
}
}
void command_util::AddParam(PbDevice& device, const string& key, string_view value)
{
if (!key.empty() && !value.empty()) {
auto& map = *device.mutable_params();
map[key] = value;
}
}
void command_util::AddParam(PbDeviceDefinition& device, const string& key, string_view value)
{
if (!key.empty() && !value.empty()) {
auto& map = *device.mutable_params();
map[key] = value;
}
}

View File

@ -18,9 +18,8 @@
//
//---------------------------------------------------------------------------
#define USE_SEL_EVENT_ENABLE // Check SEL signal by event
#define REMOVE_FIXED_SASIHD_SIZE // remove the size limitation of SASIHD
// This avoids an indefinite loop with warnings if there is no RaSCSI hardware
// and thus helps with running certain tests on X86 hardware.
#if defined(__x86_64__) || defined(__X86__)
// and thus helps with running rasctl and unit test on x86 hardware.
#if defined(__x86_64__) || defined(__X86__) || !defined(__linux__)
#undef USE_SEL_EVENT_ENABLE
#endif

View File

@ -0,0 +1,136 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "rascsi_exceptions.h"
#include "devices/primary_device.h"
#include "abstract_controller.h"
void AbstractController::AllocateBuffer(size_t size)
{
if (size > ctrl.buffer.size()) {
ctrl.buffer.resize(size);
}
}
unordered_set<shared_ptr<PrimaryDevice>> AbstractController::GetDevices() const
{
unordered_set<shared_ptr<PrimaryDevice>> devices;
for (const auto& [id, lun] : luns) {
devices.insert(lun);
}
return devices;
}
shared_ptr<PrimaryDevice> AbstractController::GetDeviceForLun(int lun) const {
const auto& it = luns.find(lun);
return it == luns.end() ? nullptr : it->second;
}
void AbstractController::Reset()
{
SetPhase(BUS::phase_t::busfree);
ctrl.status = status::GOOD;
ctrl.message = 0x00;
ctrl.blocks = 0;
ctrl.next = 0;
ctrl.offset = 0;
ctrl.length = 0;
// Reset all LUNs
for (const auto& [lun, device] : luns) {
device->Reset();
}
}
void AbstractController::ProcessPhase()
{
switch (GetPhase()) {
case BUS::phase_t::busfree:
BusFree();
break;
case BUS::phase_t::selection:
Selection();
break;
case BUS::phase_t::dataout:
DataOut();
break;
case BUS::phase_t::datain:
DataIn();
break;
case BUS::phase_t::command:
Command();
break;
case BUS::phase_t::status:
Status();
break;
case BUS::phase_t::msgout:
MsgOut();
break;
case BUS::phase_t::msgin:
MsgIn();
break;
default:
LOGERROR("Cannot process phase %s", BUS::GetPhaseStrRaw(GetPhase()))
throw scsi_exception();
break;
}
}
bool AbstractController::AddDevice(shared_ptr<PrimaryDevice> device)
{
if (device->GetLun() < 0 || device->GetLun() >= GetMaxLuns() || HasDeviceForLun(device->GetLun())) {
return false;
}
luns[device->GetLun()] = device;
device->SetController(this);
return true;
}
bool AbstractController::DeleteDevice(const shared_ptr<PrimaryDevice> device)
{
return luns.erase(device->GetLun()) == 1;
}
bool AbstractController::HasDeviceForLun(int lun) const
{
return luns.find(lun) != luns.end();
}
int AbstractController::ExtractInitiatorId(int id_data) const
{
int initiator_id = -1;
if (int tmp = id_data - (1 << target_id); tmp) {
initiator_id = 0;
for (int j = 0; j < 8; j++) {
tmp >>= 1;
if (tmp) {
initiator_id++;
}
else {
break;
}
}
}
return initiator_id;
}

View File

@ -0,0 +1,112 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2022 Uwe Seimet
//
// Base class for device controllers
//
//---------------------------------------------------------------------------
#pragma once
#include "scsi.h"
#include "bus.h"
#include "phase_handler.h"
#include <unordered_set>
#include <unordered_map>
#include <vector>
#include <memory>
using namespace std;
class PrimaryDevice;
class AbstractController : public PhaseHandler
{
friend class PrimaryDevice;
friend class ScsiController;
// Logical units of this controller mapped to their LUN numbers
unordered_map<int, shared_ptr<PrimaryDevice>> luns;
public:
enum class rascsi_shutdown_mode {
NONE,
STOP_RASCSI,
STOP_PI,
RESTART_PI
};
using ctrl_t = struct _ctrl_t {
vector<int> cmd; // Command data, dynamically allocated per received command
scsi_defs::status status; // Status data
int message; // Message data
// Transfer
vector<BYTE> buffer; // Transfer data buffer
uint32_t blocks; // Number of transfer blocks
uint64_t next; // Next record
uint32_t offset; // Transfer offset
uint32_t length; // Transfer remaining length
};
AbstractController(BUS& bus, int target_id, int max_luns) : target_id(target_id), bus(bus), max_luns(max_luns) {}
~AbstractController() override = default;
virtual void Error(scsi_defs::sense_key, scsi_defs::asc = scsi_defs::asc::NO_ADDITIONAL_SENSE_INFORMATION,
scsi_defs::status = scsi_defs::status::CHECK_CONDITION) = 0;
virtual void Reset();
virtual int GetInitiatorId() const = 0;
virtual void SetByteTransfer(bool) = 0;
// Get requested LUN based on IDENTIFY message, with LUN from the CDB as fallback
virtual int GetEffectiveLun() const = 0;
virtual void ScheduleShutdown(rascsi_shutdown_mode) = 0;
int GetTargetId() const { return target_id; }
int GetMaxLuns() const { return max_luns; }
int GetLunCount() const { return (int)luns.size(); }
unordered_set<shared_ptr<PrimaryDevice>> GetDevices() const;
shared_ptr<PrimaryDevice> GetDeviceForLun(int) const;
bool AddDevice(shared_ptr<PrimaryDevice>);
bool DeleteDevice(const shared_ptr<PrimaryDevice>);
bool HasDeviceForLun(int) const;
int ExtractInitiatorId(int) const;
void AllocateBuffer(size_t);
vector<BYTE>& GetBuffer() { return ctrl.buffer; }
scsi_defs::status GetStatus() const { return ctrl.status; }
void SetStatus(scsi_defs::status s) { ctrl.status = s; }
uint32_t GetLength() const { return ctrl.length; }
protected:
scsi_defs::scsi_command GetOpcode() const { return (scsi_defs::scsi_command)ctrl.cmd[0]; }
int GetLun() const { return (ctrl.cmd[1] >> 5) & 0x07; }
void ProcessPhase();
vector<int>& InitCmd(int size) { ctrl.cmd.resize(size); return ctrl.cmd; }
bool HasValidLength() const { return ctrl.length != 0; }
int GetOffset() const { return ctrl.offset; }
void ResetOffset() { ctrl.offset = 0; }
void UpdateOffsetAndLength() { ctrl.offset += ctrl.length; ctrl.length = 0; }
private:
int target_id;
BUS& bus;
int max_luns;
ctrl_t ctrl = {};
ctrl_t* GetCtrl() { return &ctrl; }
};

View File

@ -0,0 +1,87 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "devices/device_factory.h"
#include "devices/primary_device.h"
#include "scsi_controller.h"
#include "controller_manager.h"
using namespace std;
bool ControllerManager::AttachToScsiController(int id, shared_ptr<PrimaryDevice> device)
{
auto controller = FindController(id);
if (controller == nullptr) {
controller = make_shared<ScsiController>(bus, id);
if (controller->AddDevice(device)) {
controllers[id] = controller;
return true;
}
return false;
}
return controller->AddDevice(device);
}
bool ControllerManager::DeleteController(shared_ptr<AbstractController> controller)
{
return controllers.erase(controller->GetTargetId()) == 1;
}
shared_ptr<AbstractController> ControllerManager::IdentifyController(int data) const
{
for (const auto& [id, controller] : controllers) {
if (data & (1 << controller->GetTargetId())) {
return controller;
}
}
return nullptr;
}
shared_ptr<AbstractController> ControllerManager::FindController(int target_id) const
{
const auto& it = controllers.find(target_id);
return it == controllers.end() ? nullptr : it->second;
}
unordered_set<shared_ptr<PrimaryDevice>> ControllerManager::GetAllDevices() const
{
unordered_set<shared_ptr<PrimaryDevice>> devices;
for (const auto& [id, controller] : controllers) {
const auto& d = controller->GetDevices();
devices.insert(d.begin(), d.end());
}
return devices;
}
void ControllerManager::DeleteAllControllers()
{
controllers.clear();
}
void ControllerManager::ResetAllControllers() const
{
for (const auto& [id, controller] : controllers) {
controller->Reset();
}
}
shared_ptr<PrimaryDevice> ControllerManager::GetDeviceByIdAndLun(int id, int lun) const
{
if (const auto controller = FindController(id); controller != nullptr) {
return controller->GetDeviceForLun(lun);
}
return nullptr;
}

View File

@ -0,0 +1,46 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2022 Uwe Seimet
//
// Keeps track of and manages the controllers
//
//---------------------------------------------------------------------------
#pragma once
#include <unordered_map>
#include <unordered_set>
#include <memory>
using namespace std;
class BUS;
class AbstractController;
class PrimaryDevice;
class ControllerManager
{
BUS& bus;
unordered_map<int, shared_ptr<AbstractController>> controllers;
public:
explicit ControllerManager(BUS& bus) : bus(bus) {}
~ControllerManager() = default;
// Maximum number of controller devices
static const int DEVICE_MAX = 8;
bool AttachToScsiController(int, shared_ptr<PrimaryDevice>);
bool DeleteController(shared_ptr<AbstractController>);
shared_ptr<AbstractController> IdentifyController(int) const;
shared_ptr<AbstractController> FindController(int) const;
unordered_set<shared_ptr<PrimaryDevice>> GetAllDevices() const;
void DeleteAllControllers();
void ResetAllControllers() const;
shared_ptr<PrimaryDevice> GetDeviceByIdAndLun(int, int) const;
};

View File

@ -0,0 +1,46 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#pragma once
#include "scsi.h"
class PhaseHandler
{
BUS::phase_t phase = BUS::phase_t::busfree;
public:
PhaseHandler() = default;
virtual ~PhaseHandler() = default;
virtual void BusFree() = 0;
virtual void Selection() = 0;
virtual void Command() = 0;
virtual void Status() = 0;
virtual void DataIn() = 0;
virtual void DataOut() = 0;
virtual void MsgIn() = 0;
virtual void MsgOut() = 0;
virtual BUS::phase_t Process(int) = 0;
protected:
BUS::phase_t GetPhase() const { return phase; }
void SetPhase(BUS::phase_t p) { phase = p; }
bool IsSelection() const { return phase == BUS::phase_t::selection; }
bool IsBusFree() const { return phase == BUS::phase_t::busfree; }
bool IsCommand() const { return phase == BUS::phase_t::command; }
bool IsStatus() const { return phase == BUS::phase_t::status; }
bool IsDataIn() const { return phase == BUS::phase_t::datain; }
bool IsDataOut() const { return phase == BUS::phase_t::dataout; }
bool IsMsgIn() const { return phase == BUS::phase_t::msgin; }
bool IsMsgOut() const { return phase == BUS::phase_t::msgout; }
};

File diff suppressed because it is too large Load Diff

View File

@ -1,171 +0,0 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
// Copyright (C) akuker
//
// Licensed under the BSD 3-Clause License.
// See LICENSE file in the project root folder.
//
// [ SASI device controller ]
//
//---------------------------------------------------------------------------
#pragma once
#include "../config.h"
#include "os.h"
#include "scsi.h"
#include "fileio.h"
class PrimaryDevice;
//===========================================================================
//
// SASI Controller
//
//===========================================================================
class SASIDEV
{
protected:
private:
enum sasi_command : int {
eCmdTestUnitReady = 0x00,
eCmdRezero = 0x01,
eCmdRequestSense = 0x03,
eCmdFormat = 0x04,
eCmdReadCapacity = 0x05,
eCmdFormatLegacy = 0x06,
eCmdReassign = 0x07,
eCmdRead6 = 0x08,
eCmdWrite6 = 0x0A,
eCmdSeek6 = 0x0B,
eCmdSetMcastAddr = 0x0D, // DaynaPort specific command
eCmdInquiry = 0x12,
eCmdModeSelect6 = 0x15,
eCmdReserve6 = 0x16,
eCmdRelease6 = 0x17,
eCmdRead10 = 0x28,
eCmdWrite10 = 0x2A,
eCmdVerify10 = 0x2E,
eCmdVerify = 0x2F,
eCmdModeSelect10 = 0x55,
eCmdRead16 = 0x88,
eCmdWrite16 = 0x8A,
eCmdVerify16 = 0x8F,
eCmdWriteLong10 = 0x3F,
eCmdWriteLong16 = 0x9F,
eCmdInvalid = 0xC2,
eCmdSasiCmdAssign = 0x0E
};
public:
enum {
UnitMax = 32 // Maximum number of logical units
};
const int UNKNOWN_SCSI_ID = -1;
const int DEFAULT_BUFFER_SIZE = 0x1000;
// TODO Remove this duplicate
const int DAYNAPORT_BUFFER_SIZE = 0x1000000;
// For timing adjustments
enum {
min_exec_time_sasi = 100, // SASI BOOT/FORMAT 30:NG 35:OK
min_exec_time_scsi = 50
};
// Internal data definition
typedef struct {
// General
BUS::phase_t phase; // Transition phase
int m_scsi_id; // Controller ID (0-7)
BUS *bus; // Bus
// commands
DWORD cmd[16]; // Command data
DWORD status; // Status data
DWORD message; // Message data
// Run
DWORD execstart; // Execution start time
// Transfer
BYTE *buffer; // Transfer data buffer
int bufsize; // Transfer data buffer size
uint32_t blocks; // Number of transfer block
DWORD next; // Next record
DWORD offset; // Transfer offset
DWORD length; // Transfer remaining length
// Logical units
PrimaryDevice *unit[UnitMax];
// The current device
PrimaryDevice *device;
// The LUN from the IDENTIFY message
int lun;
} ctrl_t;
public:
// Basic Functions
SASIDEV();
virtual ~SASIDEV(); // Destructor
virtual void Reset(); // Device Reset
// External API
virtual BUS::phase_t Process(int); // Run
// Connect
void Connect(int id, BUS *sbus); // Controller connection
PrimaryDevice* GetUnit(int no); // Get logical unit
void SetUnit(int no, PrimaryDevice *dev); // Logical unit setting
bool HasUnit(); // Has a valid logical unit
// Other
BUS::phase_t GetPhase() {return ctrl.phase;} // Get the phase
int GetSCSIID() {return ctrl.m_scsi_id;} // Get the ID
ctrl_t* GetCtrl() { return &ctrl; } // Get the internal information address
virtual bool IsSASI() const { return true; } // SASI Check
virtual bool IsSCSI() const { return false; } // SCSI check
public:
void DataIn(); // Data in phase
void Status(); // Status phase
void MsgIn(); // Message in phase
void DataOut(); // Data out phase
virtual int GetEffectiveLun() const;
virtual void Error(scsi_defs::sense_key sense_key = scsi_defs::sense_key::NO_SENSE,
scsi_defs::asc = scsi_defs::asc::NO_ADDITIONAL_SENSE_INFORMATION,
scsi_defs::status = scsi_defs::status::CHECK_CONDITION); // Common error handling
protected:
// Phase processing
virtual void BusFree(); // Bus free phase
virtual void Selection(); // Selection phase
virtual void Command(); // Command phase
virtual void Execute(); // Execution phase
// Commands
void CmdAssign(); // ASSIGN command
void CmdSpecify(); // SPECIFY command
// Data transfer
virtual void Send(); // Send data
virtual void Receive(); // Receive data
bool XferIn(BYTE* buf); // Data transfer IN
virtual bool XferOut(bool cont); // Data transfer OUT
// Special operations
void FlushUnit(); // Flush the logical unit
ctrl_t ctrl; // Internal data
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,121 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
// Copyright (C) akuker
//
// Licensed under the BSD 3-Clause License.
// See LICENSE file in the project root folder.
//
//---------------------------------------------------------------------------
#pragma once
#include "abstract_controller.h"
#include "os.h"
#include "scsi.h"
#include <array>
class PrimaryDevice;
class ScsiController : public AbstractController
{
// For timing adjustments
static const unsigned int MIN_EXEC_TIME = 50;
// Transfer period factor (limited to 50 x 4 = 200ns)
static const int MAX_SYNC_PERIOD = 50;
// REQ/ACK offset(limited to 16)
static const BYTE MAX_SYNC_OFFSET = 16;
static const int UNKNOWN_INITIATOR_ID = -1;
const int DEFAULT_BUFFER_SIZE = 0x1000;
using scsi_t = struct _scsi_t {
// Synchronous transfer
bool syncenable; // Synchronous transfer possible
BYTE syncperiod = MAX_SYNC_PERIOD; // Synchronous transfer period
BYTE syncoffset; // Synchronous transfer offset
int syncack; // Number of synchronous transfer ACKs
// ATN message
bool atnmsg;
int msc;
std::array<BYTE, 256> msb;
};
public:
// Maximum number of logical units
static const int LUN_MAX = 32;
ScsiController(BUS&, int);
~ScsiController() override = default;
void Reset() override;
BUS::phase_t Process(int) override;
int GetEffectiveLun() const override;
void Error(scsi_defs::sense_key sense_key, scsi_defs::asc asc = scsi_defs::asc::NO_ADDITIONAL_SENSE_INFORMATION,
scsi_defs::status status = scsi_defs::status::CHECK_CONDITION) override;
int GetInitiatorId() const override { return initiator_id; }
void SetByteTransfer(bool b) override { is_byte_transfer = b; }
void Status() override;
void DataIn() override;
void DataOut() override;
private:
// Execution start time
uint32_t execstart = 0;
// The initiator ID may be unavailable, e.g. with Atari ACSI and old host adapters
int initiator_id = UNKNOWN_INITIATOR_ID;
// The LUN from the IDENTIFY message
int identified_lun = -1;
bool is_byte_transfer = false;
uint32_t bytes_to_transfer = 0;
// Phases
void BusFree() override;
void Selection() override;
void Command() override;
void MsgIn() override;
void MsgOut() override;
// Data transfer
void Send();
bool XferMsg(int);
bool XferIn(vector<BYTE>&);
bool XferOut(bool);
bool XferOutBlockOriented(bool);
void ReceiveBytes();
void Execute();
void DataOutNonBlockOriented();
void Receive();
void ProcessCommand();
void ParseMessage();
void ProcessMessage();
void ScheduleShutdown(rascsi_shutdown_mode mode) override { shutdown_mode = mode; }
void Sleep();
scsi_t scsi = {};
AbstractController::rascsi_shutdown_mode shutdown_mode = AbstractController::rascsi_shutdown_mode::NONE;
};

View File

@ -1,918 +0,0 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
// Copyright (C) akuker
//
// Licensed under the BSD 3-Clause License.
// See LICENSE file in the project root folder.
//
// [ SCSI device controller ]
//
//---------------------------------------------------------------------------
#include "log.h"
#include "controllers/scsidev_ctrl.h"
#include "gpiobus.h"
#include "devices/scsi_daynaport.h"
#include "devices/scsi_printer.h"
using namespace scsi_defs;
//===========================================================================
//
// SCSI Device
//
//===========================================================================
SCSIDEV::SCSIDEV() : SASIDEV()
{
scsi.is_byte_transfer = false;
scsi.bytes_to_transfer = 0;
shutdown_mode = NONE;
// Synchronous transfer work initialization
scsi.syncenable = FALSE;
scsi.syncperiod = 50;
scsi.syncoffset = 0;
scsi.atnmsg = false;
scsi.msc = 0;
memset(scsi.msb, 0x00, sizeof(scsi.msb));
}
SCSIDEV::~SCSIDEV()
{
}
void SCSIDEV::Reset()
{
scsi.is_byte_transfer = false;
scsi.bytes_to_transfer = 0;
// Work initialization
scsi.atnmsg = false;
scsi.msc = 0;
memset(scsi.msb, 0x00, sizeof(scsi.msb));
super::Reset();
}
BUS::phase_t SCSIDEV::Process(int initiator_id)
{
// Do nothing if not connected
if (ctrl.m_scsi_id < 0 || ctrl.bus == NULL) {
return ctrl.phase;
}
// Get bus information
ctrl.bus->Aquire();
// Check to see if the reset signal was asserted
if (ctrl.bus->GetRST()) {
LOGWARN("RESET signal received!");
// Reset the controller
Reset();
// Reset the bus
ctrl.bus->Reset();
return ctrl.phase;
}
scsi.initiator_id = initiator_id;
// Phase processing
switch (ctrl.phase) {
// Bus free phase
case BUS::busfree:
BusFree();
break;
// Selection
case BUS::selection:
Selection();
break;
// Data out (MCI=000)
case BUS::dataout:
DataOut();
break;
// Data in (MCI=001)
case BUS::datain:
DataIn();
break;
// Command (MCI=010)
case BUS::command:
Command();
break;
// Status (MCI=011)
case BUS::status:
Status();
break;
// Message out (MCI=110)
case BUS::msgout:
MsgOut();
break;
// Message in (MCI=111)
case BUS::msgin:
MsgIn();
break;
default:
assert(false);
break;
}
return ctrl.phase;
}
//---------------------------------------------------------------------------
//
// Bus free phase
//
//---------------------------------------------------------------------------
void SCSIDEV::BusFree()
{
// Phase change
if (ctrl.phase != BUS::busfree) {
LOGTRACE("%s Bus free phase", __PRETTY_FUNCTION__);
// Phase setting
ctrl.phase = BUS::busfree;
// Set Signal lines
ctrl.bus->SetREQ(FALSE);
ctrl.bus->SetMSG(FALSE);
ctrl.bus->SetCD(FALSE);
ctrl.bus->SetIO(FALSE);
ctrl.bus->SetBSY(false);
// Initialize status and message
ctrl.status = 0x00;
ctrl.message = 0x00;
// Initialize ATN message reception status
scsi.atnmsg = false;
ctrl.lun = -1;
scsi.is_byte_transfer = false;
scsi.bytes_to_transfer = 0;
// When the bus is free RaSCSI or the Pi may be shut down
switch(shutdown_mode) {
case STOP_RASCSI:
LOGINFO("RaSCSI shutdown requested");
exit(0);
break;
case STOP_PI:
LOGINFO("Raspberry Pi shutdown requested");
if (system("init 0") == -1) {
LOGERROR("Raspberry Pi shutdown failed: %s", strerror(errno));
}
break;
case RESTART_PI:
LOGINFO("Raspberry Pi restart requested");
if (system("init 6") == -1) {
LOGERROR("Raspberry Pi restart failed: %s", strerror(errno));
}
break;
default:
break;
}
return;
}
// Move to selection phase
if (ctrl.bus->GetSEL() && !ctrl.bus->GetBSY()) {
Selection();
}
}
//---------------------------------------------------------------------------
//
// Selection Phase
//
//---------------------------------------------------------------------------
void SCSIDEV::Selection()
{
// Phase change
if (ctrl.phase != BUS::selection) {
// invalid if IDs do not match
int id = 1 << ctrl.m_scsi_id;
if ((ctrl.bus->GetDAT() & id) == 0) {
return;
}
// Return if there is no valid LUN
if (!HasUnit()) {
return;
}
LOGTRACE("%s Selection Phase ID=%d (with device)", __PRETTY_FUNCTION__, (int)ctrl.m_scsi_id);
if (scsi.initiator_id != UNKNOWN_SCSI_ID) {
LOGTRACE("%s Initiator ID is %d", __PRETTY_FUNCTION__, scsi.initiator_id);
}
else {
LOGTRACE("%s Initiator ID is unknown", __PRETTY_FUNCTION__);
}
// Phase setting
ctrl.phase = BUS::selection;
// Raise BSY and respond
ctrl.bus->SetBSY(true);
return;
}
// Selection completed
if (!ctrl.bus->GetSEL() && ctrl.bus->GetBSY()) {
// Message out phase if ATN=1, otherwise command phase
if (ctrl.bus->GetATN()) {
MsgOut();
} else {
Command();
}
}
}
//---------------------------------------------------------------------------
//
// Execution Phase
//
//---------------------------------------------------------------------------
void SCSIDEV::Execute()
{
LOGTRACE("%s Execution phase command $%02X", __PRETTY_FUNCTION__, (unsigned int)ctrl.cmd[0]);
// Phase Setting
ctrl.phase = BUS::execute;
// Initialization for data transfer
ctrl.offset = 0;
ctrl.blocks = 1;
ctrl.execstart = SysTimer::GetTimerLow();
// Discard pending sense data from the previous command if the current command is not REQUEST SENSE
if ((scsi_command)ctrl.cmd[0] != scsi_command::eCmdRequestSense) {
ctrl.status = 0;
}
LOGDEBUG("++++ CMD ++++ %s Executing command $%02X", __PRETTY_FUNCTION__, (unsigned int)ctrl.cmd[0]);
int lun = GetEffectiveLun();
if (!ctrl.unit[lun]) {
if ((scsi_command)ctrl.cmd[0] != scsi_command::eCmdInquiry &&
(scsi_command)ctrl.cmd[0] != scsi_command::eCmdRequestSense) {
LOGDEBUG("Invalid LUN %d for ID %d", lun, GetSCSIID());
Error(sense_key::ILLEGAL_REQUEST, asc::INVALID_LUN);
return;
}
// Use LUN 0 for INQUIRY and REQUEST SENSE because LUN0 is assumed to be always available.
// INQUIRY and REQUEST SENSE have a special LUN handling of their own, required by the SCSI standard.
else {
assert(ctrl.unit[0]);
lun = 0;
}
}
ctrl.device = ctrl.unit[lun];
// Discard pending sense data from the previous command if the current command is not REQUEST SENSE
if ((scsi_command)ctrl.cmd[0] != scsi_command::eCmdRequestSense) {
ctrl.device->SetStatusCode(0);
}
if (!ctrl.device->Dispatch(this)) {
LOGTRACE("ID %d LUN %d received unsupported command: $%02X", GetSCSIID(), lun, (BYTE)ctrl.cmd[0]);
Error(sense_key::ILLEGAL_REQUEST, asc::INVALID_COMMAND_OPERATION_CODE);
}
// SCSI-2 p.104 4.4.3 Incorrect logical unit handling
if ((scsi_command)ctrl.cmd[0] == scsi_command::eCmdInquiry && !ctrl.unit[lun]) {
lun = GetEffectiveLun();
LOGTRACE("Reporting LUN %d for device ID %d as not supported", lun, ctrl.device->GetId());
ctrl.buffer[0] = 0x7f;
}
}
//---------------------------------------------------------------------------
//
// Message out phase
//
//---------------------------------------------------------------------------
void SCSIDEV::MsgOut()
{
LOGTRACE("%s ID %d",__PRETTY_FUNCTION__, GetSCSIID());
// Phase change
if (ctrl.phase != BUS::msgout) {
LOGTRACE("Message Out Phase");
// process the IDENTIFY message
if (ctrl.phase == BUS::selection) {
scsi.atnmsg = true;
scsi.msc = 0;
memset(scsi.msb, 0x00, sizeof(scsi.msb));
}
// Phase Setting
ctrl.phase = BUS::msgout;
// Signal line operated by the target
ctrl.bus->SetMSG(TRUE);
ctrl.bus->SetCD(TRUE);
ctrl.bus->SetIO(FALSE);
// Data transfer is 1 byte x 1 block
ctrl.offset = 0;
ctrl.length = 1;
ctrl.blocks = 1;
return;
}
Receive();
}
//---------------------------------------------------------------------------
//
// Common Error Handling
//
//---------------------------------------------------------------------------
void SCSIDEV::Error(sense_key sense_key, asc asc, status status)
{
// Get bus information
ctrl.bus->Aquire();
// Reset check
if (ctrl.bus->GetRST()) {
// Reset the controller
Reset();
// Reset the bus
ctrl.bus->Reset();
return;
}
// Bus free for status phase and message in phase
if (ctrl.phase == BUS::status || ctrl.phase == BUS::msgin) {
BusFree();
return;
}
int lun = GetEffectiveLun();
if (!ctrl.unit[lun] || asc == INVALID_LUN) {
lun = 0;
assert(ctrl.unit[lun]);
}
if (sense_key || asc) {
// Set Sense Key and ASC for a subsequent REQUEST SENSE
ctrl.unit[lun]->SetStatusCode((sense_key << 16) | (asc << 8));
}
ctrl.status = status;
ctrl.message = 0x00;
LOGTRACE("%s Error (to status phase)", __PRETTY_FUNCTION__);
Status();
}
//---------------------------------------------------------------------------
//
// Send data
//
//---------------------------------------------------------------------------
void SCSIDEV::Send()
{
ASSERT(!ctrl.bus->GetREQ());
ASSERT(ctrl.bus->GetIO());
if (ctrl.length != 0) {
LOGTRACE("%s%s", __PRETTY_FUNCTION__, (" Sending handhake with offset " + to_string(ctrl.offset) + ", length "
+ to_string(ctrl.length)).c_str());
// TODO The delay has to be taken from ctrl.unit[lun], but as there are no Daynaport drivers for
// LUNs other than 0 this work-around works.
int len = ctrl.bus->SendHandShake(&ctrl.buffer[ctrl.offset], ctrl.length, ctrl.unit[0] ? ctrl.unit[0]->GetSendDelay() : 0);
// If you cannot send all, move to status phase
if (len != (int)ctrl.length) {
Error();
return;
}
// offset and length
ctrl.offset += ctrl.length;
ctrl.length = 0;
return;
}
// Block subtraction, result initialization
ctrl.blocks--;
bool result = true;
// Processing after data collection (read/data-in only)
if (ctrl.phase == BUS::datain) {
if (ctrl.blocks != 0) {
// set next buffer (set offset, length)
result = XferIn(ctrl.buffer);
LOGTRACE("%s%s", __PRETTY_FUNCTION__, (" Processing after data collection. Blocks: " + to_string(ctrl.blocks)).c_str());
}
}
// If result FALSE, move to status phase
if (!result) {
Error();
return;
}
// Continue sending if block !=0
if (ctrl.blocks != 0){
LOGTRACE("%s%s", __PRETTY_FUNCTION__, (" Continuing to send. Blocks: " + to_string(ctrl.blocks)).c_str());
ASSERT(ctrl.length > 0);
ASSERT(ctrl.offset == 0);
return;
}
// Move to next phase
LOGTRACE("%s Move to next phase %s (%d)", __PRETTY_FUNCTION__, BUS::GetPhaseStrRaw(ctrl.phase), ctrl.phase);
switch (ctrl.phase) {
// Message in phase
case BUS::msgin:
// Completed sending response to extended message of IDENTIFY message
if (scsi.atnmsg) {
// flag off
scsi.atnmsg = false;
// command phase
Command();
} else {
// Bus free phase
BusFree();
}
break;
// Data-in Phase
case BUS::datain:
// status phase
Status();
break;
// status phase
case BUS::status:
// Message in phase
ctrl.length = 1;
ctrl.blocks = 1;
ctrl.buffer[0] = (BYTE)ctrl.message;
MsgIn();
break;
default:
assert(false);
break;
}
}
//---------------------------------------------------------------------------
//
// Receive Data
//
//---------------------------------------------------------------------------
void SCSIDEV::Receive()
{
if (scsi.is_byte_transfer) {
ReceiveBytes();
return;
}
int len;
BYTE data;
LOGTRACE("%s",__PRETTY_FUNCTION__);
// REQ is low
ASSERT(!ctrl.bus->GetREQ());
ASSERT(!ctrl.bus->GetIO());
// Length != 0 if received
if (ctrl.length != 0) {
LOGTRACE("%s Length is %d bytes", __PRETTY_FUNCTION__, (int)ctrl.length);
// Receive
len = ctrl.bus->ReceiveHandShake(&ctrl.buffer[ctrl.offset], ctrl.length);
// If not able to receive all, move to status phase
if (len != (int)ctrl.length) {
LOGERROR("%s Not able to receive %d bytes of data, only received %d. Going to error",__PRETTY_FUNCTION__, (int)ctrl.length, len);
Error();
return;
}
// Offset and Length
ctrl.offset += ctrl.length;
ctrl.length = 0;
return;
}
// Block subtraction, result initialization
ctrl.blocks--;
bool result = true;
// Processing after receiving data (by phase)
LOGTRACE("%s ctrl.phase: %d (%s)",__PRETTY_FUNCTION__, (int)ctrl.phase, BUS::GetPhaseStrRaw(ctrl.phase));
switch (ctrl.phase) {
// Data out phase
case BUS::dataout:
if (ctrl.blocks == 0) {
// End with this buffer
result = XferOut(false);
} else {
// Continue to next buffer (set offset, length)
result = XferOut(true);
}
break;
// Message out phase
case BUS::msgout:
ctrl.message = ctrl.buffer[0];
if (!XferMsg(ctrl.message)) {
// Immediately free the bus if message output fails
BusFree();
return;
}
// Clear message data in preparation for message-in
ctrl.message = 0x00;
break;
default:
break;
}
// If result FALSE, move to status phase
if (!result) {
Error();
return;
}
// Continue to receive if block !=0
if (ctrl.blocks != 0){
ASSERT(ctrl.length > 0);
ASSERT(ctrl.offset == 0);
return;
}
// Move to next phase
switch (ctrl.phase) {
// Command phase
case BUS::command:
len = GPIOBUS::GetCommandByteCount(ctrl.buffer[0]);
for (int i = 0; i < len; i++) {
ctrl.cmd[i] = ctrl.buffer[i];
LOGTRACE("%s Command Byte %d: $%02X",__PRETTY_FUNCTION__, i, ctrl.cmd[i]);
}
// Execution Phase
Execute();
break;
// Message out phase
case BUS::msgout:
// Continue message out phase as long as ATN keeps asserting
if (ctrl.bus->GetATN()) {
// Data transfer is 1 byte x 1 block
ctrl.offset = 0;
ctrl.length = 1;
ctrl.blocks = 1;
return;
}
// Parsing messages sent by ATN
if (scsi.atnmsg) {
int i = 0;
while (i < scsi.msc) {
// Message type
data = scsi.msb[i];
// ABORT
if (data == 0x06) {
LOGTRACE("Message code ABORT $%02X", data);
BusFree();
return;
}
// BUS DEVICE RESET
if (data == 0x0C) {
LOGTRACE("Message code BUS DEVICE RESET $%02X", data);
scsi.syncoffset = 0;
BusFree();
return;
}
// IDENTIFY
if (data >= 0x80) {
ctrl.lun = data & 0x1F;
LOGTRACE("Message code IDENTIFY $%02X, LUN %d selected", data, ctrl.lun);
}
// Extended Message
if (data == 0x01) {
LOGTRACE("Message code EXTENDED MESSAGE $%02X", data);
// Check only when synchronous transfer is possible
if (!scsi.syncenable || scsi.msb[i + 2] != 0x01) {
ctrl.length = 1;
ctrl.blocks = 1;
ctrl.buffer[0] = 0x07;
MsgIn();
return;
}
// Transfer period factor (limited to 50 x 4 = 200ns)
scsi.syncperiod = scsi.msb[i + 3];
if (scsi.syncperiod > 50) {
scsi.syncperiod = 50;
}
// REQ/ACK offset(limited to 16)
scsi.syncoffset = scsi.msb[i + 4];
if (scsi.syncoffset > 16) {
scsi.syncoffset = 16;
}
// STDR response message generation
ctrl.length = 5;
ctrl.blocks = 1;
ctrl.buffer[0] = 0x01;
ctrl.buffer[1] = 0x03;
ctrl.buffer[2] = 0x01;
ctrl.buffer[3] = (BYTE)scsi.syncperiod;
ctrl.buffer[4] = (BYTE)scsi.syncoffset;
MsgIn();
return;
}
// next
i++;
}
}
// Initialize ATN message reception status
scsi.atnmsg = false;
// Command phase
Command();
break;
// Data out phase
case BUS::dataout:
FlushUnit();
// status phase
Status();
break;
default:
assert(false);
break;
}
}
//---------------------------------------------------------------------------
//
// Transfer MSG
//
//---------------------------------------------------------------------------
bool SCSIDEV::XferMsg(int msg)
{
ASSERT(ctrl.phase == BUS::msgout);
// Save message out data
if (scsi.atnmsg) {
scsi.msb[scsi.msc] = (BYTE)msg;
scsi.msc++;
scsi.msc %= 256;
}
return true;
}
void SCSIDEV::ReceiveBytes()
{
uint32_t len;
BYTE data;
LOGTRACE("%s",__PRETTY_FUNCTION__);
// REQ is low
ASSERT(!ctrl.bus->GetREQ());
ASSERT(!ctrl.bus->GetIO());
if (ctrl.length) {
LOGTRACE("%s Length is %d bytes", __PRETTY_FUNCTION__, ctrl.length);
len = ctrl.bus->ReceiveHandShake(&ctrl.buffer[ctrl.offset], ctrl.length);
// If not able to receive all, move to status phase
if (len != ctrl.length) {
LOGERROR("%s Not able to receive %d bytes of data, only received %d. Going to error",
__PRETTY_FUNCTION__, ctrl.length, len);
Error();
return;
}
ctrl.offset += ctrl.length;
scsi.bytes_to_transfer = ctrl.length;
ctrl.length = 0;
return;
}
// Result initialization
bool result = true;
// Processing after receiving data (by phase)
LOGTRACE("%s ctrl.phase: %d (%s)",__PRETTY_FUNCTION__, (int)ctrl.phase, BUS::GetPhaseStrRaw(ctrl.phase));
switch (ctrl.phase) {
case BUS::dataout:
result = XferOut(false);
break;
case BUS::msgout:
ctrl.message = ctrl.buffer[0];
if (!XferMsg(ctrl.message)) {
// Immediately free the bus if message output fails
BusFree();
return;
}
// Clear message data in preparation for message-in
ctrl.message = 0x00;
break;
default:
break;
}
// If result FALSE, move to status phase
if (!result) {
Error();
return;
}
// Move to next phase
switch (ctrl.phase) {
case BUS::command:
len = GPIOBUS::GetCommandByteCount(ctrl.buffer[0]);
for (uint32_t i = 0; i < len; i++) {
ctrl.cmd[i] = ctrl.buffer[i];
LOGTRACE("%s Command Byte %d: $%02X",__PRETTY_FUNCTION__, i, ctrl.cmd[i]);
}
Execute();
break;
case BUS::msgout:
// Continue message out phase as long as ATN keeps asserting
if (ctrl.bus->GetATN()) {
// Data transfer is 1 byte x 1 block
ctrl.offset = 0;
ctrl.length = 1;
ctrl.blocks = 1;
return;
}
// Parsing messages sent by ATN
if (scsi.atnmsg) {
int i = 0;
while (i < scsi.msc) {
// Message type
data = scsi.msb[i];
// ABORT
if (data == 0x06) {
LOGTRACE("Message code ABORT $%02X", data);
BusFree();
return;
}
// BUS DEVICE RESET
if (data == 0x0C) {
LOGTRACE("Message code BUS DEVICE RESET $%02X", data);
scsi.syncoffset = 0;
BusFree();
return;
}
// IDENTIFY
if (data >= 0x80) {
ctrl.lun = data & 0x1F;
LOGTRACE("Message code IDENTIFY $%02X, LUN %d selected", data, ctrl.lun);
}
// Extended Message
if (data == 0x01) {
LOGTRACE("Message code EXTENDED MESSAGE $%02X", data);
// Check only when synchronous transfer is possible
if (!scsi.syncenable || scsi.msb[i + 2] != 0x01) {
ctrl.length = 1;
ctrl.blocks = 1;
ctrl.buffer[0] = 0x07;
MsgIn();
return;
}
// Transfer period factor (limited to 50 x 4 = 200ns)
scsi.syncperiod = scsi.msb[i + 3];
if (scsi.syncperiod > 50) {
scsi.syncoffset = 50;
}
// REQ/ACK offset(limited to 16)
scsi.syncoffset = scsi.msb[i + 4];
if (scsi.syncoffset > 16) {
scsi.syncoffset = 16;
}
// STDR response message generation
ctrl.length = 5;
ctrl.blocks = 1;
ctrl.buffer[0] = 0x01;
ctrl.buffer[1] = 0x03;
ctrl.buffer[2] = 0x01;
ctrl.buffer[3] = (BYTE)scsi.syncperiod;
ctrl.buffer[4] = (BYTE)scsi.syncoffset;
MsgIn();
return;
}
// next
i++;
}
}
// Initialize ATN message reception status
scsi.atnmsg = false;
Command();
break;
case BUS::dataout:
Status();
break;
default:
assert(false);
break;
}
}
bool SCSIDEV::XferOut(bool cont)
{
if (!scsi.is_byte_transfer) {
return super::XferOut(cont);
}
ASSERT(ctrl.phase == BUS::dataout);
scsi.is_byte_transfer = false;
PrimaryDevice *device = dynamic_cast<PrimaryDevice *>(ctrl.unit[GetEffectiveLun()]);
if (device && ctrl.cmd[0] == scsi_command::eCmdWrite6) {
return device->WriteBytes(ctrl.buffer, scsi.bytes_to_transfer);
}
LOGWARN("Received an unexpected command ($%02X) in %s", (WORD)ctrl.cmd[0] , __PRETTY_FUNCTION__)
return false;
}
int SCSIDEV::GetEffectiveLun() const
{
return ctrl.lun != -1 ? ctrl.lun : (ctrl.cmd[1] >> 5) & 0x07;
}

View File

@ -1,102 +0,0 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
// Copyright (C) akuker
//
// Licensed under the BSD 3-Clause License.
// See LICENSE file in the project root folder.
//
// [ SCSI device controller ]
//
//---------------------------------------------------------------------------
#pragma once
#include "controllers/sasidev_ctrl.h"
//===========================================================================
//
// SCSI Device (Interits SASI device)
//
//===========================================================================
class SCSIDEV : public SASIDEV
{
public:
enum rascsi_shutdown_mode {
NONE,
STOP_RASCSI,
STOP_PI,
RESTART_PI
};
// Internal data definition
typedef struct {
// Synchronous transfer
BOOL syncenable; // Synchronous transfer possible
int syncperiod; // Synchronous transfer period
int syncoffset; // Synchronous transfer offset
int syncack; // Number of synchronous transfer ACKs
// ATN message
bool atnmsg;
int msc;
BYTE msb[256];
// -1 means that the initiator ID is unknown, e.g. with Atari ACSI and old host adapters
int initiator_id;
bool is_byte_transfer;
uint32_t bytes_to_transfer;
} scsi_t;
SCSIDEV();
~SCSIDEV();
void Reset() override;
BUS::phase_t Process(int) override;
void Receive() override;
// Get LUN based on IDENTIFY message, with LUN from the CDB as fallback
int GetEffectiveLun() const;
bool IsSASI() const override { return false; }
bool IsSCSI() const override { return true; }
// Common error handling
void Error(scsi_defs::sense_key sense_key = scsi_defs::sense_key::NO_SENSE,
scsi_defs::asc asc = scsi_defs::asc::NO_ADDITIONAL_SENSE_INFORMATION,
scsi_defs::status status = scsi_defs::status::CHECK_CONDITION) override;
void ScheduleShutDown(rascsi_shutdown_mode shutdown_mode) { this->shutdown_mode = shutdown_mode; }
int GetInitiatorId() const { return scsi.initiator_id; }
bool IsByteTransfer() const { return scsi.is_byte_transfer; }
void SetByteTransfer(bool is_byte_transfer) { scsi.is_byte_transfer = is_byte_transfer; }
private:
typedef SASIDEV super;
// Phases
void BusFree() override;
void Selection() override;
void Execute() override;
void MsgOut();
// Data transfer
void Send() override;
bool XferMsg(int);
bool XferOut(bool);
void ReceiveBytes();
// Internal data
scsi_t scsi;
rascsi_shutdown_mode shutdown_mode;
};

View File

@ -0,0 +1,126 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
// Copyright (C) akuker
//
// Licensed under the BSD 3-Clause License.
// See LICENSE file in the project root folder.
//
//---------------------------------------------------------------------------
#include "cd_track.h"
#include <cassert>
void CDTrack::Init(int track, uint32_t first, uint32_t last)
{
assert(!valid);
assert(track >= 1);
assert(first < last);
// Set and enable track number
track_no = track;
valid = true;
// Remember LBA
first_lba = first;
last_lba = last;
}
void CDTrack::SetPath(bool cdda, const Filepath& path)
{
assert(valid);
// CD-DA or data
audio = cdda;
// Remember the path
imgpath = path;
}
void CDTrack::GetPath(Filepath& path) const
{
assert(valid);
// Return the path (by reference)
path = imgpath;
}
//---------------------------------------------------------------------------
//
// Gets the start of LBA
//
//---------------------------------------------------------------------------
uint32_t CDTrack::GetFirst() const
{
assert(valid);
assert(first_lba < last_lba);
return first_lba;
}
//---------------------------------------------------------------------------
//
// Get the end of LBA
//
//---------------------------------------------------------------------------
uint32_t CDTrack::GetLast() const
{
assert(valid);
assert(first_lba < last_lba);
return last_lba;
}
uint32_t CDTrack::GetBlocks() const
{
assert(valid);
assert(first_lba < last_lba);
// Calculate from start LBA and end LBA
return last_lba - first_lba + 1;
}
int CDTrack::GetTrackNo() const
{
assert(valid);
assert(track_no >= 1);
return track_no;
}
//---------------------------------------------------------------------------
//
// Is valid block
//
//---------------------------------------------------------------------------
bool CDTrack::IsValid(uint32_t lba) const
{
// false if the track itself is invalid
if (!valid) {
return false;
}
// If the block is BEFORE the first block
if (lba < first_lba) {
return false;
}
// If the block is AFTER the last block
if (last_lba < lba) {
return false;
}
// This track is valid
return true;
}
bool CDTrack::IsAudio() const
{
assert(valid);
return audio;
}

View File

@ -0,0 +1,45 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
// Copyright (C) akuker
//
// Licensed under the BSD 3-Clause License.
// See LICENSE file in the project root folder.
//
//---------------------------------------------------------------------------
#pragma once
#include "filepath.h"
class CDTrack final
{
public:
CDTrack() = default;
~CDTrack() = default;
void Init(int track, uint32_t first, uint32_t last);
// Properties
void SetPath(bool cdda, const Filepath& path); // Set the path
void GetPath(Filepath& path) const; // Get the path
uint32_t GetFirst() const; // Get the start LBA
uint32_t GetLast() const; // Get the last LBA
uint32_t GetBlocks() const; // Get the number of blocks
int GetTrackNo() const; // Get the track number
bool IsValid(uint32_t lba) const; // Is this a valid LBA?
bool IsAudio() const; // Is this an audio track?
private:
bool valid = false; // Valid track
int track_no = -1; // Track number
uint32_t first_lba = 0; // First LBA
uint32_t last_lba = 0; // Last LBA
bool audio = false; // Audio track flag
Filepath imgpath; // Image file path
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,63 +7,90 @@
// Copyright (C) 2016-2020 GIMONS
// Copyright (C) akuker
//
// [ TAP Driver ]
//
//---------------------------------------------------------------------------
#include <unistd.h>
#include <poll.h>
#include <arpa/inet.h>
#ifdef __linux__
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/sockios.h>
#endif
#include "os.h"
#include "ctapdriver.h"
#include "log.h"
#include "rasutil.h"
#include "exceptions.h"
#include "rascsi_exceptions.h"
#include <net/if.h>
#include <sys/ioctl.h>
#include <sstream>
#define BRIDGE_NAME "rascsi_bridge"
#ifdef __linux__
#include <sys/epoll.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/sockios.h>
#endif
using namespace std;
using namespace ras_util;
CTapDriver::CTapDriver()
{
m_hTAP = -1;
memset(&m_MacAddr, 0, sizeof(m_MacAddr));
m_pcap = NULL;
m_pcap_dumper = NULL;
}
//---------------------------------------------------------------------------
//
// Initialization
//
//---------------------------------------------------------------------------
static bool br_setif(int br_socket_fd, const char* bridgename, const char* ifname, bool add) {
struct ifreq ifr;
#ifndef __linux__
return false;
#else
ifreq ifr;
ifr.ifr_ifindex = if_nametoindex(ifname);
if (ifr.ifr_ifindex == 0) {
LOGERROR("Can't if_nametoindex: %s", strerror(errno));
LOGERROR("Can't if_nametoindex %s: %s", ifname, strerror(errno))
return false;
}
strncpy(ifr.ifr_name, bridgename, IFNAMSIZ);
strncpy(ifr.ifr_name, bridgename, IFNAMSIZ - 1);
if (ioctl(br_socket_fd, add ? SIOCBRADDIF : SIOCBRDELIF, &ifr) < 0) {
LOGERROR("Can't ioctl %s: %s", add ? "SIOCBRADDIF" : "SIOCBRDELIF", strerror(errno));
LOGERROR("Can't ioctl %s: %s", add ? "SIOCBRADDIF" : "SIOCBRDELIF", strerror(errno))
return false;
}
return true;
#endif
}
CTapDriver::~CTapDriver()
{
if (m_hTAP != -1) {
if (int br_socket_fd; (br_socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0)) < 0) {
LOGERROR("Can't open bridge socket: %s", strerror(errno))
} else {
LOGDEBUG("brctl delif %s ras0", BRIDGE_NAME)
if (!br_setif(br_socket_fd, BRIDGE_NAME, "ras0", false)) { //NOSONAR No exception is raised here
LOGWARN("Warning: Removing ras0 from the bridge failed.")
LOGWARN("You may need to manually remove the ras0 tap device from the bridge")
}
close(br_socket_fd);
}
// Release TAP defice
close(m_hTAP);
}
if (m_pcap_dumper != nullptr) {
pcap_dump_close(m_pcap_dumper);
}
if (m_pcap != nullptr) {
pcap_close(m_pcap);
}
}
static bool ip_link(int fd, const char* ifname, bool up) {
struct ifreq ifr;
#ifndef __linux__
return false;
#else
ifreq ifr;
strncpy(ifr.ifr_name, ifname, IFNAMSIZ-1); // Need to save room for null terminator
int err = ioctl(fd, SIOCGIFFLAGS, &ifr);
if (err) {
LOGERROR("Can't ioctl SIOCGIFFLAGS: %s", strerror(errno));
LOGERROR("Can't ioctl SIOCGIFFLAGS: %s", strerror(errno))
return false;
}
ifr.ifr_flags &= ~IFF_UP;
@ -72,13 +99,14 @@ static bool ip_link(int fd, const char* ifname, bool up) {
}
err = ioctl(fd, SIOCSIFFLAGS, &ifr);
if (err) {
LOGERROR("Can't ioctl SIOCSIFFLAGS: %s", strerror(errno));
LOGERROR("Can't ioctl SIOCSIFFLAGS: %s", strerror(errno))
return false;
}
return true;
#endif
}
static bool is_interface_up(const string& interface) {
static bool is_interface_up(string_view interface) {
string file = "/sys/class/net/";
file += interface;
file += "/carrier";
@ -98,66 +126,68 @@ static bool is_interface_up(const string& interface) {
bool CTapDriver::Init(const unordered_map<string, string>& const_params)
{
#ifndef __linux__
return false;
#else
unordered_map<string, string> params = const_params;
if (params.count("interfaces")) {
LOGWARN("You are using the deprecated 'interfaces' parameter. "
"Provide the interface list and the IP address/netmask with the 'interface' and 'inet' parameters");
"Provide the interface list and the IP address/netmask with the 'interface' and 'inet' parameters")
// TODO Remove the deprecated syntax in a future version
const string& interfaces = params["interfaces"];
size_t separatorPos = interfaces.find(':');
const string& ifaces = params["interfaces"];
size_t separatorPos = ifaces.find(':');
if (separatorPos != string::npos) {
params["interface"] = interfaces.substr(0, separatorPos);
params["inet"] = interfaces.substr(separatorPos + 1);
params["interface"] = ifaces.substr(0, separatorPos);
params["inet"] = ifaces.substr(separatorPos + 1);
}
}
stringstream s(params["interface"]);
string interface;
while (getline(s, interface, ',')) {
this->interfaces.push_back(interface);
interfaces.push_back(interface);
}
this->inet = params["inet"];
inet = params["inet"];
LOGTRACE("Opening Tap device");
LOGTRACE("Opening Tap device")
// TAP device initilization
if ((m_hTAP = open("/dev/net/tun", O_RDWR)) < 0) {
LOGERROR("Can't open tun: %s", strerror(errno));
LOGERROR("Can't open tun: %s", strerror(errno))
return false;
}
LOGTRACE("Opened tap device %d",m_hTAP);
// IFF_NO_PI for no extra packet information
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
char dev[IFNAMSIZ] = "ras0";
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
LOGTRACE("Opened tap device %d", m_hTAP)
LOGTRACE("Going to open %s", ifr.ifr_name);
// IFF_NO_PI for no extra packet information
ifreq ifr = {};
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
string dev = "ras0";
strncpy(ifr.ifr_name, dev.c_str(), IFNAMSIZ - 1);
LOGTRACE("Going to open %s", ifr.ifr_name)
int ret = ioctl(m_hTAP, TUNSETIFF, (void *)&ifr);
if (ret < 0) {
LOGERROR("Can't ioctl TUNSETIFF: %s", strerror(errno));
LOGERROR("Can't ioctl TUNSETIFF: %s", strerror(errno))
close(m_hTAP);
return false;
}
LOGTRACE("Return code from ioctl was %d", ret);
LOGTRACE("Return code from ioctl was %d", ret)
int ip_fd = socket(PF_INET, SOCK_DGRAM, 0);
const int ip_fd = socket(PF_INET, SOCK_DGRAM, 0);
if (ip_fd < 0) {
LOGERROR("Can't open ip socket: %s", strerror(errno));
LOGERROR("Can't open ip socket: %s", strerror(errno))
close(m_hTAP);
return false;
}
int br_socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
const int br_socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (br_socket_fd < 0) {
LOGERROR("Can't open bridge socket: %s", strerror(errno));
LOGERROR("Can't open bridge socket: %s", strerror(errno))
close(m_hTAP);
close(ip_fd);
@ -168,35 +198,35 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
string sys_file = "/sys/class/net/";
sys_file += BRIDGE_NAME;
if (access(sys_file.c_str(), F_OK)) {
LOGINFO("%s is not yet available", BRIDGE_NAME);
LOGINFO("%s is not yet available", BRIDGE_NAME)
LOGTRACE("Checking which interface is available for creating the bridge");
LOGTRACE("Checking which interface is available for creating the bridge")
string bridge_interface;
for (const string& interface : interfaces) {
if (is_interface_up(interface)) {
LOGTRACE("%s", string("Interface " + interface + " is up").c_str());
for (const string& iface : interfaces) {
if (is_interface_up(iface)) {
LOGTRACE("%s", string("Interface " + iface + " is up").c_str())
bridge_interface = interface;
bridge_interface = iface;
break;
}
else {
LOGTRACE("%s", string("Interface " + interface + " is not available or is not up").c_str());
LOGTRACE("%s", string("Interface " + iface + " is not available or is not up").c_str())
}
}
if (bridge_interface.empty()) {
LOGERROR("No interface is up, not creating bridge");
LOGERROR("No interface is up, not creating bridge")
return false;
}
LOGINFO("Creating %s for interface %s", BRIDGE_NAME, bridge_interface.c_str());
LOGINFO("Creating %s for interface %s", BRIDGE_NAME, bridge_interface.c_str())
if (bridge_interface == "eth0") {
LOGTRACE("brctl addbr %s", BRIDGE_NAME);
LOGTRACE("brctl addbr %s", BRIDGE_NAME)
if ((ret = ioctl(br_socket_fd, SIOCBRADDBR, BRIDGE_NAME)) < 0) {
LOGERROR("Can't ioctl SIOCBRADDBR: %s", strerror(errno));
if (ioctl(br_socket_fd, SIOCBRADDBR, BRIDGE_NAME) < 0) {
LOGERROR("Can't ioctl SIOCBRADDBR: %s", strerror(errno))
close(m_hTAP);
close(ip_fd);
@ -204,7 +234,7 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
return false;
}
LOGTRACE("brctl addif %s %s", BRIDGE_NAME, bridge_interface.c_str());
LOGTRACE("brctl addif %s %s", BRIDGE_NAME, bridge_interface.c_str())
if (!br_setif(br_socket_fd, BRIDGE_NAME, bridge_interface.c_str(), true)) {
close(m_hTAP);
@ -215,14 +245,13 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
}
else {
string address = inet;
string netmask = "255.255.255.0";
size_t separatorPos = inet.find('/');
if (separatorPos != string::npos) {
string netmask = "255.255.255.0"; //NOSONAR This hardcoded IP address is safe
if (size_t separatorPos = inet.find('/'); separatorPos != string::npos) {
address = inet.substr(0, separatorPos);
int m;
if (!GetAsInt(inet.substr(separatorPos + 1), m) || m < 8 || m > 32) {
LOGERROR("Invalid CIDR netmask notation '%s'", inet.substr(separatorPos + 1).c_str());
LOGERROR("Invalid CIDR netmask notation '%s'", inet.substr(separatorPos + 1).c_str())
close(m_hTAP);
close(ip_fd);
@ -231,17 +260,16 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
}
// long long is required for compatibility with 32 bit platforms
long long mask = pow(2, 32) - (1 << (32 - m));
char buf[16];
sprintf(buf, "%lld.%lld.%lld.%lld", (mask >> 24) & 0xff, (mask >> 16) & 0xff, (mask >> 8) & 0xff,
mask & 0xff);
netmask = buf;
const auto mask = (long long)(pow(2, 32) - (1 << (32 - m)));
netmask = to_string((mask >> 24) & 0xff) + '.' + to_string((mask >> 16) & 0xff) + '.' +
to_string((mask >> 8) & 0xff) + '.' + to_string(mask & 0xff);
}
LOGTRACE("brctl addbr %s", BRIDGE_NAME);
LOGTRACE("brctl addbr %s", BRIDGE_NAME)
if ((ret = ioctl(br_socket_fd, SIOCBRADDBR, BRIDGE_NAME)) < 0) {
LOGERROR("Can't ioctl SIOCBRADDBR: %s", strerror(errno));
if (ioctl(br_socket_fd, SIOCBRADDBR, BRIDGE_NAME) < 0) {
LOGERROR("Can't ioctl SIOCBRADDBR: %s", strerror(errno))
close(m_hTAP);
close(ip_fd);
@ -249,12 +277,12 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
return false;
}
struct ifreq ifr_a;
ifreq ifr_a;
ifr_a.ifr_addr.sa_family = AF_INET;
strncpy(ifr_a.ifr_name, BRIDGE_NAME, IFNAMSIZ);
struct sockaddr_in* addr = (struct sockaddr_in*)&ifr_a.ifr_addr;
if (inet_pton(AF_INET, address.c_str(), &addr->sin_addr) != 1) {
LOGERROR("Can't convert '%s' into a network address: %s", address.c_str(), strerror(errno));
if (auto addr = (sockaddr_in*)&ifr_a.ifr_addr;
inet_pton(AF_INET, address.c_str(), &addr->sin_addr) != 1) {
LOGERROR("Can't convert '%s' into a network address: %s", address.c_str(), strerror(errno))
close(m_hTAP);
close(ip_fd);
@ -262,12 +290,12 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
return false;
}
struct ifreq ifr_n;
ifreq ifr_n;
ifr_n.ifr_addr.sa_family = AF_INET;
strncpy(ifr_n.ifr_name, BRIDGE_NAME, IFNAMSIZ);
struct sockaddr_in* mask = (struct sockaddr_in*)&ifr_n.ifr_addr;
if (inet_pton(AF_INET, netmask.c_str(), &mask->sin_addr) != 1) {
LOGERROR("Can't convert '%s' into a netmask: %s", netmask.c_str(), strerror(errno));
if (auto mask = (sockaddr_in*)&ifr_n.ifr_addr;
inet_pton(AF_INET, netmask.c_str(), &mask->sin_addr) != 1) {
LOGERROR("Can't convert '%s' into a netmask: %s", netmask.c_str(), strerror(errno))
close(m_hTAP);
close(ip_fd);
@ -275,10 +303,10 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
return false;
}
LOGTRACE("ip address add %s dev %s", inet.c_str(), BRIDGE_NAME);
LOGTRACE("ip address add %s dev %s", inet.c_str(), BRIDGE_NAME)
if (ioctl(ip_fd, SIOCSIFADDR, &ifr_a) < 0 || ioctl(ip_fd, SIOCSIFNETMASK, &ifr_n) < 0) {
LOGERROR("Can't ioctl SIOCSIFADDR or SIOCSIFNETMASK: %s", strerror(errno));
LOGERROR("Can't ioctl SIOCSIFADDR or SIOCSIFNETMASK: %s", strerror(errno))
close(m_hTAP);
close(ip_fd);
@ -287,7 +315,7 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
}
}
LOGTRACE("ip link set dev %s up", BRIDGE_NAME);
LOGTRACE("ip link set dev %s up", BRIDGE_NAME)
if (!ip_link(ip_fd, BRIDGE_NAME, true)) {
close(m_hTAP);
@ -298,10 +326,10 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
}
else
{
LOGINFO("%s is already available", BRIDGE_NAME);
LOGINFO("%s is already available", BRIDGE_NAME)
}
LOGTRACE("ip link set ras0 up");
LOGTRACE("ip link set ras0 up")
if (!ip_link(ip_fd, "ras0", true)) {
close(m_hTAP);
@ -310,7 +338,7 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
return false;
}
LOGTRACE("brctl addif %s ras0", BRIDGE_NAME);
LOGTRACE("brctl addif %s ras0", BRIDGE_NAME)
if (!br_setif(br_socket_fd, BRIDGE_NAME, "ras0", true)) {
close(m_hTAP);
@ -320,105 +348,79 @@ bool CTapDriver::Init(const unordered_map<string, string>& const_params)
}
// Get MAC address
LOGTRACE("Getting the MAC address");
LOGTRACE("Getting the MAC address")
ifr.ifr_addr.sa_family = AF_INET;
if ((ret = ioctl(m_hTAP, SIOCGIFHWADDR, &ifr)) < 0) {
LOGERROR("Can't ioctl SIOCGIFHWADDR: %s", strerror(errno));
if (ioctl(m_hTAP, SIOCGIFHWADDR, &ifr) < 0) {
LOGERROR("Can't ioctl SIOCGIFHWADDR: %s", strerror(errno))
close(m_hTAP);
close(ip_fd);
close(br_socket_fd);
return false;
}
LOGTRACE("Got the MAC");
LOGTRACE("Got the MAC")
// Save MAC address
memcpy(m_MacAddr, ifr.ifr_hwaddr.sa_data, sizeof(m_MacAddr));
memcpy(m_MacAddr.data(), ifr.ifr_hwaddr.sa_data, m_MacAddr.size());
close(ip_fd);
close(br_socket_fd);
LOGINFO("Tap device %s created", ifr.ifr_name);
LOGINFO("Tap device %s created", ifr.ifr_name)
return true;
#endif
}
void CTapDriver::OpenDump(const Filepath& path) {
if (m_pcap == NULL) {
if (m_pcap == nullptr) {
m_pcap = pcap_open_dead(DLT_EN10MB, 65535);
}
if (m_pcap_dumper != NULL) {
if (m_pcap_dumper != nullptr) {
pcap_dump_close(m_pcap_dumper);
}
m_pcap_dumper = pcap_dump_open(m_pcap, path.GetPath());
if (m_pcap_dumper == NULL) {
LOGERROR("Can't open pcap file: %s", pcap_geterr(m_pcap));
if (m_pcap_dumper == nullptr) {
LOGERROR("Can't open pcap file: %s", pcap_geterr(m_pcap))
throw io_exception("Can't open pcap file");
}
LOGTRACE("%s Opened %s for dumping", __PRETTY_FUNCTION__, path.GetPath());
LOGTRACE("%s Opened %s for dumping", __PRETTY_FUNCTION__, path.GetPath())
}
void CTapDriver::Cleanup()
bool CTapDriver::Enable() const
{
int br_socket_fd = -1;
if ((br_socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0)) < 0) {
LOGERROR("Can't open bridge socket: %s", strerror(errno));
} else {
LOGDEBUG("brctl delif %s ras0", BRIDGE_NAME);
if (!br_setif(br_socket_fd, BRIDGE_NAME, "ras0", false)) {
LOGWARN("Warning: Removing ras0 from the bridge failed.");
LOGWARN("You may need to manually remove the ras0 tap device from the bridge");
}
close(br_socket_fd);
}
// Release TAP defice
if (m_hTAP != -1) {
close(m_hTAP);
m_hTAP = -1;
}
if (m_pcap_dumper != NULL) {
pcap_dump_close(m_pcap_dumper);
m_pcap_dumper = NULL;
}
if (m_pcap != NULL) {
pcap_close(m_pcap);
m_pcap = NULL;
}
}
bool CTapDriver::Enable(){
int fd = socket(PF_INET, SOCK_DGRAM, 0);
LOGDEBUG("%s: ip link set ras0 up", __PRETTY_FUNCTION__);
bool result = ip_link(fd, "ras0", true);
const int fd = socket(PF_INET, SOCK_DGRAM, 0);
LOGDEBUG("%s: ip link set ras0 up", __PRETTY_FUNCTION__)
const bool result = ip_link(fd, "ras0", true);
close(fd);
return result;
}
bool CTapDriver::Disable(){
int fd = socket(PF_INET, SOCK_DGRAM, 0);
LOGDEBUG("%s: ip link set ras0 down", __PRETTY_FUNCTION__);
bool result = ip_link(fd, "ras0", false);
bool CTapDriver::Disable() const
{
const int fd = socket(PF_INET, SOCK_DGRAM, 0);
LOGDEBUG("%s: ip link set ras0 down", __PRETTY_FUNCTION__)
const bool result = ip_link(fd, "ras0", false);
close(fd);
return result;
}
void CTapDriver::Flush(){
LOGTRACE("%s", __PRETTY_FUNCTION__);
while(PendingPackets()){
(void)Rx(m_garbage_buffer);
void CTapDriver::Flush()
{
LOGTRACE("%s", __PRETTY_FUNCTION__)
while (PendingPackets()) {
array<BYTE, ETH_FRAME_LEN> m_garbage_buffer;
(void)Receive(m_garbage_buffer.data());
}
}
void CTapDriver::GetMacAddr(BYTE *mac)
void CTapDriver::GetMacAddr(BYTE *mac) const
{
ASSERT(mac);
assert(mac);
memcpy(mac, m_MacAddr, sizeof(m_MacAddr));
memcpy(mac, m_MacAddr.data(), m_MacAddr.size());
}
//---------------------------------------------------------------------------
@ -426,46 +428,40 @@ void CTapDriver::GetMacAddr(BYTE *mac)
// Receive
//
//---------------------------------------------------------------------------
bool CTapDriver::PendingPackets()
bool CTapDriver::PendingPackets() const
{
struct pollfd fds;
ASSERT(m_hTAP != -1);
assert(m_hTAP != -1);
// Check if there is data that can be received
pollfd fds;
fds.fd = m_hTAP;
fds.events = POLLIN | POLLERR;
fds.revents = 0;
poll(&fds, 1, 0);
LOGTRACE("%s %u revents", __PRETTY_FUNCTION__, fds.revents);
LOGTRACE("%s %u revents", __PRETTY_FUNCTION__, fds.revents)
if (!(fds.revents & POLLIN)) {
return false;
}else {
} else {
return true;
}
}
// See https://stackoverflow.com/questions/21001659/crc32-algorithm-implementation-in-c-without-a-look-up-table-and-with-a-public-li
uint32_t crc32(BYTE *buf, int length) {
uint32_t CTapDriver::Crc32(const BYTE *buf, int length) {
uint32_t crc = 0xffffffff;
for (int i = 0; i < length; i++) {
crc ^= buf[i];
for (int j = 0; j < 8; j++) {
uint32_t mask = -(crc & 1);
const uint32_t mask = -((int)crc & 1);
crc = (crc >> 1) ^ (0xEDB88320 & mask);
}
}
return ~crc;
}
//---------------------------------------------------------------------------
//
// Receive
//
//---------------------------------------------------------------------------
int CTapDriver::Rx(BYTE *buf)
int CTapDriver::Receive(BYTE *buf)
{
ASSERT(m_hTAP != -1);
assert(m_hTAP != -1);
// Check if there is data that can be received
if (!PendingPackets()) {
@ -473,9 +469,9 @@ int CTapDriver::Rx(BYTE *buf)
}
// Receive
DWORD dwReceived = read(m_hTAP, buf, ETH_FRAME_LEN);
if (dwReceived == (DWORD)-1) {
LOGWARN("%s Error occured while receiving an packet", __PRETTY_FUNCTION__);
auto dwReceived = (uint32_t)read(m_hTAP, buf, ETH_FRAME_LEN);
if (dwReceived == (uint32_t)-1) {
LOGWARN("%s Error occured while receiving a packet", __PRETTY_FUNCTION__)
return 0;
}
@ -484,52 +480,49 @@ int CTapDriver::Rx(BYTE *buf)
// We need to add the Frame Check Status (FCS) CRC back onto the end of the packet.
// The Linux network subsystem removes it, since most software apps shouldn't ever
// need it.
int crc = crc32(buf, dwReceived);
const int crc = Crc32(buf, dwReceived);
buf[dwReceived + 0] = (BYTE)((crc >> 0) & 0xFF);
buf[dwReceived + 1] = (BYTE)((crc >> 8) & 0xFF);
buf[dwReceived + 2] = (BYTE)((crc >> 16) & 0xFF);
buf[dwReceived + 3] = (BYTE)((crc >> 24) & 0xFF);
LOGDEBUG("%s CRC is %08X - %02X %02X %02X %02X\n", __PRETTY_FUNCTION__, crc, buf[dwReceived+0], buf[dwReceived+1], buf[dwReceived+2], buf[dwReceived+3]);
LOGDEBUG("%s CRC is %08X - %02X %02X %02X %02X\n", __PRETTY_FUNCTION__, crc, buf[dwReceived+0], buf[dwReceived+1], buf[dwReceived+2], buf[dwReceived+3])
// Add FCS size to the received message size
dwReceived += 4;
}
if (m_pcap_dumper != NULL) {
struct pcap_pkthdr h = {
if (m_pcap_dumper != nullptr) {
pcap_pkthdr h = {
.ts = {},
.caplen = dwReceived,
.len = dwReceived,
.len = dwReceived
};
gettimeofday(&h.ts, NULL);
gettimeofday(&h.ts, nullptr);
pcap_dump((u_char*)m_pcap_dumper, &h, buf);
LOGTRACE("%s Dumped %d byte packet (first byte: %02x last byte: %02x)", __PRETTY_FUNCTION__, (unsigned int)dwReceived, buf[0], buf[dwReceived-1]);
LOGTRACE("%s Dumped %d byte packet (first byte: %02x last byte: %02x)", __PRETTY_FUNCTION__, (unsigned int)dwReceived, buf[0], buf[dwReceived-1])
}
// Return the number of bytes
return dwReceived;
}
//---------------------------------------------------------------------------
//
// Send
//
//---------------------------------------------------------------------------
int CTapDriver::Tx(const BYTE *buf, int len)
int CTapDriver::Send(const BYTE *buf, int len)
{
ASSERT(m_hTAP != -1);
assert(m_hTAP != -1);
if (m_pcap_dumper != NULL) {
struct pcap_pkthdr h = {
if (m_pcap_dumper != nullptr) {
pcap_pkthdr h = {
.ts = {},
.caplen = (bpf_u_int32)len,
.len = (bpf_u_int32)len,
};
gettimeofday(&h.ts, NULL);
gettimeofday(&h.ts, nullptr);
pcap_dump((u_char*)m_pcap_dumper, &h, buf);
LOGTRACE("%s Dumped %d byte packet (first byte: %02x last byte: %02x)", __PRETTY_FUNCTION__, (unsigned int)h.len, buf[0], buf[h.len-1]);
LOGTRACE("%s Dumped %d byte packet (first byte: %02x last byte: %02x)", __PRETTY_FUNCTION__, (unsigned int)h.len, buf[0], buf[h.len-1])
}
// Start sending
return write(m_hTAP, buf, len);
return (int)write(m_hTAP, buf, len);
}

View File

@ -7,65 +7,53 @@
// Copyright (C) 2016-2020 GIMONS
// Copyright (C) akuker
//
// [ TAP Driver ]
//
//---------------------------------------------------------------------------
#pragma once
#include <pcap/pcap.h>
#include <net/ethernet.h>
#include "filepath.h"
#include <unordered_map>
#include <vector>
#include <list>
#include <string>
#ifndef ETH_FRAME_LEN
#define ETH_FRAME_LEN 1514
#endif
#include <array>
using namespace std;
//===========================================================================
//
// Linux Tap Driver
//
//===========================================================================
class CTapDriver
{
private:
friend class SCSIDaynaPort;
friend class SCSIBR;
CTapDriver();
~CTapDriver() {}
bool Init(const unordered_map<string, string>&);
static constexpr const char *BRIDGE_NAME = "rascsi_bridge";
public:
void OpenDump(const Filepath& path);
// Capture packets
void Cleanup(); // Cleanup
void GetMacAddr(BYTE *mac); // Get Mac Address
int Rx(BYTE *buf); // Receive
int Tx(const BYTE *buf, int len); // Send
bool PendingPackets(); // Check if there are IP packets available
bool Enable(); // Enable the ras0 interface
bool Disable(); // Disable the ras0 interface
CTapDriver() = default;
~CTapDriver();
CTapDriver(CTapDriver&) = default;
CTapDriver& operator=(const CTapDriver&) = default;
bool Init(const unordered_map<string, string>&);
void OpenDump(const Filepath& path); // Capture packets
void GetMacAddr(BYTE *mac) const;
int Receive(BYTE *buf);
int Send(const BYTE *buf, int len);
bool PendingPackets() const; // Check if there are IP packets available
bool Enable() const; // Enable the ras0 interface
bool Disable() const; // Disable the ras0 interface
void Flush(); // Purge all of the packets that are waiting to be processed
static uint32_t Crc32(const BYTE *, int);
private:
BYTE m_MacAddr[6]; // MAC Address
int m_hTAP; // File handle
array<byte, 6> m_MacAddr; // MAC Address
BYTE m_garbage_buffer[ETH_FRAME_LEN];
int m_hTAP = -1; // File handle
pcap_t *m_pcap;
pcap_dumper_t *m_pcap_dumper;
pcap_t *m_pcap = nullptr;
pcap_dumper_t *m_pcap_dumper = nullptr;
// Prioritized comma-separated list of interfaces to create the bridge for
vector<string> interfaces;
list<string> interfaces;
string inet;
};

View File

@ -3,57 +3,26 @@
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2021 Uwe Seimet
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include <cassert>
#include "rascsi_version.h"
#include "os.h"
#include "log.h"
#include "exceptions.h"
#include "device.h"
#include <cassert>
#include <sstream>
#include <iomanip>
unordered_set<Device *> Device::devices;
using namespace std;
Device::Device(const string& type)
Device::Device(const string& type, int lun) : type(type), lun(lun)
{
assert(type.length() == 4);
devices.insert(this);
this->type = type;
vendor = DEFAULT_VENDOR;
char rev[5];
sprintf(rev, "%02d%02d", rascsi_major_version, rascsi_minor_version);
revision = rev;
ready = false;
reset = false;
attn = false;
supported_luns = 32;
protectable = false;
write_protected = false;
read_only = false;
stoppable = false;
stopped = false;
removable = false;
removed = false;
lockable = false;
locked = false;
block_size_configurable = false;
supports_params = false;
id = 0;
lun = 0;
status_code = STATUS_NOERROR;
}
Device::~Device()
{
devices.erase(this);
ostringstream os;
os << setw(2) << setfill('0') << rascsi_major_version << setw(2) << setfill('0') << rascsi_minor_version;
revision = os.str();
}
void Device::Reset()
@ -63,88 +32,77 @@ void Device::Reset()
reset = false;
}
void Device::SetProtected(bool write_protected)
void Device::SetProtected(bool b)
{
if (!read_only) {
this->write_protected = write_protected;
write_protected = b;
}
}
void Device::SetVendor(const string& vendor)
void Device::SetVendor(const string& v)
{
if (vendor.empty() || vendor.length() > 8) {
throw illegal_argument_exception("Vendor '" + vendor + "' must be between 1 and 8 characters");
if (v.empty() || v.length() > 8) {
throw invalid_argument("Vendor '" + v + "' must be between 1 and 8 characters");
}
this->vendor = vendor;
vendor = v;
}
void Device::SetProduct(const string& product, bool force)
void Device::SetProduct(const string& p, bool force)
{
// Changing the device name is not SCSI compliant
if (!this->product.empty() && !force) {
if (p.empty() || p.length() > 16) {
throw invalid_argument("Product '" + p + "' must be between 1 and 16 characters");
}
// Changing vital product data is not SCSI compliant
if (!product.empty() && !force) {
return;
}
if (product.empty() || product.length() > 16) {
throw illegal_argument_exception("Product '" + product + "' must be between 1 and 16 characters");
}
this->product = product;
product = p;
}
void Device::SetRevision(const string& revision)
void Device::SetRevision(const string& r)
{
if (revision.empty() || revision.length() > 4) {
throw illegal_argument_exception("Revision '" + revision + "' must be between 1 and 4 characters");
if (r.empty() || r.length() > 4) {
throw invalid_argument("Revision '" + r + "' must be between 1 and 4 characters");
}
this->revision = revision;
revision = r;
}
const string Device::GetPaddedName() const
string Device::GetPaddedName() const
{
string name = vendor;
name.append(8 - vendor.length(), ' ');
name += product;
name.append(16 - product.length(), ' ');
name += revision;
name.append(4 - revision.length(), ' ');
ostringstream os;
os << left << setfill(' ') << setw(8) << vendor << setw(16) << product << setw(4) << revision;
const string name = os.str();
assert(name.length() == 28);
return name;
}
const string Device::GetParam(const string& key)
string Device::GetParam(const string& key) const
{
return params.find(key) != params.end() ? params[key] : "";
const auto& it = params.find(key);
return it == params.end() ? "" : it->second;
}
void Device::SetParams(const unordered_map<string, string>& params)
void Device::SetParams(const unordered_map<string, string>& set_params)
{
this->params = GetDefaultParams();
params = default_params;
for (const auto& param : params) {
for (const auto& [key, value] : set_params) {
// It is assumed that there are default parameters for all supported parameters
if (this->params.find(param.first) != this->params.end()) {
this->params[param.first] = param.second;
if (params.find(key) != params.end()) {
params[key] = value;
}
else {
LOGWARN("%s", string("Ignored unknown parameter '" + param.first + "'").c_str());
LOGWARN("%s", string("Ignored unknown parameter '" + key + "'").c_str())
}
}
}
void Device::SetStatusCode(int status_code)
{
if (status_code) {
LOGDEBUG("Error status: Sense Key $%02X, ASC $%02X, ASCQ $%02X", status_code >> 16, (status_code >> 8 &0xff), status_code & 0xff);
}
this->status_code = status_code;
}
bool Device::Start()
{
if (!ready) {
@ -161,6 +119,8 @@ void Device::Stop()
ready = false;
attn = false;
stopped = true;
status_code = 0;
}
bool Device::Eject(bool force)

View File

@ -3,96 +3,53 @@
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2021 Uwe Seimet
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#pragma once
#include <unordered_set>
#include <unordered_map>
#include <string>
using namespace std;
#define DEFAULT_VENDOR "RaSCSI"
//---------------------------------------------------------------------------
//
// Error definition (sense code returned by REQUEST SENSE)
//
// MSB Reserved (0x00)
// Sense Key
// Additional Sense Code (ASC)
// LSB Additional Sense Code Qualifier(ASCQ)
//
//---------------------------------------------------------------------------
#define STATUS_NOERROR 0x00000000 // NO ADDITIONAL SENSE INFO.
#define STATUS_DEVRESET 0x00062900 // POWER ON OR RESET OCCURED
#define STATUS_NOTREADY 0x00023a00 // MEDIUM NOT PRESENT
#define STATUS_ATTENTION 0x00062800 // MEDIUM MAY HAVE CHANGED
#define STATUS_PREVENT 0x00045302 // MEDIUM REMOVAL PREVENTED
#define STATUS_READFAULT 0x00031100 // UNRECOVERED READ ERROR
#define STATUS_WRITEFAULT 0x00030300 // PERIPHERAL DEVICE WRITE FAULT
#define STATUS_WRITEPROTECT 0x00042700 // WRITE PROTECTED
#define STATUS_MISCOMPARE 0x000e1d00 // MISCOMPARE DURING VERIFY
#define STATUS_INVALIDCMD 0x00052000 // INVALID COMMAND OPERATION CODE
#define STATUS_INVALIDLBA 0x00052100 // LOGICAL BLOCK ADDR. OUT OF RANGE
#define STATUS_INVALIDCDB 0x00052400 // INVALID FIELD IN CDB
#define STATUS_INVALIDLUN 0x00052500 // LOGICAL UNIT NOT SUPPORTED
#define STATUS_INVALIDPRM 0x00052600 // INVALID FIELD IN PARAMETER LIST
#define STATUS_INVALIDMSG 0x00054900 // INVALID MESSAGE ERROR
#define STATUS_PARAMLEN 0x00051a00 // PARAMETERS LIST LENGTH ERROR
#define STATUS_PARAMNOT 0x00052601 // PARAMETERS NOT SUPPORTED
#define STATUS_PARAMVALUE 0x00052602 // PARAMETERS VALUE INVALID
#define STATUS_PARAMSAVE 0x00053900 // SAVING PARAMETERS NOT SUPPORTED
#define STATUS_NODEFECT 0x00010000 // DEFECT LIST NOT FOUND
class SCSIDEV;
class Device
class Device //NOSONAR The number of fields and methods is justified, the complexity is low
{
private:
const string DEFAULT_VENDOR = "RaSCSI";
string type;
bool ready;
bool reset;
bool attn;
// Number of supported luns
int supported_luns;
bool ready = false;
bool reset = false;
bool attn = false;
// Device is protectable/write-protected
bool protectable;
bool write_protected;
bool protectable = false;
bool write_protected = false;
// Device is permanently read-only
bool read_only;
bool read_only = false;
// Device can be stopped (parked)/is stopped (parked)
bool stoppable;
bool stopped;
bool stoppable = false;
bool stopped = false;
// Device is removable/removed
bool removable;
bool removed;
bool removable = false;
bool removed = false;
// Device is lockable/locked
bool lockable;
bool locked;
// The block size is configurable
bool block_size_configurable;
bool lockable = false;
bool locked = false;
// Device can be created with parameters
bool supports_params;
bool supports_params = false;
// Device ID and LUN
int32_t id;
int32_t lun;
// Immutable LUN
int lun;
// Device identifier (for INQUIRY)
string vendor;
string vendor = DEFAULT_VENDOR;
string product;
string revision;
@ -103,85 +60,77 @@ private:
unordered_map<string, string> default_params;
// Sense Key, ASC and ASCQ
int status_code;
// MSB Reserved (0x00)
// Sense Key
// Additional Sense Code (ASC)
// LSB Additional Sense Code Qualifier(ASCQ)
int status_code = 0;
protected:
static unordered_set<Device *> devices;
void SetReady(bool b) { ready = b; }
bool IsReset() const { return reset; }
void SetReset(bool b) { reset = b; }
bool IsAttn() const { return attn; }
void SetAttn(bool b) { attn = b; }
int GetStatusCode() const { return status_code; }
string GetParam(const string&) const;
void SetParams(const unordered_map<string, string>&);
Device(const string&, int);
public:
Device(const string&);
virtual ~Device();
// Override for device specific initializations, to be called after all device properties have been set
virtual bool Init(const unordered_map<string, string>&) { return true; };
virtual bool Dispatch(SCSIDEV *) = 0;
virtual ~Device() = default;
const string& GetType() const { return type; }
bool IsReady() const { return ready; }
void SetReady(bool ready) { this->ready = ready; }
bool IsReset() const { return reset; }
void SetReset(bool reset) { this->reset = reset; }
void Reset();
bool IsAttn() const { return attn; }
void SetAttn(bool attn) { this->attn = attn; }
int GetSupportedLuns() const { return supported_luns; }
void SetSupportedLuns(int supported_luns) { this->supported_luns = supported_luns; }
virtual void Reset();
bool IsProtectable() const { return protectable; }
void SetProtectable(bool protectable) { this->protectable = protectable; }
void SetProtectable(bool b) { protectable = b; }
bool IsProtected() const { return write_protected; }
void SetProtected(bool);
bool IsReadOnly() const { return read_only; }
void SetReadOnly(bool read_only) { this->read_only = read_only; }
void SetReadOnly(bool b) { read_only = b; }
bool IsStoppable() const { return stoppable; }
void SetStoppable(bool stoppable) { this->stoppable = stoppable; }
void SetStoppable(bool b) { stoppable = b; }
bool IsStopped() const { return stopped; }
void SetStopped(bool stopped) { this->stopped = stopped; }
void SetStopped(bool b) { stopped = b; }
bool IsRemovable() const { return removable; }
void SetRemovable(bool removable) { this->removable = removable; }
void SetRemovable(bool b) { removable = b; }
bool IsRemoved() const { return removed; }
void SetRemoved(bool removed) { this->removed = removed; }
void SetRemoved(bool b) { removed = b; }
bool IsLockable() const { return lockable; }
void SetLockable(bool lockable) { this->lockable = lockable; }
void SetLockable(bool b) { lockable = b; }
bool IsLocked() const { return locked; }
void SetLocked(bool locked) { this->locked = locked; }
void SetLocked(bool b) { locked = b; }
int32_t GetId() const { return id; }
void SetId(int32_t id) { this->id = id; }
int32_t GetLun() const { return lun; }
void SetLun(int32_t lun) { this->lun = lun; }
virtual int GetId() const = 0;
int GetLun() const { return lun; }
const string GetVendor() const { return vendor; }
string GetVendor() const { return vendor; }
void SetVendor(const string&);
const string GetProduct() const { return product; }
string GetProduct() const { return product; }
void SetProduct(const string&, bool = true);
const string GetRevision() const { return revision; }
string GetRevision() const { return revision; }
void SetRevision(const string&);
const string GetPaddedName() const;
string GetPaddedName() const;
bool SupportsParams() const { return supports_params; }
virtual bool SupportsFile() const { return !supports_params; }
void SupportsParams(bool supports_paams) { this->supports_params = supports_paams; }
const unordered_map<string, string> GetParams() const { return params; }
const string GetParam(const string&);
void SetParams(const unordered_map<string, string>&);
const unordered_map<string, string> GetDefaultParams() const { return default_params; }
void SetDefaultParams(const unordered_map<string, string>& default_params) { this->default_params = default_params; }
void SupportsParams(bool b) { supports_params = b; }
unordered_map<string, string> GetParams() const { return params; }
void SetDefaultParams(const unordered_map<string, string>& p) { default_params = p; }
int GetStatusCode() const { return status_code; }
void SetStatusCode(int);
void SetStatusCode(int s) { status_code = s; }
bool Start();
void Stop();
virtual bool Eject(bool);
bool IsSASIHD() const { return type == "SAHD"; }
bool IsSCSIHD() const { return type == "SCHD" || type == "SCRM"; }
};

View File

@ -3,11 +3,10 @@
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2021 Uwe Seimet
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "sasihd.h"
#include "scsihd.h"
#include "scsihd_nec.h"
#include "scsimo.h"
@ -15,31 +14,24 @@
#include "scsi_printer.h"
#include "scsi_host_bridge.h"
#include "scsi_daynaport.h"
#include "exceptions.h"
#include "rascsi_exceptions.h"
#include "host_services.h"
#include "device_factory.h"
#include <ifaddrs.h>
#include "host_services.h"
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <net/if.h>
using namespace std;
using namespace rascsi_interface;
DeviceFactory::DeviceFactory()
{
sector_sizes[SAHD] = { 256, 512, 1024 };
sector_sizes[SCHD] = { 512, 1024, 2048, 4096 };
sector_sizes[SCRM] = { 512, 1024, 2048, 4096 };
sector_sizes[SCMO] = { 512, 1024, 2048, 4096 };
sector_sizes[SCCD] = { 512, 2048};
// 128 MB, 512 bytes per sector, 248826 sectors
geometries[SCMO][0x797f400] = make_pair(512, 248826);
// 230 MB, 512 bytes per block, 446325 sectors
geometries[SCMO][0xd9eea00] = make_pair(512, 446325);
// 540 MB, 512 bytes per sector, 1041500 sectors
geometries[SCMO][0x1fc8b800] = make_pair(512, 1041500);
// 640 MB, 20248 bytes per sector, 310352 sectors
geometries[SCMO][0x25e28000] = make_pair(2048, 310352);
string network_interfaces;
for (const auto& network_interface : GetNetworkInterfaces()) {
if (!network_interfaces.empty()) {
@ -49,13 +41,13 @@ DeviceFactory::DeviceFactory()
}
default_params[SCBR]["interface"] = network_interfaces;
default_params[SCBR]["inet"] = "10.10.20.1/24";
default_params[SCBR]["inet"] = DEFAULT_IP;
default_params[SCDP]["interface"] = network_interfaces;
default_params[SCDP]["inet"] = "10.10.20.1/24";
default_params[SCDP]["inet"] = DEFAULT_IP;
default_params[SCLP]["cmd"] = "lp -oraw %f";
default_params[SCLP]["timeout"] = "30";
extension_mapping["hdf"] = SAHD;
extension_mapping["hd1"] = SCHD;
extension_mapping["hds"] = SCHD;
extension_mapping["hda"] = SCHD;
extension_mapping["hdn"] = SCHD;
@ -64,19 +56,17 @@ DeviceFactory::DeviceFactory()
extension_mapping["hdr"] = SCRM;
extension_mapping["mos"] = SCMO;
extension_mapping["iso"] = SCCD;
}
DeviceFactory& DeviceFactory::instance()
{
static DeviceFactory instance;
return instance;
device_mapping["bridge"] = SCBR;
device_mapping["daynaport"] = SCDP;
device_mapping["printer"] = SCLP;
device_mapping["services"] = SCHS;
}
string DeviceFactory::GetExtension(const string& filename) const
{
string ext;
size_t separator = filename.rfind('.');
if (separator != string::npos) {
if (const size_t separator = filename.rfind('.'); separator != string::npos) {
ext = filename.substr(separator + 1);
}
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::tolower(c); });
@ -86,187 +76,164 @@ string DeviceFactory::GetExtension(const string& filename) const
PbDeviceType DeviceFactory::GetTypeForFile(const string& filename) const
{
string ext = GetExtension(filename);
const auto& it = extension_mapping.find(ext);
if (it != extension_mapping.end()) {
if (const auto& it = extension_mapping.find(GetExtension(filename)); it != extension_mapping.end()) {
return it->second;
}
else if (filename == "bridge") {
return SCBR;
}
else if (filename == "daynaport") {
return SCDP;
}
else if (filename == "printer") {
return SCLP;
}
else if (filename == "services") {
return SCHS;
if (const auto& it = device_mapping.find(filename); it != device_mapping.end()) {
return it->second;
}
return UNDEFINED;
}
Device *DeviceFactory::CreateDevice(PbDeviceType type, const string& filename)
// ID -1 is used by rascsi to create a temporary device
shared_ptr<PrimaryDevice> DeviceFactory::CreateDevice(const ControllerManager& controller_manager, PbDeviceType type,
int lun, const string& filename)
{
// If no type was specified try to derive the device type from the filename
if (type == UNDEFINED) {
type = GetTypeForFile(filename);
if (type == UNDEFINED) {
return NULL;
return nullptr;
}
}
Device *device = NULL;
try {
switch (type) {
case SAHD:
device = new SASIHD(sector_sizes[SAHD]);
device->SetSupportedLuns(2);
device->SetProduct("SASI HD");
break;
shared_ptr<PrimaryDevice> device;
switch (type) {
case SCHD: {
if (const string ext = GetExtension(filename); ext == "hdn" || ext == "hdi" || ext == "nhd") {
device = make_shared<SCSIHD_NEC>(lun);
} else {
device = make_shared<SCSIHD>(lun, sector_sizes[SCHD], false,
ext == "hd1" ? scsi_level::SCSI_1_CCS : scsi_level::SCSI_2);
case SCHD: {
string ext = GetExtension(filename);
if (ext == "hdn" || ext == "hdi" || ext == "nhd") {
device = new SCSIHD_NEC({ 512 });
} else {
device = new SCSIHD(sector_sizes[SCHD], false);
// Some Apple tools require a particular drive identification
if (ext == "hda") {
device->SetVendor("QUANTUM");
device->SetProduct("FIREBALL");
}
}
device->SetProtectable(true);
device->SetStoppable(true);
break;
// Some Apple tools require a particular drive identification
if (ext == "hda") {
device->SetVendor("QUANTUM");
device->SetProduct("FIREBALL");
}
case SCRM:
device = new SCSIHD(sector_sizes[SCRM], true);
device->SetProtectable(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI HD (REM.)");
break;
case SCMO:
device = new SCSIMO(sector_sizes[SCMO], geometries[SCMO]);
device->SetProtectable(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI MO");
break;
case SCCD:
device = new SCSICD(sector_sizes[SCCD]);
device->SetReadOnly(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI CD-ROM");
break;
case SCBR:
device = new SCSIBR();
device->SetProduct("SCSI HOST BRIDGE");
device->SupportsParams(true);
device->SetDefaultParams(default_params[SCBR]);
break;
case SCDP:
device = new SCSIDaynaPort();
// Since this is an emulation for a specific device the full INQUIRY data have to be set accordingly
device->SetVendor("Dayna");
device->SetProduct("SCSI/Link");
device->SetRevision("1.4a");
device->SupportsParams(true);
device->SetDefaultParams(default_params[SCDP]);
break;
case SCHS:
device = new HostServices();
// Since this is an emulation for a specific device the full INQUIRY data have to be set accordingly
device->SetVendor("RaSCSI");
device->SetProduct("Host Services");
break;
case SCLP:
device = new SCSIPrinter();
device->SetProduct("SCSI PRINTER");
device->SupportsParams(true);
device->SetDefaultParams(default_params[SCLP]);
break;
default:
break;
}
device->SetProtectable(true);
device->SetStoppable(true);
break;
}
catch(const illegal_argument_exception& e) {
// There was an internal problem with setting up the device data for INQUIRY
return NULL;
case SCRM:
device = make_shared<SCSIHD>(lun, sector_sizes[SCRM], true);
device->SetProtectable(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI HD (REM.)");
break;
case SCMO:
device = make_shared<SCSIMO>(lun, sector_sizes[SCMO]);
device->SetProtectable(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI MO");
break;
case SCCD:
device = make_shared<SCSICD>(lun, sector_sizes[SCCD]);
device->SetReadOnly(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI CD-ROM");
break;
case SCBR:
device = make_shared<SCSIBR>(lun);
// Since this is an emulation for a specific driver the product name has to be set accordingly
device->SetProduct("RASCSI BRIDGE");
device->SupportsParams(true);
device->SetDefaultParams(default_params[SCBR]);
break;
case SCDP:
device = make_shared<SCSIDaynaPort>(lun);
// Since this is an emulation for a specific device the full INQUIRY data have to be set accordingly
device->SetVendor("Dayna");
device->SetProduct("SCSI/Link");
device->SetRevision("1.4a");
device->SupportsParams(true);
device->SetDefaultParams(default_params[SCDP]);
break;
case SCHS:
device = make_shared<HostServices>(lun, controller_manager);
// Since this is an emulation for a specific device the full INQUIRY data have to be set accordingly
device->SetVendor("RaSCSI");
device->SetProduct("Host Services");
break;
case SCLP:
device = make_shared<SCSIPrinter>(lun);
device->SetProduct("SCSI PRINTER");
device->SupportsParams(true);
device->SetDefaultParams(default_params[SCLP]);
break;
default:
break;
}
return device;
}
const unordered_set<uint32_t>& DeviceFactory::GetSectorSizes(const string& type)
const unordered_set<uint32_t>& DeviceFactory::GetSectorSizes(PbDeviceType type) const
{
const auto& it = sector_sizes.find(type);
return it != sector_sizes.end() ? it->second : empty_set;
}
const unordered_set<uint32_t>& DeviceFactory::GetSectorSizes(const string& type) const
{
PbDeviceType t = UNDEFINED;
PbDeviceType_Parse(type, &t);
return sector_sizes[t];
return GetSectorSizes(t);
}
const unordered_set<uint64_t> DeviceFactory::GetCapacities(PbDeviceType type) const
const unordered_map<string, string>& DeviceFactory::GetDefaultParams(PbDeviceType type) const
{
unordered_set<uint64_t> keys;
for (auto it = geometries.begin(); it != geometries.end(); ++it) {
keys.insert(it->first);
}
return keys;
const auto& it = default_params.find(type);
return it != default_params.end() ? it->second : empty_map;
}
const list<string> DeviceFactory::GetNetworkInterfaces() const
list<string> DeviceFactory::GetNetworkInterfaces() const
{
list<string> network_interfaces;
struct ifaddrs *addrs;
#ifdef __linux__
ifaddrs *addrs;
getifaddrs(&addrs);
struct ifaddrs *tmp = addrs;
ifaddrs *tmp = addrs;
while (tmp) {
if (tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_PACKET &&
strcmp(tmp->ifa_name, "lo") && strcmp(tmp->ifa_name, "rascsi_bridge")) {
int fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP);
const int fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP);
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, tmp->ifa_name);
if (!ioctl(fd, SIOCGIFFLAGS, &ifr)) {
close(fd);
// Only list interfaces that are up
if (ifr.ifr_flags & IFF_UP) {
network_interfaces.push_back(tmp->ifa_name);
}
}
else {
close(fd);
ifreq ifr = {};
strcpy(ifr.ifr_name, tmp->ifa_name); //NOSONAR Using strcpy is safe here
// Only list interfaces that are up
if (!ioctl(fd, SIOCGIFFLAGS, &ifr) && (ifr.ifr_flags & IFF_UP)) {
network_interfaces.emplace_back(tmp->ifa_name);
}
close(fd);
}
tmp = tmp->ifa_next;
}
freeifaddrs(addrs);
#endif
return network_interfaces;
}

View File

@ -3,7 +3,7 @@
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2021 Uwe Seimet
// Copyright (C) 2021-2022 Uwe Seimet
//
// The DeviceFactory singleton creates devices based on their type and the image file extension
//
@ -11,47 +11,47 @@
#pragma once
#include <string>
#include <list>
#include <unordered_set>
#include <unordered_map>
#include <string>
#include "rascsi_interface.pb.h"
using namespace std;
using namespace rascsi_interface;
typedef pair<uint32_t, uint32_t> Geometry;
class Device;
class ControllerManager;
class PrimaryDevice;
class DeviceFactory
{
DeviceFactory();
~DeviceFactory() {}
const string DEFAULT_IP = "10.10.20.1/24"; //NOSONAR This hardcoded IP address is safe
public:
static DeviceFactory& instance();
DeviceFactory();
~DeviceFactory() = default;
Device *CreateDevice(PbDeviceType, const string&);
shared_ptr<PrimaryDevice> CreateDevice(const ControllerManager&, PbDeviceType, int, const string&);
PbDeviceType GetTypeForFile(const string&) const;
const unordered_set<uint32_t>& GetSectorSizes(PbDeviceType type) { return sector_sizes[type]; }
const unordered_set<uint32_t>& GetSectorSizes(const string&);
const unordered_set<uint64_t> GetCapacities(PbDeviceType) const;
const unordered_map<string, string>& GetDefaultParams(PbDeviceType type) { return default_params[type]; }
const list<string> GetNetworkInterfaces() const;
const unordered_map<string, PbDeviceType> GetExtensionMapping() const { return extension_mapping; }
const unordered_set<uint32_t>& GetSectorSizes(PbDeviceType type) const;
const unordered_set<uint32_t>& GetSectorSizes(const string&) const;
const unordered_map<string, string>& GetDefaultParams(PbDeviceType type) const;
list<string> GetNetworkInterfaces() const;
const unordered_map<string, PbDeviceType>& GetExtensionMapping() const { return extension_mapping; }
private:
unordered_map<PbDeviceType, unordered_set<uint32_t>> sector_sizes;
string GetExtension(const string&) const;
// Optional mapping of drive capacities to drive geometries
unordered_map<PbDeviceType, unordered_map<uint64_t, Geometry>> geometries;
unordered_map<PbDeviceType, unordered_set<uint32_t>> sector_sizes;
unordered_map<PbDeviceType, unordered_map<string, string>> default_params;
unordered_map<string, PbDeviceType> extension_mapping;
string GetExtension(const string&) const;
unordered_map<string, PbDeviceType> device_mapping;
unordered_set<uint32_t> empty_set;
unordered_map<string, string> empty_map;
};

File diff suppressed because it is too large Load Diff

View File

@ -11,152 +11,141 @@
// Imported sava's Anex86/T98Next image and MO format support patch.
// Comments translated to english by akuker.
//
// [ Disk ]
//
//---------------------------------------------------------------------------
#pragma once
#include "log.h"
#include "scsi.h"
#include "controllers/scsidev_ctrl.h"
#include "device.h"
#include "device_factory.h"
#include "disk_track_cache.h"
#include "file_support.h"
#include "disk_track.h"
#include "disk_cache.h"
#include "filepath.h"
#include "interfaces/scsi_block_commands.h"
#include "mode_page_device.h"
#include <string>
#include <unordered_set>
#include <unordered_map>
using namespace std;
class Disk : public ModePageDevice, ScsiBlockCommands
using id_set = pair<int, int>;
class Disk : public ModePageDevice, private ScsiBlockCommands
{
private:
enum access_mode { RW6, RW10, RW16, SEEK6, SEEK10 };
// The supported configurable block sizes, empty if not configurable
Dispatcher<Disk> dispatcher;
unique_ptr<DiskCache> cache;
// The supported configurable sector sizes, empty if not configurable
unordered_set<uint32_t> sector_sizes;
uint32_t configured_sector_size;
uint32_t configured_sector_size = 0;
// The mapping of supported capacities to block sizes and block counts, empty if there is no capacity restriction
unordered_map<uint64_t, Geometry> geometries;
// Sector size shift count (9=512, 10=1024, 11=2048, 12=4096)
uint32_t size_shift_count = 0;
typedef struct {
uint32_t size; // Sector Size (8=256, 9=512, 10=1024, 11=2048, 12=4096)
// TODO blocks should be a 64 bit value in order to support higher capacities
uint32_t blocks; // Total number of sectors
DiskCache *dcache; // Disk cache
off_t image_offset; // Offset to actual data
bool is_medium_changed;
} disk_t;
// Total number of sectors
uint64_t blocks = 0;
Dispatcher<Disk, SASIDEV> dispatcher;
bool is_medium_changed = false;
Filepath diskpath;
// The list of image files in use and the IDs and LUNs using these files
static unordered_map<string, id_set> reserved_files;
public:
Disk(const string&);
virtual ~Disk();
virtual bool Dispatch(SCSIDEV *) override;
Disk(const string&, int);
~Disk() override;
bool Dispatch(scsi_command) override;
void MediumChanged();
void ReserveFile(const string&);
// Media Operations
virtual void Open(const Filepath& path);
void GetPath(Filepath& path) const;
bool Eject(bool) override;
private:
friend class SASIDEV;
typedef ModePageDevice super;
// Commands covered by the SCSI specification (see https://www.t10.org/drafts.htm)
void StartStopUnit(SASIDEV *);
void SendDiagnostic(SASIDEV *);
void PreventAllowMediumRemoval(SASIDEV *);
void SynchronizeCache10(SASIDEV *);
void SynchronizeCache16(SASIDEV *);
void ReadDefectData10(SASIDEV *);
virtual void Read6(SASIDEV *);
void Read10(SASIDEV *) override;
void Read16(SASIDEV *) override;
virtual void Write6(SASIDEV *);
void Write10(SASIDEV *) override;
void Write16(SASIDEV *) override;
void ReadLong10(SASIDEV *);
void ReadLong16(SASIDEV *);
void WriteLong10(SASIDEV *);
void WriteLong16(SASIDEV *);
void Verify10(SASIDEV *);
void Verify16(SASIDEV *);
void Seek(SASIDEV *);
void Seek10(SASIDEV *);
virtual void ReadCapacity10(SASIDEV *) override;
void ReadCapacity16(SASIDEV *) override;
void Reserve(SASIDEV *);
void Release(SASIDEV *);
public:
// Commands covered by the SCSI specification (see https://www.t10.org/drafts.htm)
void Rezero(SASIDEV *);
void FormatUnit(SASIDEV *) override;
void ReassignBlocks(SASIDEV *);
void Seek6(SASIDEV *);
// Command helpers
virtual int WriteCheck(DWORD block);
virtual bool Write(const DWORD *cdb, const BYTE *buf, DWORD block);
bool StartStop(const DWORD *cdb);
bool SendDiag(const DWORD *cdb) const;
virtual int WriteCheck(uint64_t);
virtual void Write(const vector<int>&, const vector<BYTE>&, uint64_t);
virtual int Read(const DWORD *cdb, BYTE *buf, uint64_t block);
virtual int Read(const vector<int>&, vector<BYTE>& , uint64_t);
uint32_t GetSectorSizeInBytes() const;
void SetSectorSizeInBytes(uint32_t, bool);
uint32_t GetSectorSizeShiftCount() const;
void SetSectorSizeShiftCount(uint32_t);
bool IsSectorSizeConfigurable() const;
unordered_set<uint32_t> GetSectorSizes() const;
void SetSectorSizes(const unordered_set<uint32_t>&);
uint32_t GetConfiguredSectorSize() const;
bool SetConfiguredSectorSize(uint32_t);
void SetGeometries(const unordered_map<uint64_t, Geometry>&);
bool SetGeometryForCapacity(uint64_t);
uint64_t GetBlockCount() const;
void SetBlockCount(uint32_t);
void FlushCache();
bool IsSectorSizeConfigurable() const { return !sector_sizes.empty(); }
bool SetConfiguredSectorSize(const DeviceFactory&, uint32_t);
uint64_t GetBlockCount() const { return blocks; }
void FlushCache() override;
virtual void Open(const Filepath&);
void GetPath(Filepath& path) const { path = diskpath; }
void ReserveFile(const Filepath&, int, int) const;
void UnreserveFile() const;
static void UnreserveAll();
bool FileExists(const Filepath&);
static unordered_map<string, id_set> GetReservedFiles() { return reserved_files; }
static void SetReservedFiles(const unordered_map<string, id_set>& files_in_use) { reserved_files = files_in_use; }
static bool GetIdsForReservedFile(const Filepath&, int&, int&);
private:
using super = ModePageDevice;
// Commands covered by the SCSI specifications (see https://www.t10.org/drafts.htm)
void StartStopUnit();
void SendDiagnostic();
void PreventAllowMediumRemoval();
void SynchronizeCache();
void ReadDefectData10();
virtual void Read6();
void Read10() override;
void Read16() override;
virtual void Write6();
void Write10() override;
void Write16() override;
void Verify10();
void Verify16();
void Seek();
void Seek10();
void ReadCapacity10() override;
void ReadCapacity16() override;
void Reserve();
void Release();
void Rezero();
void FormatUnit() override;
void ReassignBlocks();
void Seek6();
void Read(access_mode);
void Write(access_mode);
void Verify(access_mode);
void ReadWriteLong10();
void ReadWriteLong16();
void ReadCapacity16_ReadLong16();
void ValidateBlockAddress(access_mode) const;
bool CheckAndGetStartAndCount(uint64_t&, uint32_t&, access_mode) const;
int ModeSense6(const vector<int>&, vector<BYTE>&) const override;
int ModeSense10(const vector<int>&, vector<BYTE>&) const override;
protected:
int ModeSense6(const DWORD *cdb, BYTE *buf);
int ModeSense10(const DWORD *cdb, BYTE *buf, int);
virtual void SetDeviceParameters(BYTE *);
void AddModePages(map<int, vector<BYTE>>&, int, bool) const override;
virtual void AddErrorPage(map<int, vector<BYTE>>&, bool) const;
virtual void AddFormatPage(map<int, vector<BYTE>>&, bool) const;
virtual void AddDrivePage(map<int, vector<BYTE>>&, bool) const;
void AddCachePage(map<int, vector<BYTE>>&, bool) const;
virtual void AddVendorPage(map<int, vector<BYTE>>&, int, bool) const;
void SetUpCache(const Filepath&, off_t, bool = false);
void ResizeCache(const Filepath&, bool);
// Internal disk data
disk_t disk;
private:
void Read(SASIDEV *, uint64_t);
void Write(SASIDEV *, uint64_t);
void Verify(SASIDEV *, uint64_t);
void ReadWriteLong10(SASIDEV *);
void ReadWriteLong16(SASIDEV *);
void ReadCapacity16_ReadLong16(SASIDEV *);
bool Format(const DWORD *cdb);
bool ValidateBlockAddress(SASIDEV *, access_mode);
bool GetStartAndCount(SASIDEV *, uint64_t&, uint32_t&, access_mode);
void SetUpModePages(map<int, vector<byte>>&, int, bool) const override;
virtual void AddErrorPage(map<int, vector<byte>>&, bool) const;
virtual void AddFormatPage(map<int, vector<byte>>&, bool) const;
virtual void AddDrivePage(map<int, vector<byte>>&, bool) const;
void AddCachePage(map<int, vector<byte>>&, bool) const;
virtual void AddVendorPage(map<int, vector<byte>>&, int, bool) const;
unordered_set<uint32_t> GetSectorSizes() const;
void SetSectorSizes(const unordered_set<uint32_t>& sizes) { sector_sizes = sizes; }
void SetSectorSizeInBytes(uint32_t);
uint32_t GetSectorSizeShiftCount() const { return size_shift_count; }
void SetSectorSizeShiftCount(uint32_t count) { size_shift_count = count; }
uint32_t GetConfiguredSectorSize() const;
void SetBlockCount(uint64_t b) { blocks = b; }
void SetPath(const Filepath& path) { diskpath = path; }
};

View File

@ -0,0 +1,199 @@
//---------------------------------------------------------------------------
//
// X68000 EMULATOR "XM6"
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
//
// XM6i
// Copyright (C) 2010-2015 isaki@NetBSD.org
// Copyright (C) 2010 Y.Sugahara
//
// Imported sava's Anex86/T98Next image and MO format support patch.
// Comments translated to english by akuker.
//
//---------------------------------------------------------------------------
#include "disk_track.h"
#include "disk_cache.h"
#include <cstdlib>
#include <cassert>
DiskCache::DiskCache(const Filepath& path, int size, uint32_t blocks, off_t imgoff)
: sec_size(size), sec_blocks(blocks), imgoffset(imgoff)
{
assert(blocks > 0);
assert(imgoff >= 0);
sec_path = path;
}
bool DiskCache::Save() const
{
// Save track
for (const cache_t& c : cache) {
// Save if this is a valid track
if (c.disktrk && !c.disktrk->Save(sec_path)) {
return false;
}
}
return true;
}
shared_ptr<DiskTrack> DiskCache::GetTrack(uint32_t block)
{
// Update first
UpdateSerialNumber();
// Calculate track (fixed to 256 sectors/track)
int track = block >> 8;
// Get track data
return Assign(track);
}
bool DiskCache::ReadSector(vector<BYTE>& buf, uint32_t block)
{
shared_ptr<DiskTrack> disktrk = GetTrack(block);
if (disktrk == nullptr) {
return false;
}
// Read the track data to the cache
return disktrk->ReadSector(buf, block & 0xff);
}
bool DiskCache::WriteSector(const vector<BYTE>& buf, uint32_t block)
{
shared_ptr<DiskTrack> disktrk = GetTrack(block);
if (disktrk == nullptr) {
return false;
}
// Write the data to the cache
return disktrk->WriteSector(buf, block & 0xff);
}
//---------------------------------------------------------------------------
//
// Track Assignment
//
//---------------------------------------------------------------------------
shared_ptr<DiskTrack> DiskCache::Assign(int track)
{
assert(sec_size != 0);
assert(track >= 0);
// First, check if it is already assigned
for (cache_t& c : cache) {
if (c.disktrk && c.disktrk->GetTrack() == track) {
// Track match
c.serial = serial;
return c.disktrk;
}
}
// Next, check for empty
for (size_t i = 0; i < cache.size(); i++) {
if (cache[i].disktrk == nullptr) {
// Try loading
if (Load((int)i, track, nullptr)) {
// Success loading
cache[i].serial = serial;
return cache[i].disktrk;
}
// Load failed
return nullptr;
}
}
// Finally, find the youngest serial number and delete it
// Set index 0 as candidate c
uint32_t s = cache[0].serial;
size_t c = 0;
// Compare candidate with serial and update to smaller one
for (size_t i = 0; i < cache.size(); i++) {
assert(cache[i].disktrk);
// Compare and update the existing serial
if (cache[i].serial < s) {
s = cache[i].serial;
c = i;
}
}
// Save this track
if (!cache[c].disktrk->Save(sec_path)) {
return nullptr;
}
// Delete this track
shared_ptr<DiskTrack> disktrk = cache[c].disktrk;
cache[c].disktrk.reset();
if (Load((int)c, track, disktrk)) {
// Successful loading
cache[c].serial = serial;
return cache[c].disktrk;
}
// Load failed
return nullptr;
}
//---------------------------------------------------------------------------
//
// Load cache
//
//---------------------------------------------------------------------------
bool DiskCache::Load(int index, int track, shared_ptr<DiskTrack> disktrk)
{
assert(index >= 0 && index < (int)cache.size());
assert(track >= 0);
assert(cache[index].disktrk == nullptr);
// Get the number of sectors on this track
int sectors = sec_blocks - (track << 8);
assert(sectors > 0);
if (sectors > 0x100) {
sectors = 0x100;
}
// Create a disk track
if (disktrk == nullptr) {
disktrk = make_shared<DiskTrack>();
}
// Initialize disk track
disktrk->Init(track, sec_size, sectors, cd_raw, imgoffset);
// Try loading
if (!disktrk->Load(sec_path)) {
// Failure
return false;
}
// Allocation successful, work set
cache[index].disktrk = disktrk;
return true;
}
void DiskCache::UpdateSerialNumber()
{
// Update and do nothing except 0
serial++;
if (serial != 0) {
return;
}
// Clear serial of all caches
for (cache_t& c : cache) {
c.serial = 0;
}
}

View File

@ -0,0 +1,64 @@
//---------------------------------------------------------------------------
//
// X68000 EMULATOR "XM6"
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
//
// XM6i
// Copyright (C) 2010-2015 isaki@NetBSD.org
//
// Imported sava's Anex86/T98Next image and MO format support patch.
// Comments translated to english by akuker.
//
//---------------------------------------------------------------------------
#pragma once
#include "filepath.h"
#include <array>
#include <memory>
using namespace std;
class DiskCache
{
// Number of tracks to cache
static const int CACHE_MAX = 16;
public:
// Internal data definition
using cache_t = struct {
shared_ptr<DiskTrack> disktrk; // Disk Track
uint32_t serial; // Serial
};
DiskCache(const Filepath& path, int size, uint32_t blocks, off_t imgoff = 0);
~DiskCache() = default;
void SetRawMode(bool b) { cd_raw = b; } // CD-ROM raw mode setting
// Access
bool Save() const; // Save and release all
bool ReadSector(vector<BYTE>&, uint32_t); // Sector Read
bool WriteSector(const vector<BYTE>&, uint32_t); // Sector Write
private:
// Internal Management
shared_ptr<DiskTrack> Assign(int);
shared_ptr<DiskTrack> GetTrack(uint32_t);
bool Load(int index, int track, shared_ptr<DiskTrack>);
void UpdateSerialNumber();
// Internal data
array<cache_t, CACHE_MAX> cache = {}; // Cache management
uint32_t serial = 0; // Last serial number
Filepath sec_path; // Path
int sec_size; // Sector Size (8=256, 9=512, 10=1024, 11=2048, 12=4096)
int sec_blocks; // Blocks per sector
bool cd_raw = false; // CD-ROM RAW mode
off_t imgoffset; // Offset to actual data
};

View File

@ -0,0 +1,286 @@
//---------------------------------------------------------------------------
//
// X68000 EMULATOR "XM6"
//
// Copyright (C) 2001-2006 (ytanaka@ipc-tokai.or.jp)
// Copyright (C) 2014-2020 GIMONS
//
// XM6i
// Copyright (C) 2010-2015 isaki@NetBSD.org
// Copyright (C) 2010 Y.Sugahara
//
// Imported sava's Anex86/T98Next image and MO format support patch.
// Comments translated to english by akuker.
//
//---------------------------------------------------------------------------
#include "log.h"
#include "fileio.h"
#include "disk_track.h"
DiskTrack::~DiskTrack()
{
// Release memory, but do not save automatically
free(dt.buffer);
}
void DiskTrack::Init(int track, int size, int sectors, bool raw, off_t imgoff)
{
assert(track >= 0);
assert((sectors > 0) && (sectors <= 0x100));
assert(imgoff >= 0);
// Set Parameters
dt.track = track;
dt.size = size;
dt.sectors = sectors;
dt.raw = raw;
// Not initialized (needs to be loaded)
dt.init = false;
// Not Changed
dt.changed = false;
// Offset to actual data
dt.imgoffset = imgoff;
}
bool DiskTrack::Load(const Filepath& path)
{
// Not needed if already loaded
if (dt.init) {
assert(dt.buffer);
return true;
}
// Calculate offset (previous tracks are considered to hold 256 sectors)
off_t offset = ((off_t)dt.track << 8);
if (dt.raw) {
assert(dt.size == 11);
offset *= 0x930;
offset += 0x10;
} else {
offset <<= dt.size;
}
// Add offset to real image
offset += dt.imgoffset;
// Calculate length (data size of this track)
const int length = dt.sectors << dt.size;
// Allocate buffer memory
assert((dt.sectors > 0) && (dt.sectors <= 0x100));
if (dt.buffer == nullptr) {
if (posix_memalign((void **)&dt.buffer, 512, ((length + 511) / 512) * 512)) {
LOGWARN("%s posix_memalign failed", __PRETTY_FUNCTION__)
}
dt.length = length;
}
if (dt.buffer == nullptr) {
return false;
}
// Reallocate if the buffer length is different
if (dt.length != (uint32_t)length) {
free(dt.buffer);
if (posix_memalign((void **)&dt.buffer, 512, ((length + 511) / 512) * 512)) {
LOGWARN("%s posix_memalign failed", __PRETTY_FUNCTION__)
}
dt.length = length;
}
// Resize and clear changemap
dt.changemap.resize(dt.sectors);
fill(dt.changemap.begin(), dt.changemap.end(), false);
// Read from File
Fileio fio;
if (!fio.OpenDIO(path, Fileio::OpenMode::ReadOnly)) {
return false;
}
if (dt.raw) {
// Split Reading
for (int i = 0; i < dt.sectors; i++) {
// Seek
if (!fio.Seek(offset)) {
fio.Close();
return false;
}
// Read
if (!fio.Read(&dt.buffer[i << dt.size], 1 << dt.size)) {
fio.Close();
return false;
}
// Next offset
offset += 0x930;
}
} else {
// Continuous reading
if (!fio.Seek(offset)) {
fio.Close();
return false;
}
if (!fio.Read(dt.buffer, length)) {
fio.Close();
return false;
}
}
fio.Close();
// Set a flag and end normally
dt.init = true;
dt.changed = false;
return true;
}
bool DiskTrack::Save(const Filepath& path)
{
// Not needed if not initialized
if (!dt.init) {
return true;
}
// Not needed unless changed
if (!dt.changed) {
return true;
}
// Need to write
assert(dt.buffer);
assert((dt.sectors > 0) && (dt.sectors <= 0x100));
// Writing in RAW mode is not allowed
assert(!dt.raw);
// Calculate offset (previous tracks are considered to hold 256 sectors)
off_t offset = ((off_t)dt.track << 8);
offset <<= dt.size;
// Add offset to real image
offset += dt.imgoffset;
// Calculate length per sector
const int length = 1 << dt.size;
// Open file
Fileio fio;
if (!fio.Open(path, Fileio::OpenMode::ReadWrite)) {
return false;
}
// Partial write loop
int total;
for (int i = 0; i < dt.sectors;) {
// If changed
if (dt.changemap[i]) {
// Initialize write size
total = 0;
// Seek
if (!fio.Seek(offset + ((off_t)i << dt.size))) {
fio.Close();
return false;
}
// Consectutive sector length
int j;
for (j = i; j < dt.sectors; j++) {
// end when interrupted
if (!dt.changemap[j]) {
break;
}
// Add one sector
total += length;
}
// Write
if (!fio.Write(&dt.buffer[i << dt.size], total)) {
fio.Close();
return false;
}
// To unmodified sector
i = j;
} else {
// Next Sector
i++;
}
}
fio.Close();
// Drop the change flag and exit
fill(dt.changemap.begin(), dt.changemap.end(), false);
dt.changed = false;
return true;
}
bool DiskTrack::ReadSector(vector<BYTE>& buf, int sec) const
{
assert(sec >= 0 && sec < 0x100);
LOGTRACE("%s reading sector: %d", __PRETTY_FUNCTION__,sec)
// Error if not initialized
if (!dt.init) {
return false;
}
// // Error if the number of sectors exceeds the valid number
if (sec >= dt.sectors) {
return false;
}
// Copy
assert(dt.buffer);
assert((dt.sectors > 0) && (dt.sectors <= 0x100));
memcpy(buf.data(), &dt.buffer[(off_t)sec << dt.size], (off_t)1 << dt.size);
// Success
return true;
}
bool DiskTrack::WriteSector(const vector<BYTE>& buf, int sec)
{
assert((sec >= 0) && (sec < 0x100));
assert(!dt.raw);
// Error if not initialized
if (!dt.init) {
return false;
}
// // Error if the number of sectors exceeds the valid number
if (sec >= dt.sectors) {
return false;
}
// Calculate offset and length
const int offset = sec << dt.size;
const int length = 1 << dt.size;
// Compare
assert(dt.buffer);
assert((dt.sectors > 0) && (dt.sectors <= 0x100));
if (memcmp(buf.data(), &dt.buffer[offset], length) == 0) {
// Exit normally since it's attempting to write the same thing
return true;
}
// Copy, change
memcpy(&dt.buffer[offset], buf.data(), length);
dt.changemap[sec] = true;
dt.changed = true;
// Success
return true;
}

Some files were not shown because too many files have changed in this diff Show More