Add quote bridge and quote handoff flow

This commit is contained in:
2026-06-29 18:41:45 -05:00
parent be1475dd8e
commit a329168765
17 changed files with 1707 additions and 509 deletions
+379
View File
@@ -0,0 +1,379 @@
# Quote Bridge Automation Design
## Goal
Extend `quote-bridge` so it can do more than move quote files. The target design is a helper app that:
1. watches for quote JSON files,
2. selects the correct location profile,
3. launches the DOS app through DOSBox,
4. simulates user input,
5. enters quote data into the DOS application,
6. records success or failure.
## Recommended Design
Keep the automation separated into four layers:
1. `quote` JSON
- Business data coming from the Flask app.
2. `profile` JSON
- Machine and location configuration.
3. `instruction set` JSON
- The keystroke workflow for the DOS app.
4. `executor`
- Python code that reads the quote, profile, and instruction set and sends keys to the DOS app.
This separation keeps machine config, business data, and workflow logic from getting mixed together.
## Why Not Put Everything In The Profile
The location profile should define:
- where the remote share is mounted,
- which DOS app folder to use,
- which DOSBox binary/config to use,
- which instruction set to execute,
- which location-specific values apply.
It should not hold the full keystroke workflow. If the whole flow lives in the profile, each location file becomes hard to maintain.
## Recommended Folder Structure
```text
quote-bridge/
├── listener.py
├── dosbox_processor.py
├── profiles/
│ ├── default.json
│ ├── IOLA.json
│ ├── KC.json
│ └── LINDS.json
├── instruction-sets/
│ ├── create-quote-v1.json
│ └── create-quote-iola-v1.json
└── value-maps/
└── optional-future-files.json
```
## Profile Responsibilities
Profiles should define environment and per-location values.
Example:
```json
{
"profileName": "IOLA",
"dosboxBin": "dosbox-x",
"mountPath": "/mnt/iola-cgw",
"mountDrive": "C",
"workingDirectory": "CGWAPP",
"bridgeCommand": "BRIDGE.BAT",
"instructionSet": "create-quote-v1",
"variables": {
"taxCode": "ABC",
"locationCode": "IOLA"
}
}
```
Use the profile for:
- DOSBox path/config
- remote mount path
- DOS working directory
- instruction set selection
- per-location values like tax code, warehouse, location code, salesperson, or other defaults
## Instruction Set Responsibilities
Instruction sets should define the interactive workflow.
Instead of using only freeform key strings, use a small action DSL.
Example:
```json
{
"name": "create-quote-v1",
"version": 1,
"steps": [
{
"action": "text",
"value": "${secrets.username}"
},
{
"action": "key",
"value": "ENTER"
},
{
"action": "text",
"value": "${secrets.password}"
},
{
"action": "key",
"value": "ENTER"
},
{
"action": "text",
"value": "${runtime.today}"
},
{
"action": "key",
"value": "ENTER"
},
{
"action": "loop",
"source": "items",
"steps": [
{
"action": "text",
"value": "${item.productCode}"
},
{
"action": "key",
"value": "ENTER"
},
{
"action": "text",
"value": "${item.quantity}"
},
{
"action": "key",
"value": "ENTER"
}
]
}
]
}
```
## Recommended Action Types
The executor should support a small set of explicit actions:
- `text`
- `key`
- `combo`
- `sleep`
- `wait`
- `loop`
- `conditional`
- `set-variable`
- optional future `assert`
This is better than a plain `keys: "abc"` design because real workflows need timing, branching, loops, and variable substitution.
## Special Keys And Combos
Use symbolic key names for special keys:
- `ENTER`
- `ESC`
- `UP`
- `DOWN`
- `LEFT`
- `RIGHT`
- `TAB`
- `BACKSPACE`
- `F1` through `F12`
For combos, prefer an array form.
Example:
```json
{
"action": "combo",
"keys": ["SHIFT", "~"]
}
```
That is less ambiguous than a single string.
## Handling Location-Specific Differences
There are two kinds of per-location differences.
### 1. Data Differences
Examples:
- tax value
- warehouse code
- location code
- default salesperson
These should stay in the profile:
```json
{
"variables": {
"taxCode": "IOLA-TAX",
"warehouseCode": "01"
}
}
```
### 2. Flow Differences
Examples:
- one location needs two extra keys
- one location lands on a different screen
- one location skips a field
If the difference is small, use the same instruction set and substitute different values.
If the difference is structural, create a separate instruction set.
Examples:
- `create-quote-v1`
- `create-quote-iola-v1`
That is cleaner than putting location branches on every single step.
## Suggested Runtime Context
When the executor runs, it should build a context containing:
- `quote`
- `items`
- `profile.variables`
- `runtime.today`
- `secrets.username`
- `secrets.password`
Then placeholders such as `${item.productCode}` or `${profile.variables.taxCode}` can be resolved during execution.
## How The Executor Should Work
At runtime:
1. Read quote file.
2. Read `createdBy.location` from the quote.
3. Load matching location profile.
4. Load the instruction set named by the profile.
5. Build runtime context.
6. Launch DOSBox.
7. Focus the DOSBox window if needed.
8. Execute steps sequentially.
9. Log each action.
10. On success, move file to `processed`.
11. On failure, move file to `failed`.
## How To Actually Send Keys
DOSBox runs the DOS app, but a separate automation backend is usually needed to send dynamic interactive keys.
On Ubuntu, the likely choices are:
- `xdotool` for X11
- `ydotool` for Wayland
That means the likely stack is:
- DOSBox-X runs the DOS app
- Python executor controls the DOSBox window
- `xdotool` or `ydotool` sends keystrokes
## Installation Requirements
Minimum:
- DOSBox or preferably DOSBox-X
- access to the DOS application files from Ubuntu
- Python
Useful additions:
- `xdotool` for X11-based key injection
- `ydotool` for Wayland-based key injection if needed
- `cifs-utils` for mounting Windows shares on Ubuntu
## Security Note
Do not store real usernames and passwords in committed profile JSON files.
Better options:
- environment variables
- a local untracked secrets JSON file
- a machine-local config file ignored by git
Example approach:
```json
{
"usernameEnv": "IOLA_DOS_USERNAME",
"passwordEnv": "IOLA_DOS_PASSWORD"
}
```
## Constraints And Risks
The hardest part is not the JSON format. The difficult part is making the execution deterministic enough that the DOS app is always on the expected screen.
Main risks:
- timing drift
- focus problems
- unexpected dialogs
- location-specific screen differences
- item mapping differences between web app and DOS app
Because of that, start small and keep logging detailed.
## Recommended First Implementation Scope
Start with a narrow slice:
1. one instruction set for login + single item entry
2. one location profile
3. one automation backend using `xdotool`
4. fixed waits only, no screen-reading yet
5. detailed action logging
After that works, extend to:
1. multi-item loops
2. per-location variables
3. per-location alternate flows
4. optional checkpoints/assertions
5. result files with external quote/order references
## Next Steps For quote-bridge
### Phase 1: Structure
1. Add `instruction-sets/` folder.
2. Add one starter instruction set file such as `create-quote-v1.json`.
3. Extend location profiles to include `instructionSet` and optional `variables`.
4. Add a local secrets mechanism for usernames/passwords.
### Phase 2: Execution Engine
1. Create an automation executor module.
2. Implement action handlers for:
- `text`
- `key`
- `combo`
- `sleep`
- `loop`
3. Add placeholder resolution for quote/profile/runtime values.
4. Add step-by-step logging.
### Phase 3: Input Backend
1. Decide whether Ubuntu is running X11 or Wayland.
2. If X11, install and integrate `xdotool`.
3. If Wayland, evaluate `ydotool`.
4. Add DOSBox window targeting/focus handling.
### Phase 4: DOS App Integration
1. Build a real `BRIDGE.BAT` startup contract.
2. Define the login sequence.
3. Define single-item quote entry.
4. Test with one known product and one location.
### Phase 5: Expansion
1. Add multi-item support.
2. Add value/code translation maps if needed.
3. Add location-specific instruction set variants only where necessary.
4. Add result file generation with success/failure metadata.
## Recommendation
Keep workflow logic in instruction sets, keep environment/location settings in profiles, and keep secrets out of tracked JSON files.
That will give `quote-bridge` the best chance of staying maintainable as the DOS automation grows.
+269
View File
@@ -0,0 +1,269 @@
# Quote Bridge (Listener App)
This is a separate Python app that runs independently from the Flask app.
It watches for quote files in `shared/outgoing` and can run in two modes:
- `move` (default): move incoming files to `shared/processed`
- `dosbox`: run DOSBox command/script per quote file, then move to
`shared/processed` on success or `shared/failed` on error
## Run
From repository root:
```bash
./.venv/bin/python quote-bridge/listener.py
```
Or from this folder:
```bash
../.venv/bin/python listener.py
```
## Install DOSBox On Ubuntu
You will need DOSBox installed on the Ubuntu machine if you want to use
`QUOTE_PROCESSOR_MODE=dosbox`.
### Option 1: Standard DOSBox
```bash
sudo apt update
sudo apt install dosbox
```
### Option 2: DOSBox-X
If available in your environment or package source, DOSBox-X is a better choice
for many protected-mode DOS applications.
Example:
```bash
sudo apt update
sudo apt install dosbox-x
```
If your distro does not provide `dosbox-x`, install it from your preferred
package source and then update `dosboxBin` in your profile.
## Environment Variables
- `QUOTE_OUTGOING_DIR`: Source directory to watch
- `QUOTE_PROCESSED_DIR`: Destination directory for moved files
- `QUOTE_FAILED_DIR`: Destination for failed quote files
- `QUOTE_PROCESSOR_MODE`: `move` or `dosbox` (default: `move`)
- `QUOTE_PROFILE_DIR`: Directory containing per-location DOSBox profiles
- `QUOTE_POLL_INTERVAL_SECONDS`: Poll interval (default: `2`)
### DOSBox Mode Variables
When `QUOTE_PROCESSOR_MODE=dosbox`, these variables are used:
- `DOSBOX_BIN`: DOSBox executable (default: `dosbox`)
- `DOSBOX_CONF`: Optional DOSBox config file path
- `DOSBOX_MOUNT_PATH`: Host path mounted into DOS (default: shared parent path)
- `DOSBOX_MOUNT_DRIVE`: DOS drive letter (default: `C`)
- `DOSBOX_BRIDGE_COMMAND`: DOS command/batch to run (default: `BRIDGE.BAT`)
- `DOSBOX_EXTRA_COMMANDS`: Extra DOS commands separated by `;`
- `DOSBOX_TIMEOUT_SECONDS`: Timeout for one DOSBox run (default: `120`)
- `DOSBOX_NOCONSOLE`: `true/false` to add `-noconsole` flag
## Profiles
When `QUOTE_PROCESSOR_MODE=dosbox`, the helper looks at the quote file's
`createdBy.location` field and tries to load a matching profile from:
- `quote-bridge/profiles/<LOCATION>.json`
- then `quote-bridge/profiles/default.json`
Included sample profiles:
- `quote-bridge/profiles/IOLA.json`
- `quote-bridge/profiles/KC.json`
- `quote-bridge/profiles/LINDS.json`
- `quote-bridge/profiles/default.json`
### Where To Enter The Remote Path
For each location profile, set:
- `mountPath`: the Linux path where that remote PC's shared drive/folder is mounted
Example:
```json
{
"mountPath": "/mnt/iola-cgw"
}
```
That is the main place to enter the mapped path.
### Where To Enter The DOS App Folder
For each location profile, set:
- `workingDirectory`: the DOS folder under the mounted share that contains the app
Example:
```json
{
"workingDirectory": "CGWAPP"
}
```
If the DOS app lives at:
```text
/mnt/iola-cgw/CGWAPP
```
then use:
- `mountPath = /mnt/iola-cgw`
- `workingDirectory = CGWAPP`
### Where To Enter The DOS Entry Script
For each location profile, set:
- `bridgeCommand`: the DOS-side batch/script/executable to run after mounting and changing directory
Example:
```json
{
"bridgeCommand": "BRIDGE.BAT"
}
```
## Suggested Setup For Multiple PCs
If each location runs from a different PC, the clean pattern is:
1. Mount each remote PC's application share on Ubuntu.
2. Put that mount path into the matching location profile.
3. Keep one profile per location.
4. Let the helper auto-select the profile based on the quote's current location.
Example mapping:
- `IOLA` -> `/mnt/iola-cgw`
- `KC` -> `/mnt/kc-cgw`
- `LINDS` -> `/mnt/linds-cgw`
## Mounting Remote PC Shares On Ubuntu
If the DOS app is stored on remote Windows PCs, Ubuntu needs those folders
mounted locally first.
Example mount points:
```bash
sudo mkdir -p /mnt/iola-cgw
sudo mkdir -p /mnt/kc-cgw
sudo mkdir -p /mnt/linds-cgw
```
Example CIFS mount command:
```bash
sudo mount -t cifs //REMOTE-PC/SharedFolder /mnt/iola-cgw \
-o username=YOUR_USER,password=YOUR_PASSWORD,uid=$(id -u),gid=$(id -g)
```
Replace:
- `REMOTE-PC` with the Windows machine name or IP
- `SharedFolder` with the shared folder name
- `YOUR_USER` and `YOUR_PASSWORD` with Windows credentials
After the mount is working, put that Linux mount path into the matching profile
as `mountPath`.
## Example .env-Style Setup
You can export variables in the shell before starting the listener.
Example:
```bash
export QUOTE_PROCESSOR_MODE=dosbox
export QUOTE_PROFILE_DIR=/home/jsalmon/Documents/git/quote-builder/quote-bridge/profiles
./.venv/bin/python quote-bridge/listener.py
```
If you prefer, create a small shell script such as `start-quote-bridge.sh` that
exports these values and starts the listener.
## Sample Profile Fields
Example profile:
```json
{
"profileName": "IOLA",
"dosboxBin": "dosbox-x",
"mountDrive": "C",
"mountPath": "/mnt/iola-cgw",
"workingDirectory": "CGWAPP",
"bridgeCommand": "BRIDGE.BAT",
"extraCommands": [
"SET CLIPPER=F200"
],
"timeoutSeconds": 180,
"noConsole": false
}
```
## DOS-Side BRIDGE.BAT Contract
The helper currently assumes a DOS-side entry script like:
```bat
BRIDGE.BAT "C:\OUTGOING\20260629-153012-ab12cd34.json"
```
That means `BRIDGE.BAT` should accept the quote file path as `%1`.
Minimal example:
```bat
@echo off
rem %1 is the quote JSON file path inside DOSBox
echo Processing quote file: %1
rem Start your DOS app here and pass or import the file as needed
rem Example only:
rem MYAPP.EXE %1
```
If your DOS app requires a different startup sequence, change `bridgeCommand`
in the location profile.
## End-To-End Flow
1. User clicks `Quote` in the Flask app.
2. Flask writes a quote JSON file to `shared/outgoing`.
3. Listener sees the new file.
4. Listener selects profile based on `createdBy.location` in the quote.
5. In `dosbox` mode, listener launches DOSBox using that profile.
6. DOSBox runs the configured `bridgeCommand`.
7. File moves to:
- `shared/processed` on success
- `shared/failed` on failure
## Current Behavior
- Processes `.json` files only
- Ignores temporary files such as `*.tmp`
- Appends a timestamp suffix if a destination filename already exists
- In `move` mode, files are moved from outgoing to processed
- In `dosbox` mode, each file is passed to DOSBox first, then moved to:
- `processed` on success
- `failed` on failure
Use `Ctrl+C` to stop the listener.
+183
View File
@@ -0,0 +1,183 @@
"""DOSBox processing helpers for quote bridge.
This module provides a lightweight wrapper around DOSBox invocation so the
listener can process quote files using a DOS-side script.
"""
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
from typing import Any, Callable, Dict, List
Logger = Callable[[str], None]
def _default_path(*parts: str) -> str:
base_dir = Path(__file__).resolve().parent
return str(base_dir.joinpath(*parts))
def _env_bool(name: str, default: bool = False) -> bool:
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def _split_commands(raw: str) -> List[str]:
return [cmd.strip() for cmd in raw.split(";") if cmd.strip()]
def _load_quote_payload(quote_file: Path) -> Dict[str, Any]:
with open(quote_file, 'r', encoding='utf-8') as f:
return json.load(f)
def _profile_dir() -> Path:
return Path(os.environ.get("QUOTE_PROFILE_DIR", _default_path("profiles")))
def _profile_candidates(location: str) -> List[Path]:
profile_dir = _profile_dir()
candidates: List[Path] = []
if location:
candidates.append(profile_dir / f"{location.upper()}.json")
candidates.append(profile_dir / f"{location.lower()}.json")
candidates.append(profile_dir / "default.json")
return candidates
def _load_profile_for_quote(quote_file: Path, logger: Logger) -> Dict[str, Any]:
payload = _load_quote_payload(quote_file)
location = str(payload.get('createdBy', {}).get('location', '') or '').strip()
for candidate in _profile_candidates(location):
if candidate.exists():
with open(candidate, 'r', encoding='utf-8') as f:
profile = json.load(f)
profile['_profileFile'] = str(candidate)
profile['_quoteLocation'] = location
logger(f"Using DOSBox profile: {candidate}")
return profile
logger("No DOSBox profile file found; using environment defaults")
return {
'_profileFile': None,
'_quoteLocation': location,
}
def _profile_bool(profile: Dict[str, Any], key: str, env_name: str, default: bool = False) -> bool:
value = profile.get(key)
if value is None:
return _env_bool(env_name, default=default)
if isinstance(value, bool):
return value
return str(value).strip().lower() in {"1", "true", "yes", "on"}
def _profile_string(profile: Dict[str, Any], key: str, env_name: str, default: str = "") -> str:
value = profile.get(key)
if value is None or value == "":
return os.environ.get(env_name, default)
return str(value)
def _profile_float(profile: Dict[str, Any], key: str, env_name: str, default: float) -> float:
value = profile.get(key)
if value is None or value == "":
return float(os.environ.get(env_name, str(default)))
return float(value)
def _profile_commands(profile: Dict[str, Any], key: str, env_name: str) -> List[str]:
value = profile.get(key)
if isinstance(value, list):
return [str(item).strip() for item in value if str(item).strip()]
if isinstance(value, str) and value.strip():
return _split_commands(value)
return _split_commands(os.environ.get(env_name, ""))
def _quote_path_for_dos(path: Path, mount_path: Path, drive: str) -> str:
drive_letter = (drive or "C").strip().upper()[:1]
try:
rel = path.resolve().relative_to(mount_path.resolve())
rel_str = str(rel).replace("/", "\\")
return f"{drive_letter}:\\{rel_str}"
except Exception:
# Fallback to absolute path transformed to backslashes.
return str(path.resolve()).replace("/", "\\")
def run_dosbox_for_quote(quote_file: Path, logger: Logger) -> None:
"""Run DOSBox and execute bridge command for one quote file.
Required environment values are provided with sensible defaults, but you'll
typically set these in deployment:
- DOSBOX_BIN
- DOSBOX_MOUNT_PATH
- DOSBOX_BRIDGE_COMMAND
"""
profile = _load_profile_for_quote(quote_file, logger)
dosbox_bin = _profile_string(profile, 'dosboxBin', 'DOSBOX_BIN', 'dosbox')
mount_path = Path(_profile_string(profile, 'mountPath', 'DOSBOX_MOUNT_PATH', str(quote_file.parent.parent)))
mount_drive = _profile_string(profile, 'mountDrive', 'DOSBOX_MOUNT_DRIVE', 'C')
working_directory = _profile_string(profile, 'workingDirectory', 'DOSBOX_WORKING_DIRECTORY', '')
bridge_command = _profile_string(profile, 'bridgeCommand', 'DOSBOX_BRIDGE_COMMAND', 'BRIDGE.BAT')
dosbox_conf = _profile_string(profile, 'dosboxConf', 'DOSBOX_CONF', '') or None
dosbox_timeout = _profile_float(profile, 'timeoutSeconds', 'DOSBOX_TIMEOUT_SECONDS', 120.0)
dosbox_no_console = _profile_bool(profile, 'noConsole', 'DOSBOX_NOCONSOLE', default=False)
dosbox_extra_commands = _profile_commands(profile, 'extraCommands', 'DOSBOX_EXTRA_COMMANDS')
quote_path_for_dos = _quote_path_for_dos(quote_file, mount_path, mount_drive)
# Build DOS command script.
dos_commands = [
f"mount {mount_drive} \"{mount_path}\"",
f"{mount_drive}",
]
if working_directory:
dos_commands.append(f"cd {working_directory}")
dos_commands.extend(dosbox_extra_commands)
dos_commands.append(f"{bridge_command} \"{quote_path_for_dos}\"")
dos_commands.append("exit")
cmd = [dosbox_bin]
if dosbox_conf:
cmd.extend(["-conf", dosbox_conf])
if dosbox_no_console:
cmd.append("-noconsole")
for line in dos_commands:
cmd.extend(["-c", line])
logger(f"DOSBox command: {' '.join(cmd)}")
try:
result = subprocess.run(
cmd,
check=False,
text=True,
capture_output=True,
timeout=dosbox_timeout,
)
except FileNotFoundError as exc:
raise RuntimeError(f"DOSBox executable not found: {dosbox_bin}") from exc
except subprocess.TimeoutExpired as exc:
raise RuntimeError(
f"DOSBox timed out after {dosbox_timeout} seconds"
) from exc
if result.stdout:
logger(f"DOSBox stdout:\n{result.stdout.strip()}")
if result.stderr:
logger(f"DOSBox stderr:\n{result.stderr.strip()}")
if result.returncode != 0:
raise RuntimeError(f"DOSBox exited with code {result.returncode}")
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Quote bridge listener.
Watches a source directory for quote JSON files and processes them in one of
two modes:
1) move - move straight to processed folder (default)
2) dosbox - invoke DOSBox script, then move to processed/failed
"""
from __future__ import annotations
import os
import shutil
import signal
import time
from datetime import datetime
from pathlib import Path
from dosbox_processor import run_dosbox_for_quote
def _default_path(*parts: str) -> str:
base_dir = Path(__file__).resolve().parent.parent
return str(base_dir.joinpath(*parts))
QUOTE_OUTGOING_DIR = Path(
os.environ.get("QUOTE_OUTGOING_DIR", _default_path("shared", "outgoing"))
)
QUOTE_PROCESSED_DIR = Path(
os.environ.get("QUOTE_PROCESSED_DIR", _default_path("shared", "processed"))
)
QUOTE_FAILED_DIR = Path(
os.environ.get("QUOTE_FAILED_DIR", _default_path("shared", "failed"))
)
PROCESSOR_MODE = os.environ.get("QUOTE_PROCESSOR_MODE", "move").strip().lower()
POLL_INTERVAL_SECONDS = float(os.environ.get("QUOTE_POLL_INTERVAL_SECONDS", "2"))
RUNNING = True
def _timestamp() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def log(message: str) -> None:
print(f"[{_timestamp()}] {message}", flush=True)
def should_process(path: Path) -> bool:
if not path.is_file():
return False
if path.suffix.lower() != ".json":
return False
if path.name.endswith(".tmp"):
return False
return True
def move_quote_file(path: Path, destination_dir: Path) -> Path:
target = destination_dir / path.name
# Avoid filename collisions by appending epoch milliseconds.
if target.exists():
stem = path.stem
suffix = path.suffix
target = QUOTE_PROCESSED_DIR / f"{stem}-{int(time.time() * 1000)}{suffix}"
shutil.move(str(path), str(target))
return target
def process_quote_file(path: Path) -> None:
if PROCESSOR_MODE == "move":
target = move_quote_file(path, QUOTE_PROCESSED_DIR)
log(f"Moved {path.name} -> {target.name}")
return
if PROCESSOR_MODE == "dosbox":
try:
run_dosbox_for_quote(path, log)
target = move_quote_file(path, QUOTE_PROCESSED_DIR)
log(f"DOSBox processed {path.name} -> {target.name}")
except Exception as exc:
failed_target = move_quote_file(path, QUOTE_FAILED_DIR)
log(f"DOSBox failed for {path.name}: {exc}")
log(f"Moved to failed: {failed_target.name}")
return
# Unknown mode: fail fast so operator can fix configuration.
raise RuntimeError(
f"Unsupported QUOTE_PROCESSOR_MODE '{PROCESSOR_MODE}'. Use 'move' or 'dosbox'."
)
def process_once() -> int:
moved = 0
for entry in sorted(QUOTE_OUTGOING_DIR.iterdir()):
if not should_process(entry):
continue
process_quote_file(entry)
moved += 1
return moved
def handle_shutdown(signum: int, _frame) -> None:
global RUNNING
RUNNING = False
log(f"Received signal {signum}; shutting down...")
def main() -> int:
QUOTE_OUTGOING_DIR.mkdir(parents=True, exist_ok=True)
QUOTE_PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
QUOTE_FAILED_DIR.mkdir(parents=True, exist_ok=True)
signal.signal(signal.SIGINT, handle_shutdown)
signal.signal(signal.SIGTERM, handle_shutdown)
log("Quote bridge listener started")
log(f"Processor mode: {PROCESSOR_MODE}")
log(f"Watching: {QUOTE_OUTGOING_DIR}")
log(f"Processed dir: {QUOTE_PROCESSED_DIR}")
log(f"Failed dir: {QUOTE_FAILED_DIR}")
log(f"Poll interval: {POLL_INTERVAL_SECONDS}s")
while RUNNING:
try:
moved = process_once()
if moved == 0:
time.sleep(POLL_INTERVAL_SECONDS)
except FileNotFoundError:
# If a folder disappears unexpectedly, recreate it and continue.
QUOTE_OUTGOING_DIR.mkdir(parents=True, exist_ok=True)
QUOTE_PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
QUOTE_FAILED_DIR.mkdir(parents=True, exist_ok=True)
time.sleep(POLL_INTERVAL_SECONDS)
except Exception as exc: # pragma: no cover - defensive runtime logging
log(f"Error: {exc}")
time.sleep(POLL_INTERVAL_SECONDS)
log("Quote bridge listener stopped")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+13
View File
@@ -0,0 +1,13 @@
{
"profileName": "IOLA",
"description": "Iola DOSBox profile",
"enabled": false,
"dosboxBin": "dosbox-x",
"mountDrive": "C",
"mountPath": "/mnt/iola-cgw",
"workingDirectory": "CGWAPP",
"bridgeCommand": "BRIDGE.BAT",
"extraCommands": [],
"timeoutSeconds": 180,
"noConsole": false
}
+13
View File
@@ -0,0 +1,13 @@
{
"profileName": "KC",
"description": "Kansas City DOSBox profile",
"enabled": false,
"dosboxBin": "dosbox-x",
"mountDrive": "C",
"mountPath": "/mnt/kc-cgw",
"workingDirectory": "CGWAPP",
"bridgeCommand": "BRIDGE.BAT",
"extraCommands": [],
"timeoutSeconds": 180,
"noConsole": false
}
+13
View File
@@ -0,0 +1,13 @@
{
"profileName": "LINDS",
"description": "Lindsborg DOSBox profile",
"enabled": false,
"dosboxBin": "dosbox-x",
"mountDrive": "C",
"mountPath": "/mnt/linds-cgw",
"workingDirectory": "CGWAPP",
"bridgeCommand": "BRIDGE.BAT",
"extraCommands": [],
"timeoutSeconds": 180,
"noConsole": false
}
+20
View File
@@ -0,0 +1,20 @@
{
"profileName": "DEFAULT",
"description": "Fallback DOSBox profile. Copy and customize per location.",
"enabled": false,
"dosboxBin": "dosbox-x",
"mountDrive": "C",
"mountPath": "/mnt/remote-cgw-share",
"workingDirectory": "CGWAPP",
"bridgeCommand": "BRIDGE.BAT",
"extraCommands": [
"SET CLIPPER=F200"
],
"timeoutSeconds": 180,
"noConsole": false,
"notes": {
"mountPath": "Enter the Linux path where the remote PC share is mounted.",
"workingDirectory": "Enter the DOS folder under the mounted share that contains your Harbour/Clipper app and BRIDGE.BAT.",
"bridgeCommand": "Change this if your DOS-side entry point is not BRIDGE.BAT."
}
}
+1
View File
@@ -0,0 +1 @@
# Standard library only for now