Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65d8c75bbe |
+45
-76
@@ -1,85 +1,54 @@
|
|||||||
# .NET Build Outputs
|
# ---> C
|
||||||
bin/
|
# Prerequisites
|
||||||
obj/
|
*.d
|
||||||
publish/
|
|
||||||
|
|
||||||
# Runtime Data Folders (don't commit capture files or queues)
|
# Object files
|
||||||
Captures/
|
*.o
|
||||||
Queue/
|
*.ko
|
||||||
Archive/
|
*.obj
|
||||||
Error/
|
*.elf
|
||||||
Output/
|
|
||||||
|
|
||||||
# Rust Project (reference implementation)
|
# Linker output
|
||||||
RUST/
|
*.ilk
|
||||||
|
*.map
|
||||||
|
*.exp
|
||||||
|
|
||||||
# User-specific files
|
# Precompiled Headers
|
||||||
*.suo
|
*.gch
|
||||||
*.user
|
*.pch
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# Visual Studio Code
|
# Libraries
|
||||||
.vscode/*
|
*.lib
|
||||||
!.vscode/tasks.json
|
*.a
|
||||||
!.vscode/launch.json
|
*.la
|
||||||
!.vscode/extensions.json
|
*.lo
|
||||||
|
|
||||||
# Visual Studio
|
# Shared objects (inc. Windows DLLs)
|
||||||
.vs/
|
*.dll
|
||||||
*.userprefs
|
*.so
|
||||||
|
*.so.*
|
||||||
|
*.dylib
|
||||||
|
|
||||||
# Build results
|
# Executables
|
||||||
[Dd]ebug/
|
*.exe
|
||||||
[Rr]elease/
|
*.out
|
||||||
x64/
|
*.app
|
||||||
x86/
|
*.i*86
|
||||||
[Aa][Rr][Mm]/
|
*.x86_64
|
||||||
[Aa][Rr][Mm]64/
|
*.hex
|
||||||
bld/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
[Ll]og/
|
|
||||||
[Ll]ogs/
|
|
||||||
|
|
||||||
# NuGet Packages
|
# Debug files
|
||||||
*.nupkg
|
*.dSYM/
|
||||||
*.snupkg
|
*.su
|
||||||
**/packages/*
|
*.idb
|
||||||
!**/packages/build/
|
*.pdb
|
||||||
*.nuget.props
|
|
||||||
*.nuget.targets
|
|
||||||
project.lock.json
|
|
||||||
project.fragment.lock.json
|
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# Test Results
|
# Kernel Module Compile Results
|
||||||
[Tt]est[Rr]esult*/
|
*.mod*
|
||||||
[Bb]uild[Ll]og.*
|
*.cmd
|
||||||
|
.tmp_versions/
|
||||||
|
modules.order
|
||||||
|
Module.symvers
|
||||||
|
Mkfile.old
|
||||||
|
dkms.conf
|
||||||
|
|
||||||
# Files built by Visual Studio
|
|
||||||
*.pidb
|
|
||||||
*.svclog
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# ReSharper
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
*.DotSettings.user
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
Thumbs.db
|
|
||||||
ehthumbs.db
|
|
||||||
Desktop.ini
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
.DS_Store
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Queue state file (contains runtime job queue)
|
|
||||||
queue-state.json
|
|
||||||
|
|||||||
Vendored
-502
@@ -1,502 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
// ============================================
|
|
||||||
// BUILD TASKS - Debug
|
|
||||||
// ============================================
|
|
||||||
{
|
|
||||||
"label": "build-service-debug-x86",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintService/PrintService.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Debug",
|
|
||||||
"-r",
|
|
||||||
"win-x86"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-service-debug-x64",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintService/PrintService.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Debug",
|
|
||||||
"-r",
|
|
||||||
"win-x64"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-cli-debug-x86",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Debug",
|
|
||||||
"-r",
|
|
||||||
"win-x86"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-cli-debug-x64",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Debug",
|
|
||||||
"-r",
|
|
||||||
"win-x64"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
// ============================================
|
|
||||||
// BUILD TASKS - Release
|
|
||||||
// ============================================
|
|
||||||
{
|
|
||||||
"label": "build-service-release-x86",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintService/PrintService.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x86"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-service-release-x64",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintService/PrintService.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x64"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-cli-release-x86",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x86"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-cli-release-x64",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x64"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
// ============================================
|
|
||||||
// BUILD ALL TASKS
|
|
||||||
// ============================================
|
|
||||||
{
|
|
||||||
"label": "build-all-x86",
|
|
||||||
"dependsOn": [
|
|
||||||
"build-service-release-x86",
|
|
||||||
"build-cli-release-x86",
|
|
||||||
"build-tray-release-x86"
|
|
||||||
],
|
|
||||||
"dependsOrder": "parallel",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-all-x64",
|
|
||||||
"dependsOn": [
|
|
||||||
"build-service-release-x64",
|
|
||||||
"build-cli-release-x64",
|
|
||||||
"build-tray-release-x64"
|
|
||||||
],
|
|
||||||
"dependsOrder": "parallel",
|
|
||||||
"group": "build",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-all-platforms",
|
|
||||||
"dependsOn": [
|
|
||||||
"build-all-x86",
|
|
||||||
"build-all-x64"
|
|
||||||
],
|
|
||||||
"dependsOrder": "sequence",
|
|
||||||
"group": "build",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
// ============================================
|
|
||||||
// PUBLISH TASKS (Self-Contained)
|
|
||||||
// ============================================
|
|
||||||
{
|
|
||||||
"label": "publish-service-x86-self-contained",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"publish",
|
|
||||||
"${workspaceFolder}/PrintService/PrintService.csproj",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x86",
|
|
||||||
"--self-contained",
|
|
||||||
"true",
|
|
||||||
"/p:PublishSingleFile=true",
|
|
||||||
"/p:PublishTrimmed=false",
|
|
||||||
"-o",
|
|
||||||
"${workspaceFolder}/publish/x86"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish-service-x64-self-contained",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"publish",
|
|
||||||
"${workspaceFolder}/PrintService/PrintService.csproj",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x64",
|
|
||||||
"--self-contained",
|
|
||||||
"true",
|
|
||||||
"/p:PublishSingleFile=true",
|
|
||||||
"/p:PublishTrimmed=false",
|
|
||||||
"-o",
|
|
||||||
"${workspaceFolder}/publish/x64"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish-cli-x86-self-contained",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"publish",
|
|
||||||
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x86",
|
|
||||||
"--self-contained",
|
|
||||||
"true",
|
|
||||||
"/p:PublishSingleFile=true",
|
|
||||||
"/p:PublishTrimmed=false",
|
|
||||||
"-o",
|
|
||||||
"${workspaceFolder}/publish/x86"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish-cli-x64-self-contained",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"publish",
|
|
||||||
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x64",
|
|
||||||
"--self-contained",
|
|
||||||
"true",
|
|
||||||
"/p:PublishSingleFile=true",
|
|
||||||
"/p:PublishTrimmed=false",
|
|
||||||
"-o",
|
|
||||||
"${workspaceFolder}/publish/x64"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish-all-x86",
|
|
||||||
"dependsOn": [
|
|
||||||
"publish-service-x86-self-contained",
|
|
||||||
"publish-cli-x86-self-contained",
|
|
||||||
"publish-tray-x86-self-contained"
|
|
||||||
],
|
|
||||||
"dependsOrder": "parallel",
|
|
||||||
"group": "build",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish-all-x64",
|
|
||||||
"dependsOn": [
|
|
||||||
"publish-service-x64-self-contained",
|
|
||||||
"publish-cli-x64-self-contained",
|
|
||||||
"publish-tray-x64-self-contained"
|
|
||||||
],
|
|
||||||
"dependsOrder": "parallel",
|
|
||||||
"group": "build",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
// ============================================
|
|
||||||
// RUN/TEST TASKS
|
|
||||||
// ============================================
|
|
||||||
{
|
|
||||||
"label": "run-service",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"--project",
|
|
||||||
"${workspaceFolder}/PrintService/PrintService.csproj"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "run-cli",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"--project",
|
|
||||||
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
|
|
||||||
"--",
|
|
||||||
// Add CLI arguments after --
|
|
||||||
// Example: "-f", "${workspaceFolder}/Captures/test.txt", "-t", "invoice"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "test-pdf-output",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"--project",
|
|
||||||
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
|
|
||||||
"--",
|
|
||||||
"-f",
|
|
||||||
"${workspaceFolder}/Captures/~DARLENE~20260514@072912.TXT",
|
|
||||||
"-t",
|
|
||||||
"test",
|
|
||||||
"--skip-service-check"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": {
|
|
||||||
"kind": "test",
|
|
||||||
"isDefault": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// ============================================
|
|
||||||
// WATCH TASKS (Auto-rebuild on changes)
|
|
||||||
// ============================================
|
|
||||||
{
|
|
||||||
"label": "watch-service",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"watch",
|
|
||||||
"run",
|
|
||||||
"--project",
|
|
||||||
"${workspaceFolder}/PrintService/PrintService.csproj"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
// ============================================
|
|
||||||
// TRAY APP TASKS
|
|
||||||
// ============================================
|
|
||||||
{
|
|
||||||
"label": "build-tray-debug",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Debug"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-tray-release-x86",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x86"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build-tray-release-x64",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x64"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "run-tray",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"--project",
|
|
||||||
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish-tray-x86-self-contained",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"publish",
|
|
||||||
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x86",
|
|
||||||
"--self-contained",
|
|
||||||
"true",
|
|
||||||
"/p:PublishSingleFile=true",
|
|
||||||
"/p:PublishTrimmed=false",
|
|
||||||
"-o",
|
|
||||||
"${workspaceFolder}/publish/x86"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish-tray-x64-self-contained",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"publish",
|
|
||||||
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
|
|
||||||
"-c",
|
|
||||||
"Release",
|
|
||||||
"-r",
|
|
||||||
"win-x64",
|
|
||||||
"--self-contained",
|
|
||||||
"true",
|
|
||||||
"/p:PublishSingleFile=true",
|
|
||||||
"/p:PublishTrimmed=false",
|
|
||||||
"-o",
|
|
||||||
"${workspaceFolder}/publish/x64"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
// ============================================
|
|
||||||
// CLEAN TASKS
|
|
||||||
// ============================================
|
|
||||||
{
|
|
||||||
"label": "clean",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"clean",
|
|
||||||
"${workspaceFolder}/PrintService.sln"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile",
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "clean-publish",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "Remove-Item",
|
|
||||||
"args": [
|
|
||||||
"-Path",
|
|
||||||
"${workspaceFolder}/publish",
|
|
||||||
"-Recurse",
|
|
||||||
"-Force",
|
|
||||||
"-ErrorAction",
|
|
||||||
"SilentlyContinue"
|
|
||||||
],
|
|
||||||
"windows": {
|
|
||||||
"options": {
|
|
||||||
"shell": {
|
|
||||||
"executable": "powershell.exe"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
# Harbor/Clipper Print Service - Implementation Plan
|
|
||||||
|
|
||||||
**TL;DR**: Build a persistent Windows service that monitors for new print capture files, immediately moves them to prevent overwrites, queues them for ordered processing, and sends multi-tray print jobs to laser printers with document-type-specific configurations.
|
|
||||||
|
|
||||||
**Recommended Approach**: C#/.NET 8+ Windows Service with FileSystemWatcher, print queue, and configurable document templates. C# is completely free (.NET SDK), has superior Windows printer APIs, and excellent service infrastructure.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
### Phase 1: Service Foundation (Parallel work possible after step 1)
|
|
||||||
|
|
||||||
1. **Create Windows Service project structure** using .NET 8+ Worker Service template
|
|
||||||
|
|
||||||
- Use `BackgroundService` base class for hosting
|
|
||||||
- Configure as Windows Service with `Microsoft.Extensions.Hosting.WindowsServices`
|
|
||||||
- Set up dependency injection, logging, and configuration (appsettings.json)
|
|
||||||
2. **Implement file monitoring system** (*parallel with step 3*)
|
|
||||||
|
|
||||||
- `FileSystemWatcher` on Captures folder with `Created`, `Changed`, and `Renamed` events
|
|
||||||
- Debounce logic to handle rapid file creation (buffer 100-200ms)
|
|
||||||
- Immediate file move to processing queue folder with GUID-based naming to prevent collisions
|
|
||||||
- Include original filename, timestamp, and metadata in queue
|
|
||||||
3. **Design configuration system** (*parallel with step 2*)
|
|
||||||
|
|
||||||
- Document type definitions (invoice, order, delivery, etc.)
|
|
||||||
- Per-document-type settings: printer name, tray sequence, content transformations
|
|
||||||
- Global settings: queue folder paths, archive settings, retry policies
|
|
||||||
- JSON schema for easy editing
|
|
||||||
|
|
||||||
### Phase 2: Command Interface & Queue Management
|
|
||||||
|
|
||||||
4. **Create CLI command handler** (*depends on 1*)
|
|
||||||
|
|
||||||
- Parse `-f <filepath> -t <doctype> -o <ordernumber>` arguments
|
|
||||||
- Check if service is running via named pipe or TCP
|
|
||||||
- If running: send command to service via IPC
|
|
||||||
- If not running: log error or auto-start service
|
|
||||||
- Return exit code immediately
|
|
||||||
5. **Build print job queue** (*depends on 1*)
|
|
||||||
|
|
||||||
- Thread-safe queue with priority/ordering (FIFO by default, configurable)
|
|
||||||
- Persistent queue state (survive service restart)
|
|
||||||
- Job metadata: source file, document type, order number, timestamp, retry count
|
|
||||||
- Status tracking: pending, processing, completed, failed
|
|
||||||
6. **Implement IPC mechanism** (*depends on 4, 5*)
|
|
||||||
|
|
||||||
- Named pipe server listening for commands from CLI
|
|
||||||
- Deserialize command → create job → enqueue
|
|
||||||
- Return acknowledgment to caller
|
|
||||||
|
|
||||||
### Phase 3: Printer Integration
|
|
||||||
|
|
||||||
7. **Implement queue-based printer control** (*depends on 5*)
|
|
||||||
|
|
||||||
- Use `System.Drawing.Printing.PrintDocument` with Windows print queue
|
|
||||||
- Multi-page document with dynamic `PageSettings.PaperSource` for tray selection
|
|
||||||
- Per-printer tray mapping configuration (map logical tray numbers to physical `PaperSource` indexes)
|
|
||||||
- Fallback handling when tray empty (configurable: fail job, use default tray, alert)
|
|
||||||
- Optional: Print to PDF for testing/archiving
|
|
||||||
8. **Build document processor with Graphics rendering** (*depends on 7*)
|
|
||||||
|
|
||||||
- Read queued capture file and parse text content
|
|
||||||
- Apply document-type-specific transformations:
|
|
||||||
- Line position adjustments using `Graphics.DrawString()` coordinates
|
|
||||||
- Text removal/replacement via regex patterns
|
|
||||||
- Font and formatting configuration per document type
|
|
||||||
- Future: header/footer injection, graphics overlay, logo placement
|
|
||||||
- Render each page with `Graphics` API for pixel-perfect control
|
|
||||||
- Generate multi-page `PrintDocument` with correct tray sequence
|
|
||||||
- Send to Windows print queue
|
|
||||||
9. **Add printer routing logic** (*depends on 7, 8*)
|
|
||||||
|
|
||||||
- Map document types to printer(s) and tray sequences
|
|
||||||
- Support multiple printer profiles per document type
|
|
||||||
- Configuration: `{ "invoice": { "printer": "LaserJet-1", "trays": [3,4,1,2] } }`
|
|
||||||
|
|
||||||
### Phase 4: Reliability & Operations
|
|
||||||
|
|
||||||
10. **Implement error handling & retry** (*depends on 5, 8*)
|
|
||||||
|
|
||||||
- Printer offline detection → requeue with exponential backoff
|
|
||||||
- File read errors → log and move to error folder
|
|
||||||
- Max retry limit → move to dead letter queue
|
|
||||||
- Detailed logging with correlation IDs
|
|
||||||
11. **Add file lifecycle management** (*depends on 2*)
|
|
||||||
|
|
||||||
- Post-processing: delete or move to archive folder (configurable per document type)
|
|
||||||
- Archive folder structure: organized by date, document type, or both
|
|
||||||
- Retention policy support (future: auto-cleanup old archives)
|
|
||||||
12. **Create service installer & deployment** (*depends on all previous*)
|
|
||||||
|
|
||||||
- MSI installer or PowerShell install script
|
|
||||||
- Service registration with `sc.exe` or WiX toolset
|
|
||||||
- Auto-start configuration
|
|
||||||
- Uninstall/upgrade support
|
|
||||||
|
|
||||||
### Phase 5: Testing & Validation
|
|
||||||
|
|
||||||
13. **System testing** (*depends on 12*)
|
|
||||||
|
|
||||||
- Test rapid file creation (simulate Harbor/Clipper speed)
|
|
||||||
- Verify no overwrites or missing files
|
|
||||||
- Confirm correct print order
|
|
||||||
- Test printer offline scenarios
|
|
||||||
- Validate tray sequences for each document type
|
|
||||||
14. **Integration with Harbor/Clipper** (*depends on 13*)
|
|
||||||
|
|
||||||
- Replace current Rust CLI calls with new CLI
|
|
||||||
- Monitor production for 24-48 hours
|
|
||||||
- Compare output quality and reliability
|
|
||||||
- Performance tuning if needed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Relevant Files
|
|
||||||
|
|
||||||
**To be created**:
|
|
||||||
|
|
||||||
- `PrintService/Program.cs` — Service host and startup configuration
|
|
||||||
- `PrintService/Worker.cs` — Background service implementing FileSystemWatcher and queue processor
|
|
||||||
- `PrintService/Models/PrintJob.cs` — Job data model
|
|
||||||
- `PrintService/Models/DocumentConfig.cs` — Document type configuration schema
|
|
||||||
- `PrintService/Services/FileMonitorService.cs` — File watching and immediate move logic
|
|
||||||
- `PrintService/Services/PrintQueueService.cs` — Queue management and processing
|
|
||||||
- `PrintService/Services/PrinterService.cs` — Queue-based printer control with tray commands
|
|
||||||
- `PrintService/Services/DocumentProcessor.cs` — Text parsing and Graphics rendering
|
|
||||||
- `PrintService/Services/IpcService.cs` — Named pipe listener for CLI commands
|
|
||||||
- `PrintService/appsettings.json` — Configuration file for document types, printers, paths
|
|
||||||
- `PrintServiceCLI/Program.cs` — Command-line tool for sending commands to service
|
|
||||||
- `PrintService.sln` — Solution file
|
|
||||||
|
|
||||||
**Existing**:
|
|
||||||
|
|
||||||
- `Captures/` — Source folder for Harbor/Clipper output files (will be monitored)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
1. **Unit tests**: Queue ordering, configuration parsing, tray sequence generation
|
|
||||||
2. **Integration test**: Create 50 files in 10 seconds → verify all printed in order with correct trays
|
|
||||||
3. **Stress test**: 1000+ files → no overwrites, no missing files, proper ordering
|
|
||||||
4. **Manual test**: Print each document type to each configured printer and verify tray usage
|
|
||||||
5. **Production validation**: Monitor service for 48 hours, compare output count with Harbor/Clipper expected count
|
|
||||||
6. **Error scenario testing**: Unplug printer mid-job → verify retry → reconnect → verify job completes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
**Language**: C#/.NET 8+
|
|
||||||
|
|
||||||
- **Rationale**: Free, best Windows service/printer APIs, excellent tooling, easier printer control than Rust
|
|
||||||
- **Alternative**: Could still use Rust with `windows-rs` crate, but printer control is more complex
|
|
||||||
|
|
||||||
**Architecture**: Service with IPC (Named Pipes)
|
|
||||||
|
|
||||||
- CLI tool sends commands → service processes asynchronously
|
|
||||||
- Decouples Harbor/Clipper from processing delays
|
|
||||||
|
|
||||||
**File handling**: Immediate move with GUID renaming
|
|
||||||
|
|
||||||
- **Critical**: Prevents timestamp collision overwrites
|
|
||||||
- Original metadata preserved in queue state
|
|
||||||
|
|
||||||
**Queue persistence**: JSON file or SQLite
|
|
||||||
|
|
||||||
- Survives service restart
|
|
||||||
- Allows inspection/recovery
|
|
||||||
|
|
||||||
**Configuration**: JSON file (appsettings.json)
|
|
||||||
|
|
||||||
- Editable without recompilation
|
|
||||||
- Per-document-type printer/tray/transformation settings
|
|
||||||
|
|
||||||
**Printer control**: PrintDocument with Windows print queue
|
|
||||||
|
|
||||||
- Per-page tray control via `PageSettings.PaperSource`
|
|
||||||
- Content rendered using `Graphics.DrawString()` for positioning control
|
|
||||||
- Jobs appear in Windows print queue (pausable, survives restarts)
|
|
||||||
- Can print to PDF for testing
|
|
||||||
|
|
||||||
**Scope included**:
|
|
||||||
|
|
||||||
- Windows service with file monitoring
|
|
||||||
- Command-line interface for Harbor/Clipper integration
|
|
||||||
- Multi-tray printer control via Windows print queue
|
|
||||||
- Configurable document routing
|
|
||||||
- Archive/delete options
|
|
||||||
- Error handling and retry logic
|
|
||||||
- Content transformation with Graphics rendering (line positioning, text removal)
|
|
||||||
|
|
||||||
**Scope excluded** (future enhancements):
|
|
||||||
|
|
||||||
- GUI management console (use config file + logs for now)
|
|
||||||
- Advanced document parsing/templating (start with regex transformations)
|
|
||||||
- Web API or remote monitoring
|
|
||||||
- Database storage (use file-based queue initially)
|
|
||||||
- Automatic printer discovery
|
|
||||||
- Print preview or validation UI
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Further Considerations
|
|
||||||
|
|
||||||
1. **Printer-specific tray mapping**: Need to identify your laser printer models and test `PaperSource` indexes. Do you know the printer makes/models?
|
|
||||||
2. **Content transformation complexity**: You mentioned looping line items. For Phase 1, suggest simple regex-based transformations. If parsing becomes complex, consider a template engine (Scriban, Handlebars.NET) in Phase 2.
|
|
||||||
3. **Testing environment**: Do you have a test laser printer or can we print to PDF initially to verify tray selection and content rendering work correctly?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Benefits of Queue-Based Approach
|
|
||||||
|
|
||||||
- ✅ **Visibility**: Jobs appear in Windows print queue UI
|
|
||||||
- ✅ **Resilience**: Queue survives service restarts
|
|
||||||
- ✅ **Control**: Can pause/cancel jobs through Windows
|
|
||||||
- ✅ **Testing**: Print to PDF for verification without paper waste
|
|
||||||
- ✅ **Content Control**: `Graphics.DrawString()` gives pixel-perfect positioning for your line adjustments
|
|
||||||
- ✅ **Debugging**: Easier to troubleshoot than raw printer commands
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.0.31903.59
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintService", "PrintService\PrintService.csproj", "{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintServiceCLI", "PrintServiceCLI\PrintServiceCLI.csproj", "{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintServiceTray", "PrintServiceTray\PrintServiceTray.csproj", "{42CF28FF-90E4-4912-B427-EA5F675F641E}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{42CF28FF-90E4-4912-B427-EA5F675F641E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{42CF28FF-90E4-4912-B427-EA5F675F641E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{42CF28FF-90E4-4912-B427-EA5F675F641E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{42CF28FF-90E4-4912-B427-EA5F675F641E}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
namespace PrintService.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Application settings loaded from appsettings.json
|
|
||||||
/// </summary>
|
|
||||||
public class AppSettings
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Path to monitor for incoming capture files
|
|
||||||
/// </summary>
|
|
||||||
public string CapturesPath { get; set; } = "Captures";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Path for queued files (after moving from Captures)
|
|
||||||
/// </summary>
|
|
||||||
public string QueuePath { get; set; } = "Queue";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Path for failed jobs
|
|
||||||
/// </summary>
|
|
||||||
public string ErrorPath { get; set; } = "Errors";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default archive path
|
|
||||||
/// </summary>
|
|
||||||
public string ArchivePath { get; set; } = "Archive";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum retry attempts before moving to error folder
|
|
||||||
/// </summary>
|
|
||||||
public int MaxRetryAttempts { get; set; } = 3;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Debounce delay in milliseconds for file system watcher
|
|
||||||
/// </summary>
|
|
||||||
public int DebounceDelayMs { get; set; } = 200;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Named pipe name for IPC communication
|
|
||||||
/// </summary>
|
|
||||||
public string PipeName { get; set; } = "PrintServicePipe";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Document type configurations
|
|
||||||
/// </summary>
|
|
||||||
public List<DocumentConfig> DocumentTypes { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Queue state file path
|
|
||||||
/// </summary>
|
|
||||||
public string QueueStateFile { get; set; } = "queue_state.json";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enable detailed logging
|
|
||||||
/// </summary>
|
|
||||||
public bool VerboseLogging { get; set; } = false;
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
namespace PrintService.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for document types
|
|
||||||
/// </summary>
|
|
||||||
public class DocumentConfig
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Name/identifier of the document type
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Printer name to send to
|
|
||||||
/// </summary>
|
|
||||||
public string PrinterName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sequence of tray numbers to use (1-based, e.g., [3, 4, 1, 2])
|
|
||||||
/// These will be mapped to physical PaperSource indexes
|
|
||||||
/// </summary>
|
|
||||||
public int[] TraySequence { get; set; } = Array.Empty<int>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Font name to use for rendering
|
|
||||||
/// </summary>
|
|
||||||
public string FontName { get; set; } = "Courier New";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Font size in points
|
|
||||||
/// </summary>
|
|
||||||
public float FontSize { get; set; } = 10f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Vertical offset adjustment (in pixels)
|
|
||||||
/// </summary>
|
|
||||||
public int VerticalOffset { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Horizontal offset adjustment (in pixels)
|
|
||||||
/// </summary>
|
|
||||||
public int HorizontalOffset { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Text transformation rules (regex patterns to remove/replace)
|
|
||||||
/// </summary>
|
|
||||||
public List<TextTransform> Transformations { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to archive files after printing (true) or delete them (false)
|
|
||||||
/// </summary>
|
|
||||||
public bool ArchiveAfterPrint { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Archive folder path (if ArchiveAfterPrint is true)
|
|
||||||
/// </summary>
|
|
||||||
public string? ArchivePath { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Output folder for PDF files (when using "Microsoft Print to PDF")
|
|
||||||
/// If specified, PDFs will be saved to this folder automatically
|
|
||||||
/// </summary>
|
|
||||||
public string? OutputPath { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Skip post-processing (archive/delete) to leave files in queue for repeated testing
|
|
||||||
/// Useful for development and testing scenarios
|
|
||||||
/// </summary>
|
|
||||||
public bool SkipPostProcessing { get; set; } = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Text transformation rule
|
|
||||||
/// </summary>
|
|
||||||
public class TextTransform
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Regex pattern to match
|
|
||||||
/// </summary>
|
|
||||||
public string Pattern { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Replacement text (empty string to remove)
|
|
||||||
/// </summary>
|
|
||||||
public string Replacement { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Description of what this transformation does
|
|
||||||
/// </summary>
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
namespace PrintService.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a print job in the queue
|
|
||||||
/// </summary>
|
|
||||||
public class PrintJob
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Unique identifier for the job
|
|
||||||
/// </summary>
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Path to the original capture file
|
|
||||||
/// </summary>
|
|
||||||
public string SourceFilePath { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Path to the queued file (after being moved with GUID name)
|
|
||||||
/// </summary>
|
|
||||||
public string QueuedFilePath { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Document type (invoice, order, delivery, etc.)
|
|
||||||
/// </summary>
|
|
||||||
public string DocumentType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional order number
|
|
||||||
/// </summary>
|
|
||||||
public string? OrderNumber { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When the job was created
|
|
||||||
/// </summary>
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When the job was last updated
|
|
||||||
/// </summary>
|
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.Now;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current status of the job
|
|
||||||
/// </summary>
|
|
||||||
public PrintJobStatus Status { get; set; } = PrintJobStatus.Pending;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of times this job has been attempted
|
|
||||||
/// </summary>
|
|
||||||
public int RetryCount { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Last error message if failed
|
|
||||||
/// </summary>
|
|
||||||
public string? LastError { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Original filename before moving to queue
|
|
||||||
/// </summary>
|
|
||||||
public string OriginalFilename { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Status of a print job
|
|
||||||
/// </summary>
|
|
||||||
public enum PrintJobStatus
|
|
||||||
{
|
|
||||||
Pending,
|
|
||||||
Processing,
|
|
||||||
Completed,
|
|
||||||
Failed,
|
|
||||||
RetryScheduled
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
namespace PrintService.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Root configuration loaded from printer-config.json
|
|
||||||
/// </summary>
|
|
||||||
public class PrinterConfiguration
|
|
||||||
{
|
|
||||||
public Dictionary<string, DocumentTypeConfig> DocumentTypes { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for a single document type (replaces DocumentConfig for printer/tray mapping)
|
|
||||||
/// </summary>
|
|
||||||
public class DocumentTypeConfig
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public List<PageConfig> Pages { get; set; } = new();
|
|
||||||
|
|
||||||
// Rendering settings
|
|
||||||
public string FontName { get; set; } = "Courier New";
|
|
||||||
public float FontSize { get; set; } = 10f;
|
|
||||||
public int HorizontalOffset { get; set; } = 0;
|
|
||||||
public int VerticalOffset { get; set; } = 0;
|
|
||||||
|
|
||||||
// Post-processing settings
|
|
||||||
public bool ArchiveAfterPrint { get; set; } = true;
|
|
||||||
public string? ArchivePath { get; set; }
|
|
||||||
public string? OutputPath { get; set; }
|
|
||||||
public bool SkipPostProcessing { get; set; } = false;
|
|
||||||
|
|
||||||
// Text transformation rules
|
|
||||||
public List<TextTransform> Transformations { get; set; } = new();
|
|
||||||
|
|
||||||
// Rust transformation properties
|
|
||||||
/// <summary>
|
|
||||||
/// Left margin/indent for all lines (in 1/100 inch units)
|
|
||||||
/// </summary>
|
|
||||||
public int LinePadding { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove both "Order #: " label and the order number entirely
|
|
||||||
/// </summary>
|
|
||||||
public bool RemoveOrderNumber { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove "Order #: " label but keep the order number (and make it bold)
|
|
||||||
/// </summary>
|
|
||||||
public bool RemoveOrderNumberLabel { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add spacing before the order number (shifts it right)
|
|
||||||
/// </summary>
|
|
||||||
public string IndentOrderNumber { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Vertical position adjustments per line (line number → adjustment in 1/100 inch)
|
|
||||||
/// Negative values move up, positive values move down
|
|
||||||
/// </summary>
|
|
||||||
public Dictionary<int, int> RowShift { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Horizontal character trimming per line (line number → trim configuration)
|
|
||||||
/// Removes characters from Start to End indices
|
|
||||||
/// </summary>
|
|
||||||
public Dictionary<int, RowTrimConfig> RowTrim { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for a single page (printer and tray assignment)
|
|
||||||
/// </summary>
|
|
||||||
public class PageConfig
|
|
||||||
{
|
|
||||||
public int PageNumber { get; set; }
|
|
||||||
public string PrinterName { get; set; } = string.Empty;
|
|
||||||
public int TrayNumber { get; set; }
|
|
||||||
public string? TrayLabel { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for row trimming (horizontal character removal)
|
|
||||||
/// </summary>
|
|
||||||
public class RowTrimConfig
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Start index of characters to remove (0-based)
|
|
||||||
/// </summary>
|
|
||||||
public int Start { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// End index of characters to remove (exclusive)
|
|
||||||
/// </summary>
|
|
||||||
public int End { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<UserSecretsId>dotnet-PrintService-3e06b535-3a68-4806-9c5b-3a32bde82bf9</UserSecretsId>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Use x86 only for Release builds (for 32-bit target machines) -->
|
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
|
||||||
<PlatformTarget>x86</PlatformTarget>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.0" />
|
|
||||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using PrintService;
|
|
||||||
using PrintService.Models;
|
|
||||||
using PrintService.Services;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
IHost host = Host.CreateDefaultBuilder(args)
|
|
||||||
.UseWindowsService(options =>
|
|
||||||
{
|
|
||||||
options.ServiceName = "LAAPC Print Service";
|
|
||||||
})
|
|
||||||
.ConfigureServices((hostContext, services) =>
|
|
||||||
{
|
|
||||||
// Bind configuration
|
|
||||||
services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));
|
|
||||||
|
|
||||||
// Register services
|
|
||||||
services.AddSingleton<ConfigurationService>();
|
|
||||||
services.AddSingleton<PrintQueueService>();
|
|
||||||
services.AddSingleton<FileMonitorService>();
|
|
||||||
services.AddSingleton<ContentTransformer>();
|
|
||||||
services.AddSingleton<PrinterService>();
|
|
||||||
services.AddSingleton<DocumentProcessor>();
|
|
||||||
services.AddSingleton<IpcService>();
|
|
||||||
|
|
||||||
// Register the main worker
|
|
||||||
services.AddHostedService<Worker>();
|
|
||||||
})
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Ensure directories exist
|
|
||||||
var config = host.Services.GetRequiredService<IOptions<AppSettings>>().Value;
|
|
||||||
Directory.CreateDirectory(config.QueuePath);
|
|
||||||
Directory.CreateDirectory(config.ErrorPath);
|
|
||||||
Directory.CreateDirectory(config.ArchivePath);
|
|
||||||
|
|
||||||
host.Run();
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"PrintService": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"DOTNET_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using PrintService.Models;
|
|
||||||
|
|
||||||
namespace PrintService.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads printer-config.json (global and local), merging local over global.
|
|
||||||
/// Mirrors the same logic used in PrintServiceTray.
|
|
||||||
/// </summary>
|
|
||||||
public class ConfigurationService
|
|
||||||
{
|
|
||||||
private readonly string _globalConfigPath;
|
|
||||||
private readonly string _localConfigPath;
|
|
||||||
private readonly ILogger<ConfigurationService> _logger;
|
|
||||||
|
|
||||||
public ConfigurationService(ILogger<ConfigurationService> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
_globalConfigPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
|
||||||
"LAAPC", "printer-config.json");
|
|
||||||
|
|
||||||
_localConfigPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"LAAPC", "printer-config.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load merged configuration. Returns an empty config (not null) if no files exist.
|
|
||||||
/// </summary>
|
|
||||||
public PrinterConfiguration Load()
|
|
||||||
{
|
|
||||||
var config = new PrinterConfiguration();
|
|
||||||
|
|
||||||
if (File.Exists(_globalConfigPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(_globalConfigPath);
|
|
||||||
var global = JsonSerializer.Deserialize<PrinterConfiguration>(json);
|
|
||||||
if (global != null)
|
|
||||||
config = global;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to read global printer config from {Path}", _globalConfigPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(_localConfigPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(_localConfigPath);
|
|
||||||
var local = JsonSerializer.Deserialize<PrinterConfiguration>(json);
|
|
||||||
if (local != null)
|
|
||||||
{
|
|
||||||
// Local overrides global per document type
|
|
||||||
foreach (var kvp in local.DocumentTypes)
|
|
||||||
config.DocumentTypes[kvp.Key] = kvp.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to read local printer config from {Path}", _localConfigPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.DocumentTypes.Count == 0)
|
|
||||||
_logger.LogWarning("No document types found in printer-config.json. Checked: {Global} and {Local}",
|
|
||||||
_globalConfigPath, _localConfigPath);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a single document type config by name (case-insensitive).
|
|
||||||
/// </summary>
|
|
||||||
public DocumentTypeConfig? GetDocumentType(string documentType)
|
|
||||||
{
|
|
||||||
var config = Load();
|
|
||||||
return config.DocumentTypes
|
|
||||||
.FirstOrDefault(kvp => kvp.Key.Equals(documentType, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
using PrintService.Models;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace PrintService.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles content transformations from Rust CLI (cgwprint) logic
|
|
||||||
/// </summary>
|
|
||||||
public class ContentTransformer
|
|
||||||
{
|
|
||||||
private readonly ILogger<ContentTransformer> _logger;
|
|
||||||
|
|
||||||
public ContentTransformer(ILogger<ContentTransformer> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transform document content according to configuration
|
|
||||||
/// </summary>
|
|
||||||
public TransformedDocument TransformContent(string content, DocumentTypeConfig config, string? orderNumber = null)
|
|
||||||
{
|
|
||||||
// Step 1: Normalize font codes
|
|
||||||
content = NormalizeFontCodes(content);
|
|
||||||
|
|
||||||
// Step 2: Order number manipulation (if order number provided)
|
|
||||||
if (!string.IsNullOrEmpty(orderNumber))
|
|
||||||
{
|
|
||||||
content = ManipulateOrderNumber(content, orderNumber, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Parse font codes to identify bold/normal segments
|
|
||||||
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
|
|
||||||
|
|
||||||
// Step 4: Apply row trim (modifies line content)
|
|
||||||
lines = ApplyRowTrim(lines, config.RowTrim);
|
|
||||||
|
|
||||||
// Step 5: Parse lines into segments with font styling
|
|
||||||
var styledLines = ParseStyledLines(lines);
|
|
||||||
|
|
||||||
return new TransformedDocument
|
|
||||||
{
|
|
||||||
Lines = styledLines,
|
|
||||||
RowShifts = config.RowShift,
|
|
||||||
LinePadding = config.LinePadding
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Step 1: Normalize font codes (pre-processing)
|
|
||||||
/// </summary>
|
|
||||||
private string NormalizeFontCodes(string content)
|
|
||||||
{
|
|
||||||
// Prefix with normal font at start
|
|
||||||
content = "\x1Bw0" + content;
|
|
||||||
|
|
||||||
// Normalize uppercase W to lowercase w
|
|
||||||
content = content.Replace("\x1BW1", "\x1Bw1");
|
|
||||||
content = content.Replace("\x1BW0", "\x1Bw0");
|
|
||||||
|
|
||||||
// Deduplicate consecutive bold sequences
|
|
||||||
content = Regex.Replace(content, @"(\x1Bw1)+", "\x1Bw1");
|
|
||||||
|
|
||||||
// Deduplicate consecutive normal sequences
|
|
||||||
content = Regex.Replace(content, @"(\x1Bw0)+", "\x1Bw0");
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Step 2: Order number manipulation
|
|
||||||
/// </summary>
|
|
||||||
private string ManipulateOrderNumber(string content, string orderNumber, DocumentTypeConfig config)
|
|
||||||
{
|
|
||||||
var orderPattern = $"Order #: {orderNumber}";
|
|
||||||
|
|
||||||
// Only ONE operation executes based on configuration flags
|
|
||||||
if (config.RemoveOrderNumber)
|
|
||||||
{
|
|
||||||
// Replace entire "Order #: 12345" with spaces
|
|
||||||
var replacement = new string(' ', orderPattern.Length);
|
|
||||||
content = content.Replace(orderPattern, replacement);
|
|
||||||
_logger.LogDebug("Removed order number completely: {OrderNumber}", orderNumber);
|
|
||||||
}
|
|
||||||
else if (config.RemoveOrderNumberLabel)
|
|
||||||
{
|
|
||||||
// Replace "Order #: " with spaces, keep number and make it bold
|
|
||||||
var labelLength = "Order #: ".Length;
|
|
||||||
var spaces = new string(' ', labelLength);
|
|
||||||
var replacement = $"{spaces}\x1Bw1{orderNumber}\x1Bw0";
|
|
||||||
content = content.Replace(orderPattern, replacement);
|
|
||||||
_logger.LogDebug("Removed order number label, kept bold number: {OrderNumber}", orderNumber);
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(config.IndentOrderNumber))
|
|
||||||
{
|
|
||||||
// Find and indent the order number (assumes it's already bold)
|
|
||||||
var boldPattern = $@"\x1Bw1{Regex.Escape(orderNumber)}\x1Bw0";
|
|
||||||
content = Regex.Replace(content, boldPattern, $"{config.IndentOrderNumber}\x1Bw1{orderNumber}\x1Bw0");
|
|
||||||
_logger.LogDebug("Indented order number: {OrderNumber}", orderNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Step 3: Apply row trim (horizontal character removal)
|
|
||||||
/// </summary>
|
|
||||||
private string[] ApplyRowTrim(string[] lines, Dictionary<int, RowTrimConfig> rowTrim)
|
|
||||||
{
|
|
||||||
if (rowTrim.Count == 0)
|
|
||||||
return lines;
|
|
||||||
|
|
||||||
var result = new string[lines.Length];
|
|
||||||
|
|
||||||
for (int i = 0; i < lines.Length; i++)
|
|
||||||
{
|
|
||||||
if (rowTrim.TryGetValue(i, out var trim))
|
|
||||||
{
|
|
||||||
var line = lines[i];
|
|
||||||
|
|
||||||
if (trim.Start >= 0 && trim.End <= line.Length && trim.Start < trim.End)
|
|
||||||
{
|
|
||||||
// Remove characters from Start to End
|
|
||||||
result[i] = line.Substring(0, trim.Start) + line.Substring(trim.End);
|
|
||||||
_logger.LogDebug("Trimmed line {LineNum}: removed chars {Start}-{End}", i, trim.Start, trim.End);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result[i] = line;
|
|
||||||
_logger.LogWarning("Invalid row trim config for line {LineNum}: Start={Start}, End={End}, LineLength={Length}",
|
|
||||||
i, trim.Start, trim.End, line.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result[i] = lines[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Step 4: Parse lines into segments with font styling
|
|
||||||
/// </summary>
|
|
||||||
private List<StyledLine> ParseStyledLines(string[] lines)
|
|
||||||
{
|
|
||||||
var styledLines = new List<StyledLine>();
|
|
||||||
|
|
||||||
foreach (var line in lines)
|
|
||||||
{
|
|
||||||
var segments = new List<TextSegment>();
|
|
||||||
var currentText = new StringBuilder();
|
|
||||||
var currentStyle = FontStyle.Regular;
|
|
||||||
|
|
||||||
for (int i = 0; i < line.Length; i++)
|
|
||||||
{
|
|
||||||
// Check for font code escape sequence
|
|
||||||
if (i + 2 < line.Length && line[i] == '\x1B' && line[i + 1] == 'w')
|
|
||||||
{
|
|
||||||
// Save current segment if any
|
|
||||||
if (currentText.Length > 0)
|
|
||||||
{
|
|
||||||
segments.Add(new TextSegment
|
|
||||||
{
|
|
||||||
Text = currentText.ToString(),
|
|
||||||
Style = currentStyle
|
|
||||||
});
|
|
||||||
currentText.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse font code
|
|
||||||
char code = line[i + 2];
|
|
||||||
if (code == '1')
|
|
||||||
{
|
|
||||||
currentStyle = FontStyle.Bold;
|
|
||||||
}
|
|
||||||
else if (code == '0')
|
|
||||||
{
|
|
||||||
currentStyle = FontStyle.Regular;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip the escape sequence
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
currentText.Append(line[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add final segment
|
|
||||||
if (currentText.Length > 0 || segments.Count == 0)
|
|
||||||
{
|
|
||||||
segments.Add(new TextSegment
|
|
||||||
{
|
|
||||||
Text = currentText.ToString(),
|
|
||||||
Style = currentStyle
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
styledLines.Add(new StyledLine { Segments = segments });
|
|
||||||
}
|
|
||||||
|
|
||||||
return styledLines;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transformed document with styled lines and layout adjustments
|
|
||||||
/// </summary>
|
|
||||||
public class TransformedDocument
|
|
||||||
{
|
|
||||||
public List<StyledLine> Lines { get; set; } = new();
|
|
||||||
public Dictionary<int, int> RowShifts { get; set; } = new();
|
|
||||||
public int LinePadding { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A line with styled text segments
|
|
||||||
/// </summary>
|
|
||||||
public class StyledLine
|
|
||||||
{
|
|
||||||
public List<TextSegment> Segments { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A text segment with font styling
|
|
||||||
/// </summary>
|
|
||||||
public class TextSegment
|
|
||||||
{
|
|
||||||
public string Text { get; set; } = string.Empty;
|
|
||||||
public FontStyle Style { get; set; } = FontStyle.Regular;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum FontStyle
|
|
||||||
{
|
|
||||||
Regular,
|
|
||||||
Bold
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
using Microsoft.Extensions.Options;
|
|
||||||
using PrintService.Models;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Drawing.Printing;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace PrintService.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes documents and renders them for printing
|
|
||||||
/// </summary>
|
|
||||||
public class DocumentProcessor
|
|
||||||
{
|
|
||||||
private readonly AppSettings _settings;
|
|
||||||
private readonly ConfigurationService _configService;
|
|
||||||
private readonly ILogger<DocumentProcessor> _logger;
|
|
||||||
|
|
||||||
public DocumentProcessor(
|
|
||||||
IOptions<AppSettings> settings,
|
|
||||||
ConfigurationService configService,
|
|
||||||
ILogger<DocumentProcessor> logger)
|
|
||||||
{
|
|
||||||
_settings = settings.Value;
|
|
||||||
_configService = configService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process and transform document content
|
|
||||||
/// </summary>
|
|
||||||
public string ProcessDocument(string content, DocumentTypeConfig config)
|
|
||||||
{
|
|
||||||
var processed = content;
|
|
||||||
|
|
||||||
// Apply text transformations
|
|
||||||
foreach (var transform in config.Transformations)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
processed = Regex.Replace(processed, transform.Pattern, transform.Replacement);
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Applied transformation: {Description}", transform.Description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to apply transformation: {Pattern}", transform.Pattern);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return processed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get document configuration by type from printer-config.json
|
|
||||||
/// </summary>
|
|
||||||
public DocumentTypeConfig? GetDocumentConfig(string documentType)
|
|
||||||
{
|
|
||||||
return _configService.GetDocumentType(documentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Read file content
|
|
||||||
/// </summary>
|
|
||||||
public string ReadFile(string filePath)
|
|
||||||
{
|
|
||||||
return File.ReadAllText(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Archive or delete file after processing
|
|
||||||
/// </summary>
|
|
||||||
public void PostProcess(PrintJob job, DocumentTypeConfig config)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Skip post-processing if configured (useful for testing)
|
|
||||||
if (config.SkipPostProcessing)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Skipping post-processing for job {JobId} (file remains in queue)", job.Id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.ArchiveAfterPrint && !string.IsNullOrEmpty(config.ArchivePath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(config.ArchivePath);
|
|
||||||
|
|
||||||
var archiveFileName = Path.Combine(config.ArchivePath,
|
|
||||||
$"{DateTime.Now:yyyyMMdd_HHmmss}_{job.OriginalFilename}");
|
|
||||||
|
|
||||||
if (File.Exists(job.QueuedFilePath))
|
|
||||||
{
|
|
||||||
File.Move(job.QueuedFilePath, archiveFileName);
|
|
||||||
_logger.LogInformation("Archived job {JobId} to {Path}", job.Id, archiveFileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (File.Exists(job.QueuedFilePath))
|
|
||||||
{
|
|
||||||
File.Delete(job.QueuedFilePath);
|
|
||||||
_logger.LogInformation("Deleted file for job {JobId}", job.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to post-process job {JobId}", job.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
using Microsoft.Extensions.Options;
|
|
||||||
using PrintService.Models;
|
|
||||||
|
|
||||||
namespace PrintService.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Monitors the Captures folder for new files and moves them to queue
|
|
||||||
/// </summary>
|
|
||||||
public class FileMonitorService : IDisposable
|
|
||||||
{
|
|
||||||
private readonly AppSettings _settings;
|
|
||||||
private readonly PrintQueueService _queueService;
|
|
||||||
private readonly ILogger<FileMonitorService> _logger;
|
|
||||||
private FileSystemWatcher? _watcher;
|
|
||||||
private readonly Dictionary<string, DateTime> _pendingFiles = new();
|
|
||||||
private readonly Timer _debounceTimer;
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
public FileMonitorService(
|
|
||||||
IOptions<AppSettings> settings,
|
|
||||||
PrintQueueService queueService,
|
|
||||||
ILogger<FileMonitorService> logger)
|
|
||||||
{
|
|
||||||
_settings = settings.Value;
|
|
||||||
_queueService = queueService;
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
// Timer for debouncing file system events
|
|
||||||
_debounceTimer = new Timer(ProcessPendingFiles, null,
|
|
||||||
TimeSpan.FromMilliseconds(_settings.DebounceDelayMs),
|
|
||||||
TimeSpan.FromMilliseconds(_settings.DebounceDelayMs));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Start monitoring the Captures folder
|
|
||||||
/// </summary>
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(_settings.CapturesPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_settings.CapturesPath);
|
|
||||||
_logger.LogInformation("Created captures directory: {Path}", _settings.CapturesPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
_watcher = new FileSystemWatcher(_settings.CapturesPath)
|
|
||||||
{
|
|
||||||
Filter = "*.TXT",
|
|
||||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime,
|
|
||||||
EnableRaisingEvents = true
|
|
||||||
};
|
|
||||||
|
|
||||||
_watcher.Created += OnFileCreated;
|
|
||||||
_watcher.Changed += OnFileChanged;
|
|
||||||
|
|
||||||
_logger.LogInformation("Started monitoring: {Path}", _settings.CapturesPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stop monitoring
|
|
||||||
/// </summary>
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
if (_watcher != null)
|
|
||||||
{
|
|
||||||
_watcher.EnableRaisingEvents = false;
|
|
||||||
_watcher.Dispose();
|
|
||||||
_watcher = null;
|
|
||||||
}
|
|
||||||
_logger.LogInformation("Stopped monitoring");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnFileCreated(object sender, FileSystemEventArgs e)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
_pendingFiles[e.FullPath] = DateTime.Now;
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("File created: {Path}", e.FullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnFileChanged(object sender, FileSystemEventArgs e)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
_pendingFiles[e.FullPath] = DateTime.Now;
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("File changed: {Path}", e.FullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process files after debounce delay
|
|
||||||
/// </summary>
|
|
||||||
private void ProcessPendingFiles(object? state)
|
|
||||||
{
|
|
||||||
List<string> filesToProcess;
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
var cutoff = DateTime.Now.AddMilliseconds(-_settings.DebounceDelayMs);
|
|
||||||
filesToProcess = _pendingFiles
|
|
||||||
.Where(kvp => kvp.Value <= cutoff)
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var file in filesToProcess)
|
|
||||||
{
|
|
||||||
_pendingFiles.Remove(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var filePath in filesToProcess)
|
|
||||||
{
|
|
||||||
ProcessFile(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process a single file: move to queue with GUID name
|
|
||||||
/// </summary>
|
|
||||||
private void ProcessFile(string filePath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(filePath))
|
|
||||||
{
|
|
||||||
return; // File may have been moved or deleted
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileName = Path.GetFileName(filePath);
|
|
||||||
var jobId = Guid.NewGuid();
|
|
||||||
var queuedFileName = $"{jobId}.txt";
|
|
||||||
var queuedPath = Path.Combine(_settings.QueuePath, queuedFileName);
|
|
||||||
|
|
||||||
// Move file to queue with GUID name to prevent collisions
|
|
||||||
File.Move(filePath, queuedPath);
|
|
||||||
|
|
||||||
_logger.LogInformation("Moved {FileName} to queue as {QueuedFile}", fileName, queuedFileName);
|
|
||||||
|
|
||||||
// Note: Job will be created when CLI sends command with document type
|
|
||||||
// For now, we just have the file safely in the queue folder
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to process file: {Path}", filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manually process a file (called from IPC when CLI provides metadata)
|
|
||||||
/// </summary>
|
|
||||||
public PrintJob CreateJobFromFile(string filePath, string documentType, string? orderNumber)
|
|
||||||
{
|
|
||||||
var fileName = Path.GetFileName(filePath);
|
|
||||||
var jobId = Guid.NewGuid();
|
|
||||||
var queuedFileName = $"{jobId}.txt";
|
|
||||||
var queuedPath = Path.Combine(_settings.QueuePath, queuedFileName);
|
|
||||||
|
|
||||||
// Check if this document type has SkipPostProcessing enabled
|
|
||||||
var config = _settings.DocumentTypes.FirstOrDefault(dt =>
|
|
||||||
dt.Name.Equals(documentType, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (config?.SkipPostProcessing == true)
|
|
||||||
{
|
|
||||||
// Copy instead of move for testing scenarios
|
|
||||||
File.Copy(filePath, queuedPath, overwrite: true);
|
|
||||||
_logger.LogInformation("Copied {FileName} to queue (test mode - source file preserved)", fileName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Move file to queue (normal operation)
|
|
||||||
File.Move(filePath, queuedPath, overwrite: true);
|
|
||||||
_logger.LogInformation("Moved {FileName} to queue", fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var job = new PrintJob
|
|
||||||
{
|
|
||||||
Id = jobId,
|
|
||||||
SourceFilePath = filePath,
|
|
||||||
QueuedFilePath = queuedPath,
|
|
||||||
DocumentType = documentType,
|
|
||||||
OrderNumber = orderNumber,
|
|
||||||
OriginalFilename = fileName,
|
|
||||||
Status = PrintJobStatus.Pending,
|
|
||||||
CreatedAt = DateTime.Now,
|
|
||||||
UpdatedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogInformation("Created job {JobId} for {FileName} (type: {DocType})",
|
|
||||||
job.Id, fileName, documentType);
|
|
||||||
|
|
||||||
return job;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_debounceTimer?.Dispose();
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
using Microsoft.Extensions.Options;
|
|
||||||
using PrintService.Models;
|
|
||||||
using System.IO.Pipes;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace PrintService.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Named pipe server for IPC communication with CLI
|
|
||||||
/// </summary>
|
|
||||||
public class IpcService : IDisposable
|
|
||||||
{
|
|
||||||
private readonly AppSettings _settings;
|
|
||||||
private readonly PrintQueueService _queueService;
|
|
||||||
private readonly FileMonitorService _fileMonitorService;
|
|
||||||
private readonly ILogger<IpcService> _logger;
|
|
||||||
private CancellationTokenSource? _cancellationTokenSource;
|
|
||||||
private Task? _listenerTask;
|
|
||||||
|
|
||||||
public IpcService(
|
|
||||||
IOptions<AppSettings> settings,
|
|
||||||
PrintQueueService queueService,
|
|
||||||
FileMonitorService fileMonitorService,
|
|
||||||
ILogger<IpcService> logger)
|
|
||||||
{
|
|
||||||
_settings = settings.Value;
|
|
||||||
_queueService = queueService;
|
|
||||||
_fileMonitorService = fileMonitorService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Start listening for IPC commands
|
|
||||||
/// </summary>
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
_cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
_listenerTask = Task.Run(() => ListenForConnections(_cancellationTokenSource.Token));
|
|
||||||
_logger.LogInformation("IPC service started on pipe: {PipeName}", _settings.PipeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stop listening
|
|
||||||
/// </summary>
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
_cancellationTokenSource?.Cancel();
|
|
||||||
_listenerTask?.Wait(TimeSpan.FromSeconds(5));
|
|
||||||
_logger.LogInformation("IPC service stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Listen for incoming connections
|
|
||||||
/// </summary>
|
|
||||||
private async Task ListenForConnections(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
NamedPipeServerStream? pipeServer = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
pipeServer = new NamedPipeServerStream(
|
|
||||||
_settings.PipeName,
|
|
||||||
PipeDirection.InOut,
|
|
||||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
|
||||||
PipeTransmissionMode.Message,
|
|
||||||
PipeOptions.Asynchronous);
|
|
||||||
|
|
||||||
await pipeServer.WaitForConnectionAsync(cancellationToken);
|
|
||||||
|
|
||||||
// Handle client in separate task, passing ownership of pipeServer
|
|
||||||
var serverToHandle = pipeServer;
|
|
||||||
pipeServer = null; // Clear reference so we don't dispose it in catch
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
using (serverToHandle)
|
|
||||||
{
|
|
||||||
await HandleClient(serverToHandle);
|
|
||||||
}
|
|
||||||
}, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
pipeServer?.Dispose();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in IPC listener");
|
|
||||||
pipeServer?.Dispose();
|
|
||||||
await Task.Delay(1000, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle a client connection
|
|
||||||
/// </summary>
|
|
||||||
private async Task HandleClient(NamedPipeServerStream pipeServer)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!pipeServer.IsConnected)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read command
|
|
||||||
var buffer = new byte[4096];
|
|
||||||
var bytesRead = await pipeServer.ReadAsync(buffer, 0, buffer.Length);
|
|
||||||
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
return; // Client disconnected
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
|
||||||
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Received IPC message: {Message}", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to determine command type
|
|
||||||
var commandType = DetermineCommandType(message);
|
|
||||||
|
|
||||||
if (commandType == "GetQueueStatus")
|
|
||||||
{
|
|
||||||
var status = GetQueueStatus();
|
|
||||||
await SendQueueStatusResponse(pipeServer, status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle print command
|
|
||||||
var command = JsonSerializer.Deserialize<PrintCommand>(message);
|
|
||||||
if (command == null)
|
|
||||||
{
|
|
||||||
await SendResponse(pipeServer, false, "Invalid command format");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process command
|
|
||||||
var result = ProcessCommand(command);
|
|
||||||
await SendResponse(pipeServer, result.Success, result.Message);
|
|
||||||
}
|
|
||||||
catch (ObjectDisposedException)
|
|
||||||
{
|
|
||||||
// Client disconnected, this is normal
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Client disconnected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
// Pipe communication error
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Pipe communication error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error handling IPC client");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (pipeServer.IsConnected)
|
|
||||||
{
|
|
||||||
await SendResponse(pipeServer, false, $"Error: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determine the type of command from JSON message
|
|
||||||
/// </summary>
|
|
||||||
private string? DetermineCommandType(string message)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var doc = JsonDocument.Parse(message);
|
|
||||||
if (doc.RootElement.TryGetProperty("Command", out var cmdProp))
|
|
||||||
{
|
|
||||||
return cmdProp.GetString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get current queue status
|
|
||||||
/// </summary>
|
|
||||||
private QueueStatusResponse GetQueueStatus()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var jobs = _queueService.GetAllJobs();
|
|
||||||
var jobStatuses = jobs.Select(j => new JobStatus
|
|
||||||
{
|
|
||||||
Id = j.Id.ToString(),
|
|
||||||
FileName = j.OriginalFilename,
|
|
||||||
DocumentType = j.DocumentType,
|
|
||||||
Status = j.Status.ToString(),
|
|
||||||
PrinterName = null, // Will be set when processing starts
|
|
||||||
CurrentPage = null,
|
|
||||||
TotalPages = null
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
return new QueueStatusResponse
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Jobs = jobStatuses
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to get queue status");
|
|
||||||
return new QueueStatusResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Error: {ex.Message}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send queue status response
|
|
||||||
/// </summary>
|
|
||||||
private async Task SendQueueStatusResponse(NamedPipeServerStream pipeServer, QueueStatusResponse response)
|
|
||||||
{
|
|
||||||
var json = JsonSerializer.Serialize(response);
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(json);
|
|
||||||
await pipeServer.WriteAsync(bytes, 0, bytes.Length);
|
|
||||||
await pipeServer.FlushAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process a print command
|
|
||||||
/// </summary>
|
|
||||||
private (bool Success, string Message) ProcessCommand(PrintCommand command)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(command.FilePath) || !File.Exists(command.FilePath))
|
|
||||||
{
|
|
||||||
return (false, $"File not found: {command.FilePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(command.DocumentType))
|
|
||||||
{
|
|
||||||
return (false, "Document type is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create job and enqueue
|
|
||||||
var job = _fileMonitorService.CreateJobFromFile(
|
|
||||||
command.FilePath,
|
|
||||||
command.DocumentType,
|
|
||||||
command.OrderNumber);
|
|
||||||
|
|
||||||
_queueService.Enqueue(job);
|
|
||||||
|
|
||||||
return (true, $"Job {job.Id} queued successfully");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to process command");
|
|
||||||
return (false, ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send response back to client
|
|
||||||
/// </summary>
|
|
||||||
private async Task SendResponse(NamedPipeServerStream pipeServer, bool success, string message)
|
|
||||||
{
|
|
||||||
var response = new PrintResponse
|
|
||||||
{
|
|
||||||
Success = success,
|
|
||||||
Message = message
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(response);
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(json);
|
|
||||||
await pipeServer.WriteAsync(bytes, 0, bytes.Length);
|
|
||||||
await pipeServer.FlushAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
_cancellationTokenSource?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command sent from CLI to service
|
|
||||||
/// </summary>
|
|
||||||
public class PrintCommand
|
|
||||||
{
|
|
||||||
public string FilePath { get; set; } = string.Empty;
|
|
||||||
public string DocumentType { get; set; } = string.Empty;
|
|
||||||
public string? OrderNumber { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Response from service to CLI
|
|
||||||
/// </summary>
|
|
||||||
public class PrintResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Queue status response for tray app
|
|
||||||
/// </summary>
|
|
||||||
public class QueueStatusResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string? Message { get; set; }
|
|
||||||
public List<JobStatus> Jobs { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Status of a single job
|
|
||||||
/// </summary>
|
|
||||||
public class JobStatus
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string FileName { get; set; } = string.Empty;
|
|
||||||
public string DocumentType { get; set; } = string.Empty;
|
|
||||||
public string Status { get; set; } = string.Empty;
|
|
||||||
public string? PrinterName { get; set; }
|
|
||||||
public int? CurrentPage { get; set; }
|
|
||||||
public int? TotalPages { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
using Microsoft.Extensions.Options;
|
|
||||||
using PrintService.Models;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace PrintService.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages the print job queue with persistence
|
|
||||||
/// </summary>
|
|
||||||
public class PrintQueueService
|
|
||||||
{
|
|
||||||
private readonly ConcurrentQueue<PrintJob> _queue = new();
|
|
||||||
private readonly AppSettings _settings;
|
|
||||||
private readonly ILogger<PrintQueueService> _logger;
|
|
||||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
|
||||||
|
|
||||||
public PrintQueueService(IOptions<AppSettings> settings, ILogger<PrintQueueService> logger)
|
|
||||||
{
|
|
||||||
_settings = settings.Value;
|
|
||||||
_logger = logger;
|
|
||||||
LoadQueueState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enqueue a new print job
|
|
||||||
/// </summary>
|
|
||||||
public void Enqueue(PrintJob job)
|
|
||||||
{
|
|
||||||
_queue.Enqueue(job);
|
|
||||||
_logger.LogInformation("Job {JobId} enqueued for document type: {DocType}", job.Id, job.DocumentType);
|
|
||||||
SaveQueueState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to dequeue the next pending job
|
|
||||||
/// </summary>
|
|
||||||
public bool TryDequeue(out PrintJob? job)
|
|
||||||
{
|
|
||||||
bool result = _queue.TryDequeue(out job);
|
|
||||||
if (result && job != null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Job {JobId} dequeued", job.Id);
|
|
||||||
SaveQueueState();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Peek at the next job without removing it
|
|
||||||
/// </summary>
|
|
||||||
public bool TryPeek(out PrintJob? job)
|
|
||||||
{
|
|
||||||
return _queue.TryPeek(out job);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get count of jobs in queue
|
|
||||||
/// </summary>
|
|
||||||
public int Count => _queue.Count;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all jobs in the queue (snapshot)
|
|
||||||
/// </summary>
|
|
||||||
public PrintJob[] GetAllJobs()
|
|
||||||
{
|
|
||||||
return _queue.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save queue state to disk
|
|
||||||
/// </summary>
|
|
||||||
private void SaveQueueState()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_semaphore.Wait();
|
|
||||||
var jobs = _queue.ToArray();
|
|
||||||
var json = JsonSerializer.Serialize(jobs, new JsonSerializerOptions { WriteIndented = true });
|
|
||||||
File.WriteAllText(_settings.QueueStateFile, json);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to save queue state");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_semaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load queue state from disk
|
|
||||||
/// </summary>
|
|
||||||
private void LoadQueueState()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(_settings.QueueStateFile))
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(_settings.QueueStateFile);
|
|
||||||
var jobs = JsonSerializer.Deserialize<PrintJob[]>(json);
|
|
||||||
|
|
||||||
if (jobs != null)
|
|
||||||
{
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
// Only reload pending or retry-scheduled jobs
|
|
||||||
if (job.Status == PrintJobStatus.Pending || job.Status == PrintJobStatus.RetryScheduled)
|
|
||||||
{
|
|
||||||
_queue.Enqueue(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_logger.LogInformation("Loaded {Count} jobs from queue state", _queue.Count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to load queue state");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Requeue a job (for retry)
|
|
||||||
/// </summary>
|
|
||||||
public void Requeue(PrintJob job)
|
|
||||||
{
|
|
||||||
job.RetryCount++;
|
|
||||||
job.Status = PrintJobStatus.RetryScheduled;
|
|
||||||
job.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
if (job.RetryCount >= _settings.MaxRetryAttempts)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Job {JobId} exceeded max retries, moving to error folder", job.Id);
|
|
||||||
MoveToErrorFolder(job);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Requeueing job {JobId}, attempt {Retry}", job.Id, job.RetryCount);
|
|
||||||
_queue.Enqueue(job);
|
|
||||||
SaveQueueState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move a failed job to error folder
|
|
||||||
/// </summary>
|
|
||||||
private void MoveToErrorFolder(PrintJob job)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var errorFileName = Path.Combine(_settings.ErrorPath,
|
|
||||||
$"{Path.GetFileNameWithoutExtension(job.OriginalFilename)}_{job.Id}.txt");
|
|
||||||
|
|
||||||
if (File.Exists(job.QueuedFilePath))
|
|
||||||
{
|
|
||||||
File.Move(job.QueuedFilePath, errorFileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save error details
|
|
||||||
var errorInfo = Path.Combine(_settings.ErrorPath,
|
|
||||||
$"{Path.GetFileNameWithoutExtension(job.OriginalFilename)}_{job.Id}_error.json");
|
|
||||||
var json = JsonSerializer.Serialize(job, new JsonSerializerOptions { WriteIndented = true });
|
|
||||||
File.WriteAllText(errorInfo, json);
|
|
||||||
|
|
||||||
job.Status = PrintJobStatus.Failed;
|
|
||||||
SaveQueueState();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to move job {JobId} to error folder", job.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
using Microsoft.Extensions.Options;
|
|
||||||
using PrintService.Models;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Drawing.Printing;
|
|
||||||
|
|
||||||
namespace PrintService.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles printing to Windows print queue with tray control
|
|
||||||
/// </summary>
|
|
||||||
public class PrinterService
|
|
||||||
{
|
|
||||||
private readonly AppSettings _settings;
|
|
||||||
private readonly ILogger<PrinterService> _logger;
|
|
||||||
private readonly ContentTransformer _transformer;
|
|
||||||
|
|
||||||
public PrinterService(
|
|
||||||
IOptions<AppSettings> settings,
|
|
||||||
ILogger<PrinterService> logger,
|
|
||||||
ContentTransformer transformer)
|
|
||||||
{
|
|
||||||
_settings = settings.Value;
|
|
||||||
_logger = logger;
|
|
||||||
_transformer = transformer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Print document using the new DocumentTypeConfig (printer-config.json).
|
|
||||||
/// Each PageConfig entry defines one output copy per original content page,
|
|
||||||
/// so a 4-page document with 4 PageConfig entries produces 16 output pages.
|
|
||||||
/// </summary>
|
|
||||||
public void Print(string content, DocumentTypeConfig config, string originalFileName, string? orderNumber = null)
|
|
||||||
{
|
|
||||||
if (config.Pages.Count == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("No pages/trays defined for document type");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform content according to Rust CLI logic
|
|
||||||
var transformed = _transformer.TransformContent(content, config, orderNumber);
|
|
||||||
|
|
||||||
var linesPerPage = CalculateLinesPerPage(config.FontSize);
|
|
||||||
var totalOriginalPages = Math.Max(1, (int)Math.Ceiling(transformed.Lines.Count / (double)linesPerPage));
|
|
||||||
var originalPageIndex = 0;
|
|
||||||
var copyIndex = 0; // index into config.Pages (one entry per tray copy)
|
|
||||||
|
|
||||||
// All pages within one document type share the same printer in typical use,
|
|
||||||
// but PageConfig supports per-page overrides — use the first page's printer for
|
|
||||||
// the PrintDocument; tray (PaperSource) is set per page in the event handler.
|
|
||||||
var primaryPrinterName = config.Pages[0].PrinterName;
|
|
||||||
|
|
||||||
var printDoc = new PrintDocument
|
|
||||||
{
|
|
||||||
PrinterSettings = {
|
|
||||||
PrinterName = primaryPrinterName,
|
|
||||||
Duplex = Duplex.Simplex // Force single-sided printing (no duplexing)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle PDF output if using Microsoft Print to PDF
|
|
||||||
if (primaryPrinterName.Equals("Microsoft Print to PDF", StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
!string.IsNullOrEmpty(config.OutputPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(config.OutputPath);
|
|
||||||
var pdfFileName = Path.GetFileNameWithoutExtension(originalFileName) + ".pdf";
|
|
||||||
var pdfPath = Path.Combine(config.OutputPath, pdfFileName);
|
|
||||||
|
|
||||||
printDoc.PrinterSettings.PrintToFile = true;
|
|
||||||
printDoc.PrinterSettings.PrintFileName = pdfPath;
|
|
||||||
|
|
||||||
_logger.LogInformation("PDF will be saved to: {Path}", pdfPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify primary printer exists
|
|
||||||
if (!PrinterSettings.InstalledPrinters.Cast<string>().Contains(primaryPrinterName))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Printer '{primaryPrinterName}' not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryPageSettings fires BEFORE PrintPage - set tray here
|
|
||||||
printDoc.QueryPageSettings += (sender, e) =>
|
|
||||||
{
|
|
||||||
if (e.PageSettings == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var pageConfig = config.Pages[copyIndex];
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var paperSource = GetPaperSource(printDoc.PrinterSettings, pageConfig.TrayNumber);
|
|
||||||
if (paperSource != null)
|
|
||||||
{
|
|
||||||
e.PageSettings.PaperSource = paperSource;
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(
|
|
||||||
"Output page {OutputPage}: original page {OriginalPage}, tray {Tray} ({Label}) ({Source})",
|
|
||||||
(originalPageIndex * config.Pages.Count) + copyIndex + 1,
|
|
||||||
originalPageIndex + 1,
|
|
||||||
pageConfig.TrayNumber,
|
|
||||||
pageConfig.TrayLabel ?? "?",
|
|
||||||
paperSource.SourceName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to set tray {Tray}, using default", pageConfig.TrayNumber);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
printDoc.PrintPage += (sender, e) =>
|
|
||||||
{
|
|
||||||
if (e.Graphics == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Render the same original page for every copy before moving to next original page.
|
|
||||||
RenderPage(e.Graphics, transformed, originalPageIndex * linesPerPage, linesPerPage,
|
|
||||||
config.FontName, config.FontSize, config.HorizontalOffset, config.VerticalOffset);
|
|
||||||
|
|
||||||
copyIndex++;
|
|
||||||
if (copyIndex >= config.Pages.Count)
|
|
||||||
{
|
|
||||||
copyIndex = 0;
|
|
||||||
originalPageIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.HasMorePages = originalPageIndex < totalOriginalPages;
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Printing to {Printer}: {OriginalPages} original page(s) × {Copies} copies = {Total} output pages",
|
|
||||||
primaryPrinterName,
|
|
||||||
totalOriginalPages,
|
|
||||||
config.Pages.Count,
|
|
||||||
totalOriginalPages * config.Pages.Count);
|
|
||||||
|
|
||||||
printDoc.Print();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Render page content using Graphics API with styled text and transformations
|
|
||||||
/// </summary>
|
|
||||||
private void RenderPage(Graphics graphics, TransformedDocument transformed, int startLine, int linesPerPage,
|
|
||||||
string fontName, float fontSize, int horizontalOffset, int verticalOffset)
|
|
||||||
{
|
|
||||||
var normalFont = new Font(fontName, fontSize, System.Drawing.FontStyle.Regular);
|
|
||||||
var boldFont = new Font(fontName, fontSize, System.Drawing.FontStyle.Bold);
|
|
||||||
var brush = Brushes.Black;
|
|
||||||
var lineHeight = normalFont.GetHeight(graphics);
|
|
||||||
|
|
||||||
// Convert line padding from 1/100 inch to pixels (assuming 96 DPI for screen, but printers use their own DPI)
|
|
||||||
// For printers, Graphics.DpiX will give the actual printer DPI
|
|
||||||
var linePaddingPixels = (transformed.LinePadding / 100f) * graphics.DpiX;
|
|
||||||
|
|
||||||
var endLine = Math.Min(startLine + linesPerPage, transformed.Lines.Count);
|
|
||||||
|
|
||||||
// Track cumulative vertical shift
|
|
||||||
float cumulativeShift = 0;
|
|
||||||
|
|
||||||
for (int lineIndex = startLine; lineIndex < endLine; lineIndex++)
|
|
||||||
{
|
|
||||||
if (lineIndex >= transformed.Lines.Count)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var styledLine = transformed.Lines[lineIndex];
|
|
||||||
|
|
||||||
// Apply row shift if defined for this line
|
|
||||||
if (transformed.RowShifts.TryGetValue(lineIndex, out var shift))
|
|
||||||
{
|
|
||||||
// Convert shift from 1/100 inch to pixels
|
|
||||||
var shiftPixels = (shift / 100f) * graphics.DpiY;
|
|
||||||
cumulativeShift += shiftPixels;
|
|
||||||
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Line {LineNum}: applying row shift {Shift} units ({Pixels} px)",
|
|
||||||
lineIndex, shift, shiftPixels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate Y position with offsets and shifts
|
|
||||||
var y = verticalOffset + ((lineIndex - startLine) * lineHeight) + cumulativeShift;
|
|
||||||
|
|
||||||
// Start X position with horizontal offset and line padding
|
|
||||||
var x = (float)horizontalOffset + linePaddingPixels;
|
|
||||||
|
|
||||||
// Render each text segment with appropriate font style
|
|
||||||
foreach (var segment in styledLine.Segments)
|
|
||||||
{
|
|
||||||
var font = segment.Style == Services.FontStyle.Bold ? boldFont : normalFont;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(segment.Text))
|
|
||||||
{
|
|
||||||
graphics.DrawString(segment.Text, font, brush, x, y);
|
|
||||||
|
|
||||||
// Measure text width to advance X position for next segment
|
|
||||||
var textSize = graphics.MeasureString(segment.Text, font);
|
|
||||||
x += textSize.Width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
normalFont.Dispose();
|
|
||||||
boldFont.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculate lines per page based on font size
|
|
||||||
/// </summary>
|
|
||||||
private int CalculateLinesPerPage(float fontSize)
|
|
||||||
{
|
|
||||||
// Estimate: standard letter size is 11 inches, at 10pt font ~66 lines
|
|
||||||
var estimatedLineHeight = fontSize * 1.2f; // points
|
|
||||||
var pageHeightInPoints = 11 * 72; // 11 inches * 72 points per inch
|
|
||||||
return (int)Math.Floor(pageHeightInPoints / estimatedLineHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get PaperSource by logical tray number
|
|
||||||
/// </summary>
|
|
||||||
private PaperSource? GetPaperSource(PrinterSettings printerSettings, int trayNumber)
|
|
||||||
{
|
|
||||||
// Try to find by RawKind (tray number)
|
|
||||||
foreach (PaperSource source in printerSettings.PaperSources)
|
|
||||||
{
|
|
||||||
// Some printers use RawKind directly as tray number
|
|
||||||
if (source.RawKind == trayNumber ||
|
|
||||||
source.RawKind == (trayNumber + 256)) // Some drivers offset by 256
|
|
||||||
{
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: use by index if available
|
|
||||||
if (trayNumber > 0 && trayNumber <= printerSettings.PaperSources.Count)
|
|
||||||
{
|
|
||||||
return printerSettings.PaperSources[trayNumber - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning("Could not map tray {Tray} to PaperSource", trayNumber);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List available paper sources for a printer (for debugging/configuration)
|
|
||||||
/// </summary>
|
|
||||||
public void ListPaperSources(string printerName)
|
|
||||||
{
|
|
||||||
var printDoc = new PrintDocument { PrinterSettings = { PrinterName = printerName } };
|
|
||||||
|
|
||||||
_logger.LogInformation("Available paper sources for {Printer}:", printerName);
|
|
||||||
foreach (PaperSource source in printDoc.PrinterSettings.PaperSources)
|
|
||||||
{
|
|
||||||
_logger.LogInformation(" - {Name} (RawKind: {Kind})", source.SourceName, source.RawKind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
using PrintService.Models;
|
|
||||||
using PrintService.Services;
|
|
||||||
|
|
||||||
namespace PrintService;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Main worker service that coordinates all components
|
|
||||||
/// </summary>
|
|
||||||
public class Worker : BackgroundService
|
|
||||||
{
|
|
||||||
private readonly ILogger<Worker> _logger;
|
|
||||||
private readonly PrintQueueService _queueService;
|
|
||||||
private readonly FileMonitorService _fileMonitorService;
|
|
||||||
private readonly PrinterService _printerService;
|
|
||||||
private readonly DocumentProcessor _documentProcessor;
|
|
||||||
private readonly IpcService _ipcService;
|
|
||||||
|
|
||||||
public Worker(
|
|
||||||
ILogger<Worker> logger,
|
|
||||||
PrintQueueService queueService,
|
|
||||||
FileMonitorService fileMonitorService,
|
|
||||||
PrinterService printerService,
|
|
||||||
DocumentProcessor documentProcessor,
|
|
||||||
IpcService ipcService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_queueService = queueService;
|
|
||||||
_fileMonitorService = fileMonitorService;
|
|
||||||
_printerService = printerService;
|
|
||||||
_documentProcessor = documentProcessor;
|
|
||||||
_ipcService = ipcService;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("LAAPC Print Service starting at: {time}", DateTimeOffset.Now);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Start IPC server for CLI communication
|
|
||||||
_ipcService.Start();
|
|
||||||
|
|
||||||
// Start file monitoring (optional - mainly using IPC for job creation)
|
|
||||||
// _fileMonitorService.Start();
|
|
||||||
|
|
||||||
_logger.LogInformation("Print service started successfully");
|
|
||||||
_logger.LogInformation("Queue has {Count} pending jobs", _queueService.Count);
|
|
||||||
|
|
||||||
// Main processing loop
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_queueService.TryDequeue(out var job) && job != null)
|
|
||||||
{
|
|
||||||
await ProcessJob(job, stoppingToken);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No jobs in queue, wait a bit
|
|
||||||
await Task.Delay(500, stoppingToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Expected during shutdown, exit gracefully
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in processing loop");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(1000, stoppingToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Shutdown requested during error delay
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Expected during shutdown
|
|
||||||
_logger.LogInformation("Service shutdown requested");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogCritical(ex, "Fatal error in worker service");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Print service stopping");
|
|
||||||
_ipcService.Stop();
|
|
||||||
_fileMonitorService.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process a single print job
|
|
||||||
/// </summary>
|
|
||||||
private async Task ProcessJob(PrintJob job, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Processing job {JobId} (Type: {DocType}, Order: {OrderNum})",
|
|
||||||
job.Id, job.DocumentType, job.OrderNumber ?? "N/A");
|
|
||||||
|
|
||||||
job.Status = PrintJobStatus.Processing;
|
|
||||||
job.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get document configuration from printer-config.json
|
|
||||||
var config = _documentProcessor.GetDocumentConfig(job.DocumentType);
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"No configuration found for document type '{job.DocumentType}' in printer-config.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file content
|
|
||||||
if (!File.Exists(job.QueuedFilePath))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException($"Queued file not found: {job.QueuedFilePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var content = _documentProcessor.ReadFile(job.QueuedFilePath);
|
|
||||||
|
|
||||||
// Process/transform content
|
|
||||||
var processedContent = _documentProcessor.ProcessDocument(content, config);
|
|
||||||
|
|
||||||
// Print to Windows queue (pass order number for transformations)
|
|
||||||
await Task.Run(() => _printerService.Print(processedContent, config, job.OriginalFilename, job.OrderNumber), cancellationToken);
|
|
||||||
|
|
||||||
// Post-process (archive or delete)
|
|
||||||
_documentProcessor.PostProcess(job, config);
|
|
||||||
|
|
||||||
// Mark as completed
|
|
||||||
job.Status = PrintJobStatus.Completed;
|
|
||||||
job.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
_logger.LogInformation("Job {JobId} completed successfully", job.Id);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to process job {JobId}", job.Id);
|
|
||||||
|
|
||||||
job.LastError = ex.Message;
|
|
||||||
job.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
// Requeue for retry
|
|
||||||
_queueService.Requeue(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AppSettings": {
|
|
||||||
"CapturesPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Captures",
|
|
||||||
"QueuePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Queue",
|
|
||||||
"ErrorPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Errors",
|
|
||||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive",
|
|
||||||
"MaxRetryAttempts": 3,
|
|
||||||
"DebounceDelayMs": 200,
|
|
||||||
"PipeName": "PrintServicePipe",
|
|
||||||
"QueueStateFile": "C:\\Users\\Work\\Desktop\\LAAPC\\queue_state.json",
|
|
||||||
"VerboseLogging": true,
|
|
||||||
"DocumentTypes": [
|
|
||||||
{
|
|
||||||
"Name": "invoice",
|
|
||||||
"PrinterName": "Microsoft Print to PDF",
|
|
||||||
"TraySequence": [ 3, 4, 1, 2 ],
|
|
||||||
"FontName": "Courier New",
|
|
||||||
"FontSize": 10.0,
|
|
||||||
"VerticalOffset": 0,
|
|
||||||
"HorizontalOffset": 0,
|
|
||||||
"ArchiveAfterPrint": true,
|
|
||||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Invoices",
|
|
||||||
"Transformations": [
|
|
||||||
{
|
|
||||||
"Pattern": "W1DUPLICATE INVW0",
|
|
||||||
"Replacement": "",
|
|
||||||
"Description": "Remove duplicate invoice marker"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "order",
|
|
||||||
"PrinterName": "Brother HL-2270DW series Printer",
|
|
||||||
"TraySequence": [ 4, 2, 1 ],
|
|
||||||
"FontName": "Courier New",
|
|
||||||
"FontSize": 10.0,
|
|
||||||
"VerticalOffset": 0,
|
|
||||||
"HorizontalOffset": 0,
|
|
||||||
"ArchiveAfterPrint": true,
|
|
||||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Orders",
|
|
||||||
"Transformations": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "delivery",
|
|
||||||
"PrinterName": "Brother HL-2270DW series Printer",
|
|
||||||
"TraySequence": [ 3, 1 ],
|
|
||||||
"FontName": "Courier New",
|
|
||||||
"FontSize": 10.0,
|
|
||||||
"VerticalOffset": 0,
|
|
||||||
"HorizontalOffset": 0,
|
|
||||||
"ArchiveAfterPrint": true,
|
|
||||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Delivery",
|
|
||||||
"Transformations": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "test",
|
|
||||||
"PrinterName": "Microsoft Print to PDF",
|
|
||||||
"TraySequence": [ 1 ],
|
|
||||||
"FontName": "Courier New",
|
|
||||||
"FontSize": 10.0,
|
|
||||||
"VerticalOffset": 0,
|
|
||||||
"HorizontalOffset": 0,
|
|
||||||
"ArchiveAfterPrint": false,
|
|
||||||
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
|
|
||||||
"SkipPostProcessing": true,
|
|
||||||
"Transformations": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Use x86 only for Release builds (for 32-bit target machines) -->
|
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
|
||||||
<PlatformTarget>x86</PlatformTarget>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="System.IO.Pipes" Version="4.3.0" />
|
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="7.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
using System.IO.Pipes;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace PrintServiceCLI;
|
|
||||||
|
|
||||||
class Program
|
|
||||||
{
|
|
||||||
private const string PipeName = "PrintServicePipe";
|
|
||||||
private const int TimeoutMs = 5000;
|
|
||||||
|
|
||||||
static async Task<int> Main(string[] args)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Handle special --install-service flag (called by elevated process)
|
|
||||||
if (args.Contains("--install-service"))
|
|
||||||
{
|
|
||||||
return ServiceManager.EnsureServiceRunning(silent: false) ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for skip-service-check flag
|
|
||||||
bool skipServiceCheck = args.Contains("--skip-service-check");
|
|
||||||
|
|
||||||
// Parse arguments
|
|
||||||
var command = ParseArguments(args, out bool showHelp);
|
|
||||||
if (showHelp || command == null)
|
|
||||||
{
|
|
||||||
ShowUsage();
|
|
||||||
return command == null ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure service is running (unless testing)
|
|
||||||
if (!skipServiceCheck)
|
|
||||||
{
|
|
||||||
if (!ServiceManager.EnsureServiceRunning())
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("ERROR: Service is not available. Cannot process print request.");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await SendCommandToService(command);
|
|
||||||
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"SUCCESS: {result.Message}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"ERROR: {result.Message}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"FATAL ERROR: {ex.Message}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse command line arguments
|
|
||||||
/// </summary>
|
|
||||||
static PrintCommand? ParseArguments(string[] args, out bool showHelp)
|
|
||||||
{
|
|
||||||
showHelp = false;
|
|
||||||
string? filePath = null;
|
|
||||||
string? documentType = null;
|
|
||||||
string? orderNumber = null;
|
|
||||||
|
|
||||||
for (int i = 0; i < args.Length; i++)
|
|
||||||
{
|
|
||||||
switch (args[i].ToLower())
|
|
||||||
{
|
|
||||||
case "-f":
|
|
||||||
case "--file":
|
|
||||||
if (i + 1 < args.Length)
|
|
||||||
{
|
|
||||||
filePath = args[++i];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "-t":
|
|
||||||
case "--type":
|
|
||||||
if (i + 1 < args.Length)
|
|
||||||
{
|
|
||||||
documentType = args[++i];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "-o":
|
|
||||||
case "--order":
|
|
||||||
if (i + 1 < args.Length)
|
|
||||||
{
|
|
||||||
orderNumber = args[++i];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "-h":
|
|
||||||
case "--help":
|
|
||||||
case "?":
|
|
||||||
showHelp = true;
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case "--skip-service-check":
|
|
||||||
case "--install-service":
|
|
||||||
// These flags are handled in Main, skip here
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required arguments
|
|
||||||
if (string.IsNullOrEmpty(filePath))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("Error: File path is required (-f)");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(documentType))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("Error: Document type is required (-t)");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(filePath))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"Error: File not found: {filePath}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PrintCommand
|
|
||||||
{
|
|
||||||
FilePath = Path.GetFullPath(filePath),
|
|
||||||
DocumentType = documentType,
|
|
||||||
OrderNumber = orderNumber
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send command to service via named pipe
|
|
||||||
/// </summary>
|
|
||||||
static async Task<PrintResponse> SendCommandToService(PrintCommand command)
|
|
||||||
{
|
|
||||||
using var pipeClient = new NamedPipeClientStream(
|
|
||||||
".",
|
|
||||||
PipeName,
|
|
||||||
PipeDirection.InOut,
|
|
||||||
PipeOptions.Asynchronous);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await pipeClient.ConnectAsync(TimeoutMs);
|
|
||||||
}
|
|
||||||
catch (TimeoutException)
|
|
||||||
{
|
|
||||||
return new PrintResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Service not responding. Is the LAAPC Print Service running?"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
return new PrintResponse
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"Cannot connect to service: {ex.Message}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send command
|
|
||||||
var json = JsonSerializer.Serialize(command);
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(json);
|
|
||||||
await pipeClient.WriteAsync(bytes, 0, bytes.Length);
|
|
||||||
await pipeClient.FlushAsync();
|
|
||||||
|
|
||||||
// Read response
|
|
||||||
var buffer = new byte[4096];
|
|
||||||
var bytesRead = await pipeClient.ReadAsync(buffer, 0, buffer.Length);
|
|
||||||
var responseJson = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
|
||||||
|
|
||||||
var response = JsonSerializer.Deserialize<PrintResponse>(responseJson);
|
|
||||||
return response ?? new PrintResponse { Success = false, Message = "Invalid response from service" };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show usage information
|
|
||||||
/// </summary>
|
|
||||||
static void ShowUsage()
|
|
||||||
{
|
|
||||||
Console.WriteLine("LAAPC Print Service CLI");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Usage: PrintServiceCLI -f <filepath> -t <doctype> [-o <ordernumber>]");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Options:");
|
|
||||||
Console.WriteLine(" -f, --file <path> Path to the capture file to print (required)");
|
|
||||||
Console.WriteLine(" -t, --type <type> Document type (required)");
|
|
||||||
Console.WriteLine(" Examples: invoice, order, delivery");
|
|
||||||
Console.WriteLine(" -o, --order <number> Optional order number");
|
|
||||||
Console.WriteLine(" -h, --help Show this help message");
|
|
||||||
Console.WriteLine(" --skip-service-check Skip service availability check (for testing)");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Examples:");
|
|
||||||
Console.WriteLine(" PrintServiceCLI -f \"C:\\Captures\\file.txt\" -t invoice");
|
|
||||||
Console.WriteLine(" PrintServiceCLI -f \"C:\\Captures\\file.txt\" -t order -o 12345");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("For Development/Testing:");
|
|
||||||
Console.WriteLine(" PrintServiceCLI -f \"file.txt\" -t test --skip-service-check");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command to send to service
|
|
||||||
/// </summary>
|
|
||||||
class PrintCommand
|
|
||||||
{
|
|
||||||
public string FilePath { get; set; } = string.Empty;
|
|
||||||
public string DocumentType { get; set; } = string.Empty;
|
|
||||||
public string? OrderNumber { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Response from service
|
|
||||||
/// </summary>
|
|
||||||
class PrintResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.ServiceProcess;
|
|
||||||
using System.Security.Principal;
|
|
||||||
|
|
||||||
namespace PrintServiceCLI;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages Windows Service installation and verification
|
|
||||||
/// </summary>
|
|
||||||
public static class ServiceManager
|
|
||||||
{
|
|
||||||
private const string ServiceName = "LAAPC Print Service";
|
|
||||||
private const string ServiceInstallPath = @"C:\ProgramData\LAAPC";
|
|
||||||
private const string ServiceExeName = "PrintService.exe";
|
|
||||||
|
|
||||||
private static bool? _serviceAvailable = null; // Cache result
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check if service is installed and running, with auto-install option
|
|
||||||
/// </summary>
|
|
||||||
public static bool EnsureServiceRunning(bool silent = false)
|
|
||||||
{
|
|
||||||
// Return cached result if already checked
|
|
||||||
if (_serviceAvailable.HasValue)
|
|
||||||
return _serviceAvailable.Value;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Check if service exists and is running
|
|
||||||
using var service = new ServiceController(ServiceName);
|
|
||||||
service.Refresh();
|
|
||||||
|
|
||||||
if (service.Status == ServiceControllerStatus.Running)
|
|
||||||
{
|
|
||||||
_serviceAvailable = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service exists but not running - try to start it
|
|
||||||
if (service.Status == ServiceControllerStatus.Stopped)
|
|
||||||
{
|
|
||||||
if (!silent)
|
|
||||||
Console.WriteLine("Service is stopped. Starting...");
|
|
||||||
|
|
||||||
service.Start();
|
|
||||||
service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10));
|
|
||||||
_serviceAvailable = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
// Service doesn't exist - offer to install
|
|
||||||
if (silent)
|
|
||||||
{
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return PromptAndInstall();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (!silent)
|
|
||||||
Console.Error.WriteLine($"Error checking service: {ex.Message}");
|
|
||||||
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prompt user and install service if they agree
|
|
||||||
/// </summary>
|
|
||||||
private static bool PromptAndInstall()
|
|
||||||
{
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("====================================================");
|
|
||||||
Console.WriteLine("LAAPC Print Service is not installed on this PC.");
|
|
||||||
Console.WriteLine("====================================================");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("The service must be installed locally to process print jobs.");
|
|
||||||
Console.WriteLine("This is a one-time setup that requires administrator access.");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.Write("Install the service now? [Y/N]: ");
|
|
||||||
|
|
||||||
var response = Console.ReadLine()?.Trim().ToUpper();
|
|
||||||
|
|
||||||
if (response != "Y" && response != "YES")
|
|
||||||
{
|
|
||||||
Console.WriteLine("Installation cancelled. Service is required for printing.");
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return InstallService();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Install and start the service with UAC elevation
|
|
||||||
/// </summary>
|
|
||||||
private static bool InstallService()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Installing service...");
|
|
||||||
|
|
||||||
// Check if running as admin
|
|
||||||
if (!IsAdministrator())
|
|
||||||
{
|
|
||||||
Console.WriteLine("Requesting administrator privileges...");
|
|
||||||
return InstallWithElevation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy service files to local machine
|
|
||||||
var cliPath = AppContext.BaseDirectory;
|
|
||||||
var serviceSourcePath = Path.Combine(Path.GetDirectoryName(cliPath)!, ServiceExeName);
|
|
||||||
|
|
||||||
if (!File.Exists(serviceSourcePath))
|
|
||||||
{
|
|
||||||
// Try relative path
|
|
||||||
serviceSourcePath = Path.Combine(cliPath, "..", "PrintService", ServiceExeName);
|
|
||||||
serviceSourcePath = Path.GetFullPath(serviceSourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(serviceSourcePath))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"ERROR: Could not find {ServiceExeName}");
|
|
||||||
Console.Error.WriteLine($"Expected location: {serviceSourcePath}");
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create installation directory
|
|
||||||
Directory.CreateDirectory(ServiceInstallPath);
|
|
||||||
|
|
||||||
// Copy service executable and dependencies
|
|
||||||
var serviceDestPath = Path.Combine(ServiceInstallPath, ServiceExeName);
|
|
||||||
CopyServiceFiles(Path.GetDirectoryName(serviceSourcePath)!, ServiceInstallPath);
|
|
||||||
|
|
||||||
Console.WriteLine($"Files copied to: {ServiceInstallPath}");
|
|
||||||
|
|
||||||
// Install Windows Service using sc.exe
|
|
||||||
var scInstall = Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "sc.exe",
|
|
||||||
Arguments = $"create \"{ServiceName}\" binPath=\"{serviceDestPath}\" start=auto",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
});
|
|
||||||
|
|
||||||
scInstall?.WaitForExit();
|
|
||||||
|
|
||||||
if (scInstall?.ExitCode != 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("ERROR: Failed to install service");
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("Service installed successfully.");
|
|
||||||
|
|
||||||
// Start the service
|
|
||||||
Console.WriteLine("Starting service...");
|
|
||||||
var scStart = Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "sc.exe",
|
|
||||||
Arguments = $"start \"{ServiceName}\"",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
});
|
|
||||||
|
|
||||||
scStart?.WaitForExit();
|
|
||||||
|
|
||||||
if (scStart?.ExitCode != 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("WARNING: Service installed but failed to start");
|
|
||||||
Console.Error.WriteLine("Try running: sc start \"LAAPC Print Service\"");
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for service to be ready
|
|
||||||
Thread.Sleep(2000);
|
|
||||||
|
|
||||||
Console.WriteLine("Service started successfully!");
|
|
||||||
Console.WriteLine();
|
|
||||||
_serviceAvailable = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"ERROR: Installation failed: {ex.Message}");
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Restart process with admin elevation
|
|
||||||
/// </summary>
|
|
||||||
private static bool InstallWithElevation()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var startInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = Process.GetCurrentProcess().MainModule?.FileName ?? "PrintServiceCLI.exe",
|
|
||||||
Arguments = "--install-service",
|
|
||||||
UseShellExecute = true,
|
|
||||||
Verb = "runas" // Trigger UAC prompt
|
|
||||||
};
|
|
||||||
|
|
||||||
var process = Process.Start(startInfo);
|
|
||||||
process?.WaitForExit();
|
|
||||||
|
|
||||||
if (process?.ExitCode == 0)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Service installed successfully.");
|
|
||||||
_serviceAvailable = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine("Installation was cancelled or failed.");
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"ERROR: Could not elevate privileges: {ex.Message}");
|
|
||||||
_serviceAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copy service files from source to destination
|
|
||||||
/// </summary>
|
|
||||||
private static void CopyServiceFiles(string sourceDir, string destDir)
|
|
||||||
{
|
|
||||||
// Copy main executable
|
|
||||||
var sourceExe = Path.Combine(sourceDir, ServiceExeName);
|
|
||||||
var destExe = Path.Combine(destDir, ServiceExeName);
|
|
||||||
File.Copy(sourceExe, destExe, overwrite: true);
|
|
||||||
|
|
||||||
// Copy all DLLs and config files
|
|
||||||
foreach (var file in Directory.GetFiles(sourceDir))
|
|
||||||
{
|
|
||||||
var fileName = Path.GetFileName(file);
|
|
||||||
var ext = Path.GetExtension(file).ToLower();
|
|
||||||
|
|
||||||
if (ext == ".dll" || ext == ".json" || ext == ".config")
|
|
||||||
{
|
|
||||||
var destFile = Path.Combine(destDir, fileName);
|
|
||||||
File.Copy(file, destFile, overwrite: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create necessary folders
|
|
||||||
var capturesPath = Path.Combine(destDir, "Captures");
|
|
||||||
var queuePath = Path.Combine(destDir, "Queue");
|
|
||||||
var archivePath = Path.Combine(destDir, "Archive");
|
|
||||||
var errorPath = Path.Combine(destDir, "Error");
|
|
||||||
var outputPath = Path.Combine(destDir, "Output");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(capturesPath);
|
|
||||||
Directory.CreateDirectory(queuePath);
|
|
||||||
Directory.CreateDirectory(archivePath);
|
|
||||||
Directory.CreateDirectory(errorPath);
|
|
||||||
Directory.CreateDirectory(outputPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check if running with administrator privileges
|
|
||||||
/// </summary>
|
|
||||||
private static bool IsAdministrator()
|
|
||||||
{
|
|
||||||
using var identity = WindowsIdentity.GetCurrent();
|
|
||||||
var principal = new WindowsPrincipal(identity);
|
|
||||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-38
@@ -1,38 +0,0 @@
|
|||||||
namespace PrintServiceTray;
|
|
||||||
|
|
||||||
partial class Form1
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Required designer variable.
|
|
||||||
/// </summary>
|
|
||||||
private System.ComponentModel.IContainer components = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clean up any resources being used.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing && (components != null))
|
|
||||||
{
|
|
||||||
components.Dispose();
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Windows Form Designer generated code
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Required method for Designer support - do not modify
|
|
||||||
/// the contents of this method with the code editor.
|
|
||||||
/// </summary>
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
this.components = new System.ComponentModel.Container();
|
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
|
||||||
this.ClientSize = new System.Drawing.Size(800, 450);
|
|
||||||
this.Text = "Form1";
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace PrintServiceTray;
|
|
||||||
|
|
||||||
public partial class Form1 : Form
|
|
||||||
{
|
|
||||||
public Form1()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,515 +0,0 @@
|
|||||||
using PrintServiceTray.Models;
|
|
||||||
using PrintServiceTray.Services;
|
|
||||||
|
|
||||||
namespace PrintServiceTray.Forms;
|
|
||||||
|
|
||||||
public partial class ConfigurationForm : Form
|
|
||||||
{
|
|
||||||
private readonly ConfigurationService _configService;
|
|
||||||
private PrinterConfiguration _config;
|
|
||||||
private string? _selectedDocType;
|
|
||||||
private List<string> _installedPrinters;
|
|
||||||
|
|
||||||
// UI Controls
|
|
||||||
private ListBox _docTypeListBox = null!;
|
|
||||||
private Button _newDocTypeButton = null!;
|
|
||||||
private Button _deleteDocTypeButton = null!;
|
|
||||||
private Panel _pageConfigPanel = null!;
|
|
||||||
private RadioButton _saveGlobalRadio = null!;
|
|
||||||
private RadioButton _saveLocalRadio = null!;
|
|
||||||
private Button _saveButton = null!;
|
|
||||||
private Button _cancelButton = null!;
|
|
||||||
private Button _addPageButton = null!;
|
|
||||||
private Button _removePageButton = null!;
|
|
||||||
private Label _configLabel = null!;
|
|
||||||
|
|
||||||
private List<PageConfigControl> _pageControls = new();
|
|
||||||
|
|
||||||
public ConfigurationForm()
|
|
||||||
{
|
|
||||||
_configService = new ConfigurationService();
|
|
||||||
_config = _configService.Load();
|
|
||||||
_installedPrinters = _configService.GetInstalledPrinters();
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
LoadDocumentTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
Text = "LAAPC Printer Configuration";
|
|
||||||
Size = new Size(900, 600);
|
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
|
||||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
||||||
MaximizeBox = false;
|
|
||||||
|
|
||||||
// Left panel - Document Types
|
|
||||||
var leftPanel = new Panel
|
|
||||||
{
|
|
||||||
Location = new Point(10, 10),
|
|
||||||
Size = new Size(250, 540),
|
|
||||||
BorderStyle = BorderStyle.FixedSingle
|
|
||||||
};
|
|
||||||
|
|
||||||
var docTypeLabel = new Label
|
|
||||||
{
|
|
||||||
Text = "Document Types:",
|
|
||||||
Location = new Point(10, 10),
|
|
||||||
Size = new Size(230, 20),
|
|
||||||
Font = new Font(Font, FontStyle.Bold)
|
|
||||||
};
|
|
||||||
|
|
||||||
_docTypeListBox = new ListBox
|
|
||||||
{
|
|
||||||
Location = new Point(10, 35),
|
|
||||||
Size = new Size(230, 430)
|
|
||||||
};
|
|
||||||
_docTypeListBox.SelectedIndexChanged += DocTypeListBox_SelectedIndexChanged;
|
|
||||||
|
|
||||||
_newDocTypeButton = new Button
|
|
||||||
{
|
|
||||||
Text = "New",
|
|
||||||
Location = new Point(10, 475),
|
|
||||||
Size = new Size(110, 30)
|
|
||||||
};
|
|
||||||
_newDocTypeButton.Click += NewDocType_Click;
|
|
||||||
|
|
||||||
_deleteDocTypeButton = new Button
|
|
||||||
{
|
|
||||||
Text = "Delete",
|
|
||||||
Location = new Point(130, 475),
|
|
||||||
Size = new Size(110, 30)
|
|
||||||
};
|
|
||||||
_deleteDocTypeButton.Click += DeleteDocType_Click;
|
|
||||||
|
|
||||||
leftPanel.Controls.AddRange(new Control[] {
|
|
||||||
docTypeLabel, _docTypeListBox, _newDocTypeButton, _deleteDocTypeButton
|
|
||||||
});
|
|
||||||
|
|
||||||
// Right panel - Page Configuration
|
|
||||||
var rightPanel = new Panel
|
|
||||||
{
|
|
||||||
Location = new Point(270, 10),
|
|
||||||
Size = new Size(610, 540),
|
|
||||||
BorderStyle = BorderStyle.FixedSingle
|
|
||||||
};
|
|
||||||
|
|
||||||
_configLabel = new Label
|
|
||||||
{
|
|
||||||
Text = "Configuration for: (Select a document type)",
|
|
||||||
Location = new Point(10, 10),
|
|
||||||
Size = new Size(590, 20),
|
|
||||||
Font = new Font(Font, FontStyle.Bold)
|
|
||||||
};
|
|
||||||
|
|
||||||
var pageConfigLabel = new Label
|
|
||||||
{
|
|
||||||
Text = "Page Configuration:",
|
|
||||||
Location = new Point(10, 40),
|
|
||||||
Size = new Size(590, 20)
|
|
||||||
};
|
|
||||||
|
|
||||||
_pageConfigPanel = new Panel
|
|
||||||
{
|
|
||||||
Location = new Point(10, 65),
|
|
||||||
Size = new Size(590, 350),
|
|
||||||
AutoScroll = true,
|
|
||||||
BorderStyle = BorderStyle.FixedSingle
|
|
||||||
};
|
|
||||||
|
|
||||||
_addPageButton = new Button
|
|
||||||
{
|
|
||||||
Text = "+ Add Page",
|
|
||||||
Location = new Point(10, 425),
|
|
||||||
Size = new Size(120, 30)
|
|
||||||
};
|
|
||||||
_addPageButton.Click += AddPage_Click;
|
|
||||||
|
|
||||||
_removePageButton = new Button
|
|
||||||
{
|
|
||||||
Text = "- Remove Last",
|
|
||||||
Location = new Point(140, 425),
|
|
||||||
Size = new Size(120, 30)
|
|
||||||
};
|
|
||||||
_removePageButton.Click += RemovePage_Click;
|
|
||||||
|
|
||||||
// Save options
|
|
||||||
var saveToLabel = new Label
|
|
||||||
{
|
|
||||||
Text = "Save to:",
|
|
||||||
Location = new Point(10, 465),
|
|
||||||
Size = new Size(60, 20)
|
|
||||||
};
|
|
||||||
|
|
||||||
_saveGlobalRadio = new RadioButton
|
|
||||||
{
|
|
||||||
Text = "Global (All Users)",
|
|
||||||
Location = new Point(80, 465),
|
|
||||||
Size = new Size(150, 20)
|
|
||||||
};
|
|
||||||
|
|
||||||
_saveLocalRadio = new RadioButton
|
|
||||||
{
|
|
||||||
Text = "Local (My Computer)",
|
|
||||||
Location = new Point(240, 465),
|
|
||||||
Size = new Size(170, 20),
|
|
||||||
Checked = true
|
|
||||||
};
|
|
||||||
|
|
||||||
_saveButton = new Button
|
|
||||||
{
|
|
||||||
Text = "Save",
|
|
||||||
Location = new Point(430, 495),
|
|
||||||
Size = new Size(80, 35)
|
|
||||||
};
|
|
||||||
_saveButton.Click += Save_Click;
|
|
||||||
|
|
||||||
_cancelButton = new Button
|
|
||||||
{
|
|
||||||
Text = "Cancel",
|
|
||||||
Location = new Point(520, 495),
|
|
||||||
Size = new Size(80, 35)
|
|
||||||
};
|
|
||||||
_cancelButton.Click += (s, e) => Close();
|
|
||||||
|
|
||||||
rightPanel.Controls.AddRange(new Control[] {
|
|
||||||
_configLabel, pageConfigLabel, _pageConfigPanel,
|
|
||||||
_addPageButton, _removePageButton,
|
|
||||||
saveToLabel, _saveGlobalRadio, _saveLocalRadio,
|
|
||||||
_saveButton, _cancelButton
|
|
||||||
});
|
|
||||||
|
|
||||||
Controls.AddRange(new Control[] { leftPanel, rightPanel });
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadDocumentTypes()
|
|
||||||
{
|
|
||||||
_docTypeListBox.Items.Clear();
|
|
||||||
foreach (var docType in _config.DocumentTypes.Keys.OrderBy(k => k))
|
|
||||||
{
|
|
||||||
_docTypeListBox.Items.Add(docType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DocTypeListBox_SelectedIndexChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (_docTypeListBox.SelectedItem is string docType)
|
|
||||||
{
|
|
||||||
_selectedDocType = docType;
|
|
||||||
LoadPageConfiguration(docType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadPageConfiguration(string docType)
|
|
||||||
{
|
|
||||||
_configLabel.Text = $"Configuration for: {docType}";
|
|
||||||
_pageConfigPanel.Controls.Clear();
|
|
||||||
_pageControls.Clear();
|
|
||||||
|
|
||||||
if (!_config.DocumentTypes.TryGetValue(docType, out var config))
|
|
||||||
{
|
|
||||||
config = new DocumentTypeConfig { Name = docType };
|
|
||||||
_config.DocumentTypes[docType] = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
int yPos = 10;
|
|
||||||
foreach (var page in config.Pages.OrderBy(p => p.PageNumber))
|
|
||||||
{
|
|
||||||
var pageControl = new PageConfigControl(page.PageNumber, _installedPrinters, _configService);
|
|
||||||
pageControl.Location = new Point(10, yPos);
|
|
||||||
pageControl.LoadPage(page);
|
|
||||||
_pageConfigPanel.Controls.Add(pageControl);
|
|
||||||
_pageControls.Add(pageControl);
|
|
||||||
yPos += pageControl.Height + 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.Pages.Count == 0)
|
|
||||||
{
|
|
||||||
// Add first page by default
|
|
||||||
AddPage_Click(null, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddPage_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (_selectedDocType == null) return;
|
|
||||||
|
|
||||||
var config = _config.DocumentTypes[_selectedDocType];
|
|
||||||
int nextPageNum = config.Pages.Count > 0 ? config.Pages.Max(p => p.PageNumber) + 1 : 1;
|
|
||||||
|
|
||||||
var newPage = new PageConfig
|
|
||||||
{
|
|
||||||
PageNumber = nextPageNum,
|
|
||||||
PrinterName = _installedPrinters.FirstOrDefault() ?? "",
|
|
||||||
TrayNumber = 1
|
|
||||||
};
|
|
||||||
config.Pages.Add(newPage);
|
|
||||||
|
|
||||||
LoadPageConfiguration(_selectedDocType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemovePage_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (_selectedDocType == null) return;
|
|
||||||
|
|
||||||
var config = _config.DocumentTypes[_selectedDocType];
|
|
||||||
if (config.Pages.Count > 0)
|
|
||||||
{
|
|
||||||
var lastPage = config.Pages.OrderByDescending(p => p.PageNumber).First();
|
|
||||||
config.Pages.Remove(lastPage);
|
|
||||||
LoadPageConfiguration(_selectedDocType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NewDocType_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var dialog = new TextInputDialog("New Document Type", "Enter document type name:");
|
|
||||||
if (dialog.ShowDialog() == DialogResult.OK && !string.IsNullOrWhiteSpace(dialog.InputText))
|
|
||||||
{
|
|
||||||
var docType = dialog.InputText.Trim();
|
|
||||||
if (!_config.DocumentTypes.ContainsKey(docType))
|
|
||||||
{
|
|
||||||
_config.DocumentTypes[docType] = new DocumentTypeConfig { Name = docType };
|
|
||||||
LoadDocumentTypes();
|
|
||||||
_docTypeListBox.SelectedItem = docType;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MessageBox.Show($"Document type '{docType}' already exists.", "Duplicate", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteDocType_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (_selectedDocType == null) return;
|
|
||||||
|
|
||||||
var result = MessageBox.Show(
|
|
||||||
$"Are you sure you want to delete '{_selectedDocType}'?",
|
|
||||||
"Confirm Delete",
|
|
||||||
MessageBoxButtons.YesNo,
|
|
||||||
MessageBoxIcon.Question);
|
|
||||||
|
|
||||||
if (result == DialogResult.Yes)
|
|
||||||
{
|
|
||||||
_config.DocumentTypes.Remove(_selectedDocType);
|
|
||||||
_selectedDocType = null;
|
|
||||||
LoadDocumentTypes();
|
|
||||||
_pageConfigPanel.Controls.Clear();
|
|
||||||
_configLabel.Text = "Configuration for: (Select a document type)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Save_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Save current page configurations
|
|
||||||
if (_selectedDocType != null && _config.DocumentTypes.TryGetValue(_selectedDocType, out var config))
|
|
||||||
{
|
|
||||||
config.Pages.Clear();
|
|
||||||
foreach (var pageControl in _pageControls)
|
|
||||||
{
|
|
||||||
config.Pages.Add(pageControl.GetPageConfig());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool saveGlobal = _saveGlobalRadio.Checked;
|
|
||||||
_configService.Save(_config, saveGlobal);
|
|
||||||
|
|
||||||
MessageBox.Show(
|
|
||||||
$"Configuration saved to {(saveGlobal ? "global" : "local")} file successfully!",
|
|
||||||
"Success",
|
|
||||||
MessageBoxButtons.OK,
|
|
||||||
MessageBoxIcon.Information);
|
|
||||||
|
|
||||||
DialogResult = DialogResult.OK;
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show(
|
|
||||||
$"Failed to save configuration: {ex.Message}",
|
|
||||||
"Error",
|
|
||||||
MessageBoxButtons.OK,
|
|
||||||
MessageBoxIcon.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom control for each page configuration
|
|
||||||
public class PageConfigControl : UserControl
|
|
||||||
{
|
|
||||||
private readonly int _pageNumber;
|
|
||||||
private readonly List<string> _printers;
|
|
||||||
private readonly ConfigurationService _configService;
|
|
||||||
|
|
||||||
private ComboBox _printerCombo = null!;
|
|
||||||
private ComboBox _trayCombo = null!;
|
|
||||||
|
|
||||||
public PageConfigControl(int pageNumber, List<string> printers, ConfigurationService configService)
|
|
||||||
{
|
|
||||||
_pageNumber = pageNumber;
|
|
||||||
_printers = printers;
|
|
||||||
_configService = configService;
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
Size = new Size(560, 60);
|
|
||||||
BorderStyle = BorderStyle.FixedSingle;
|
|
||||||
|
|
||||||
var pageLabel = new Label
|
|
||||||
{
|
|
||||||
Text = $"Page {_pageNumber}:",
|
|
||||||
Location = new Point(10, 10),
|
|
||||||
Size = new Size(70, 20),
|
|
||||||
Font = new Font(Font, FontStyle.Bold)
|
|
||||||
};
|
|
||||||
|
|
||||||
var printerLabel = new Label
|
|
||||||
{
|
|
||||||
Text = "Printer:",
|
|
||||||
Location = new Point(10, 32),
|
|
||||||
Size = new Size(60, 20)
|
|
||||||
};
|
|
||||||
|
|
||||||
_printerCombo = new ComboBox
|
|
||||||
{
|
|
||||||
Location = new Point(75, 30),
|
|
||||||
Size = new Size(250, 25),
|
|
||||||
DropDownStyle = ComboBoxStyle.DropDownList
|
|
||||||
};
|
|
||||||
_printerCombo.Items.AddRange(_printers.ToArray());
|
|
||||||
_printerCombo.SelectedIndexChanged += PrinterCombo_SelectedIndexChanged;
|
|
||||||
|
|
||||||
var trayLabel = new Label
|
|
||||||
{
|
|
||||||
Text = "Tray:",
|
|
||||||
Location = new Point(340, 32),
|
|
||||||
Size = new Size(40, 20)
|
|
||||||
};
|
|
||||||
|
|
||||||
_trayCombo = new ComboBox
|
|
||||||
{
|
|
||||||
Location = new Point(385, 30),
|
|
||||||
Size = new Size(165, 25),
|
|
||||||
DropDownStyle = ComboBoxStyle.DropDownList
|
|
||||||
};
|
|
||||||
|
|
||||||
Controls.AddRange(new Control[] {
|
|
||||||
pageLabel, printerLabel, _printerCombo, trayLabel, _trayCombo
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadPage(PageConfig page)
|
|
||||||
{
|
|
||||||
_printerCombo.SelectedItem = page.PrinterName;
|
|
||||||
LoadTrays(page.PrinterName);
|
|
||||||
|
|
||||||
// Try to find and select the tray
|
|
||||||
for (int i = 0; i < _trayCombo.Items.Count; i++)
|
|
||||||
{
|
|
||||||
if (_trayCombo.Items[i] is TrayItem item && item.TrayNumber == page.TrayNumber)
|
|
||||||
{
|
|
||||||
_trayCombo.SelectedIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PrinterCombo_SelectedIndexChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (_printerCombo.SelectedItem is string printer)
|
|
||||||
{
|
|
||||||
LoadTrays(printer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadTrays(string printerName)
|
|
||||||
{
|
|
||||||
_trayCombo.Items.Clear();
|
|
||||||
var trays = _configService.GetPrinterTrays(printerName);
|
|
||||||
foreach (var (number, name) in trays)
|
|
||||||
{
|
|
||||||
_trayCombo.Items.Add(new TrayItem { TrayNumber = number, TrayName = name });
|
|
||||||
}
|
|
||||||
if (_trayCombo.Items.Count > 0)
|
|
||||||
{
|
|
||||||
_trayCombo.SelectedIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PageConfig GetPageConfig()
|
|
||||||
{
|
|
||||||
var selectedTray = _trayCombo.SelectedItem as TrayItem;
|
|
||||||
return new PageConfig
|
|
||||||
{
|
|
||||||
PageNumber = _pageNumber,
|
|
||||||
PrinterName = _printerCombo.SelectedItem?.ToString() ?? "",
|
|
||||||
TrayNumber = selectedTray?.TrayNumber ?? 1,
|
|
||||||
TrayLabel = selectedTray?.TrayName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TrayItem
|
|
||||||
{
|
|
||||||
public int TrayNumber { get; set; }
|
|
||||||
public string TrayName { get; set; } = "";
|
|
||||||
|
|
||||||
public override string ToString() => $"Tray {TrayNumber} - {TrayName}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple text input dialog
|
|
||||||
public class TextInputDialog : Form
|
|
||||||
{
|
|
||||||
private TextBox _textBox = null!;
|
|
||||||
public string InputText => _textBox.Text;
|
|
||||||
|
|
||||||
public TextInputDialog(string title, string prompt)
|
|
||||||
{
|
|
||||||
Text = title;
|
|
||||||
Size = new Size(400, 150);
|
|
||||||
StartPosition = FormStartPosition.CenterParent;
|
|
||||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
||||||
MaximizeBox = false;
|
|
||||||
MinimizeBox = false;
|
|
||||||
|
|
||||||
var promptLabel = new Label
|
|
||||||
{
|
|
||||||
Text = prompt,
|
|
||||||
Location = new Point(20, 20),
|
|
||||||
Size = new Size(360, 20)
|
|
||||||
};
|
|
||||||
|
|
||||||
_textBox = new TextBox
|
|
||||||
{
|
|
||||||
Location = new Point(20, 50),
|
|
||||||
Size = new Size(360, 25)
|
|
||||||
};
|
|
||||||
|
|
||||||
var okButton = new Button
|
|
||||||
{
|
|
||||||
Text = "OK",
|
|
||||||
DialogResult = DialogResult.OK,
|
|
||||||
Location = new Point(220, 85),
|
|
||||||
Size = new Size(75, 30)
|
|
||||||
};
|
|
||||||
|
|
||||||
var cancelButton = new Button
|
|
||||||
{
|
|
||||||
Text = "Cancel",
|
|
||||||
DialogResult = DialogResult.Cancel,
|
|
||||||
Location = new Point(305, 85),
|
|
||||||
Size = new Size(75, 30)
|
|
||||||
};
|
|
||||||
|
|
||||||
AcceptButton = okButton;
|
|
||||||
CancelButton = cancelButton;
|
|
||||||
|
|
||||||
Controls.AddRange(new Control[] { promptLabel, _textBox, okButton, cancelButton });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
namespace PrintServiceTray.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for all document types
|
|
||||||
/// </summary>
|
|
||||||
public class PrinterConfiguration
|
|
||||||
{
|
|
||||||
public Dictionary<string, DocumentTypeConfig> DocumentTypes { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for a single document type
|
|
||||||
/// </summary>
|
|
||||||
public class DocumentTypeConfig
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public List<PageConfig> Pages { get; set; } = new();
|
|
||||||
|
|
||||||
// Rust transformation properties (optional - can be edited in JSON directly)
|
|
||||||
public int LinePadding { get; set; } = 0;
|
|
||||||
public bool RemoveOrderNumber { get; set; } = false;
|
|
||||||
public bool RemoveOrderNumberLabel { get; set; } = false;
|
|
||||||
public string IndentOrderNumber { get; set; } = string.Empty;
|
|
||||||
public Dictionary<int, int> RowShift { get; set; } = new();
|
|
||||||
public Dictionary<int, RowTrimConfig> RowTrim { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for a single page
|
|
||||||
/// </summary>
|
|
||||||
public class PageConfig
|
|
||||||
{
|
|
||||||
public int PageNumber { get; set; }
|
|
||||||
public string PrinterName { get; set; } = string.Empty;
|
|
||||||
public int TrayNumber { get; set; }
|
|
||||||
public string? TrayLabel { get; set; } // Optional: "Pink Paper", "Green Paper", etc.
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for row trimming (horizontal character removal)
|
|
||||||
/// </summary>
|
|
||||||
public class RowTrimConfig
|
|
||||||
{
|
|
||||||
public int Start { get; set; }
|
|
||||||
public int End { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace PrintServiceTray.Models;
|
|
||||||
|
|
||||||
public class QueueStatusRequest
|
|
||||||
{
|
|
||||||
public string Command { get; set; } = "GetQueueStatus";
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace PrintServiceTray.Models;
|
|
||||||
|
|
||||||
public class QueueStatusResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string? Message { get; set; }
|
|
||||||
public List<JobStatus> Jobs { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class JobStatus
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string FileName { get; set; } = string.Empty;
|
|
||||||
public string DocumentType { get; set; } = string.Empty;
|
|
||||||
public string Status { get; set; } = string.Empty;
|
|
||||||
public string? PrinterName { get; set; }
|
|
||||||
public int? CurrentPage { get; set; }
|
|
||||||
public int? TotalPages { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<RuntimeHostConfigurationOption Include="System.Drawing.EnableWindowsSupport" Value="true" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
namespace PrintServiceTray;
|
|
||||||
|
|
||||||
static class Program
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The main entry point for the application.
|
|
||||||
/// </summary>
|
|
||||||
[STAThread]
|
|
||||||
static void Main()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Prevent multiple instances
|
|
||||||
bool createdNew;
|
|
||||||
using var mutex = new Mutex(true, "LAAPC_PrintServiceTray_Mutex", out createdNew);
|
|
||||||
|
|
||||||
if (!createdNew)
|
|
||||||
{
|
|
||||||
MessageBox.Show("LAAPC Print Service Tray is already running.", "Already Running",
|
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationConfiguration.Initialize();
|
|
||||||
Application.Run(new TrayApplicationContext());
|
|
||||||
|
|
||||||
GC.KeepAlive(mutex);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"Fatal error: {ex.Message}\n\n{ex.StackTrace}", "Error",
|
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using PrintServiceTray.Models;
|
|
||||||
|
|
||||||
namespace PrintServiceTray.Services;
|
|
||||||
|
|
||||||
public class ConfigurationService
|
|
||||||
{
|
|
||||||
private readonly string _globalConfigPath;
|
|
||||||
private readonly string _localConfigPath;
|
|
||||||
|
|
||||||
public ConfigurationService()
|
|
||||||
{
|
|
||||||
// Global config: next to CLI executable (C:\LAAPC or wherever installed)
|
|
||||||
var cliPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
|
||||||
"LAAPC"
|
|
||||||
);
|
|
||||||
_globalConfigPath = Path.Combine(cliPath, "printer-config.json");
|
|
||||||
|
|
||||||
// Local config: user's AppData
|
|
||||||
var appDataPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"LAAPC"
|
|
||||||
);
|
|
||||||
Directory.CreateDirectory(appDataPath);
|
|
||||||
_localConfigPath = Path.Combine(appDataPath, "printer-config.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load configuration (local overrides global)
|
|
||||||
/// </summary>
|
|
||||||
public PrinterConfiguration Load()
|
|
||||||
{
|
|
||||||
var config = new PrinterConfiguration();
|
|
||||||
|
|
||||||
// Load global first
|
|
||||||
if (File.Exists(_globalConfigPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(_globalConfigPath);
|
|
||||||
var globalConfig = JsonSerializer.Deserialize<PrinterConfiguration>(json);
|
|
||||||
if (globalConfig != null)
|
|
||||||
{
|
|
||||||
config = globalConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load local and merge/override
|
|
||||||
if (File.Exists(_localConfigPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(_localConfigPath);
|
|
||||||
var localConfig = JsonSerializer.Deserialize<PrinterConfiguration>(json);
|
|
||||||
if (localConfig != null)
|
|
||||||
{
|
|
||||||
// Merge: local overrides global for matching document types
|
|
||||||
foreach (var kvp in localConfig.DocumentTypes)
|
|
||||||
{
|
|
||||||
config.DocumentTypes[kvp.Key] = kvp.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save configuration to global or local
|
|
||||||
/// </summary>
|
|
||||||
public void Save(PrinterConfiguration config, bool saveGlobal)
|
|
||||||
{
|
|
||||||
var path = saveGlobal ? _globalConfigPath : _localConfigPath;
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
var directory = Path.GetDirectoryName(path);
|
|
||||||
if (!string.IsNullOrEmpty(directory))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
};
|
|
||||||
var json = JsonSerializer.Serialize(config, options);
|
|
||||||
File.WriteAllText(path, json);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all installed printers
|
|
||||||
/// </summary>
|
|
||||||
public List<string> GetInstalledPrinters()
|
|
||||||
{
|
|
||||||
var printers = new List<string>();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (string printerName in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
|
|
||||||
{
|
|
||||||
printers.Add(printerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return printers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get available trays for a printer
|
|
||||||
/// </summary>
|
|
||||||
public List<(int Number, string Name)> GetPrinterTrays(string printerName)
|
|
||||||
{
|
|
||||||
var trays = new List<(int, string)>();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var printerSettings = new System.Drawing.Printing.PrinterSettings
|
|
||||||
{
|
|
||||||
PrinterName = printerName
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (System.Drawing.Printing.PaperSource source in printerSettings.PaperSources)
|
|
||||||
{
|
|
||||||
trays.Add((source.RawKind, source.SourceName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return trays;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GlobalConfigPath => _globalConfigPath;
|
|
||||||
public string LocalConfigPath => _localConfigPath;
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
using System.IO.Pipes;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using PrintServiceTray.Models;
|
|
||||||
|
|
||||||
namespace PrintServiceTray.Services;
|
|
||||||
|
|
||||||
public class ServiceClient
|
|
||||||
{
|
|
||||||
private readonly string _pipeName;
|
|
||||||
private readonly int _timeout;
|
|
||||||
|
|
||||||
public ServiceClient(string pipeName = "PrintServicePipe", int timeoutMs = 5000)
|
|
||||||
{
|
|
||||||
_pipeName = pipeName;
|
|
||||||
_timeout = timeoutMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<QueueStatusResponse> GetQueueStatusAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut);
|
|
||||||
await pipe.ConnectAsync(_timeout);
|
|
||||||
|
|
||||||
var request = new QueueStatusRequest();
|
|
||||||
var requestJson = JsonSerializer.Serialize(request);
|
|
||||||
var requestBytes = Encoding.UTF8.GetBytes(requestJson);
|
|
||||||
|
|
||||||
await pipe.WriteAsync(requestBytes);
|
|
||||||
await pipe.FlushAsync();
|
|
||||||
|
|
||||||
var buffer = new byte[4096];
|
|
||||||
var bytesRead = await pipe.ReadAsync(buffer);
|
|
||||||
var responseJson = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
|
||||||
|
|
||||||
var response = JsonSerializer.Deserialize<QueueStatusResponse>(responseJson);
|
|
||||||
return response ?? new QueueStatusResponse { Success = false, Message = "Invalid response" };
|
|
||||||
}
|
|
||||||
catch (TimeoutException)
|
|
||||||
{
|
|
||||||
return new QueueStatusResponse { Success = false, Message = "Service not responding" };
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new QueueStatusResponse { Success = false, Message = $"Error: {ex.Message}" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsServiceRunningAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await GetQueueStatusAsync();
|
|
||||||
return response.Success;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using PrintServiceTray.Services;
|
|
||||||
using PrintServiceTray.Models;
|
|
||||||
|
|
||||||
namespace PrintServiceTray;
|
|
||||||
|
|
||||||
public class TrayApplicationContext : ApplicationContext
|
|
||||||
{
|
|
||||||
private readonly NotifyIcon _trayIcon;
|
|
||||||
private readonly ServiceClient _serviceClient;
|
|
||||||
private readonly System.Windows.Forms.Timer _refreshTimer;
|
|
||||||
private readonly ToolStripMenuItem _queueStatusItem;
|
|
||||||
private readonly ToolStripMenuItem _serviceStatusItem;
|
|
||||||
|
|
||||||
public TrayApplicationContext()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_serviceClient = new ServiceClient();
|
|
||||||
|
|
||||||
// Create context menu
|
|
||||||
var contextMenu = new ContextMenuStrip();
|
|
||||||
|
|
||||||
// Service status (at top)
|
|
||||||
_serviceStatusItem = new ToolStripMenuItem("● Service Running")
|
|
||||||
{
|
|
||||||
Enabled = false,
|
|
||||||
Font = new Font(contextMenu.Font, FontStyle.Bold)
|
|
||||||
};
|
|
||||||
contextMenu.Items.Add(_serviceStatusItem);
|
|
||||||
contextMenu.Items.Add(new ToolStripSeparator());
|
|
||||||
|
|
||||||
// Queue status section
|
|
||||||
_queueStatusItem = new ToolStripMenuItem("Queue Status")
|
|
||||||
{
|
|
||||||
Enabled = false
|
|
||||||
};
|
|
||||||
contextMenu.Items.Add(_queueStatusItem);
|
|
||||||
|
|
||||||
var viewQueueItem = new ToolStripMenuItem("View Full Queue...");
|
|
||||||
viewQueueItem.Click += ViewQueue_Click;
|
|
||||||
contextMenu.Items.Add(viewQueueItem);
|
|
||||||
|
|
||||||
contextMenu.Items.Add(new ToolStripSeparator());
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
var configureItem = new ToolStripMenuItem("Configure Printers...");
|
|
||||||
configureItem.Click += Configure_Click;
|
|
||||||
contextMenu.Items.Add(configureItem);
|
|
||||||
|
|
||||||
contextMenu.Items.Add(new ToolStripSeparator());
|
|
||||||
|
|
||||||
// Exit
|
|
||||||
var exitItem = new ToolStripMenuItem("Exit");
|
|
||||||
exitItem.Click += Exit_Click;
|
|
||||||
contextMenu.Items.Add(exitItem);
|
|
||||||
|
|
||||||
// Create tray icon
|
|
||||||
_trayIcon = new NotifyIcon
|
|
||||||
{
|
|
||||||
Icon = SystemIcons.Information,
|
|
||||||
ContextMenuStrip = contextMenu,
|
|
||||||
Visible = true,
|
|
||||||
Text = "LAAPC Print Service"
|
|
||||||
};
|
|
||||||
|
|
||||||
_trayIcon.DoubleClick += TrayIcon_DoubleClick;
|
|
||||||
|
|
||||||
// Show startup notification
|
|
||||||
_trayIcon.ShowBalloonTip(2000, "LAAPC Print Service", "Tray app started", ToolTipIcon.Info);
|
|
||||||
|
|
||||||
// Set up refresh timer (every 2 seconds)
|
|
||||||
_refreshTimer = new System.Windows.Forms.Timer
|
|
||||||
{
|
|
||||||
Interval = 2000
|
|
||||||
};
|
|
||||||
_refreshTimer.Tick += RefreshTimer_Tick;
|
|
||||||
_refreshTimer.Start();
|
|
||||||
|
|
||||||
// Initial refresh
|
|
||||||
_ = RefreshStatusAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"Failed to initialize tray app: {ex.Message}\n\n{ex.StackTrace}",
|
|
||||||
"Initialization Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void RefreshTimer_Tick(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
await RefreshStatusAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshStatusAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var status = await _serviceClient.GetQueueStatusAsync();
|
|
||||||
|
|
||||||
if (status.Success)
|
|
||||||
{
|
|
||||||
_serviceStatusItem.Text = "● Service Running";
|
|
||||||
_serviceStatusItem.ForeColor = Color.Green;
|
|
||||||
|
|
||||||
// Update queue status
|
|
||||||
if (status.Jobs.Count == 0)
|
|
||||||
{
|
|
||||||
_queueStatusItem.Text = "Queue Status: Empty";
|
|
||||||
_queueStatusItem.DropDownItems.Clear();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_queueStatusItem.Text = $"Queue Status ({status.Jobs.Count} {(status.Jobs.Count == 1 ? "job" : "jobs")})";
|
|
||||||
_queueStatusItem.Enabled = true;
|
|
||||||
|
|
||||||
// Update dropdown items with job list
|
|
||||||
_queueStatusItem.DropDownItems.Clear();
|
|
||||||
|
|
||||||
foreach (var job in status.Jobs.Take(10)) // Show max 10 in menu
|
|
||||||
{
|
|
||||||
var statusIcon = job.Status == "Processing" ? "●" : "○";
|
|
||||||
var jobText = $"{statusIcon} {job.Status}: {job.FileName}";
|
|
||||||
|
|
||||||
if (job.PrinterName != null)
|
|
||||||
{
|
|
||||||
jobText += $" ({job.PrinterName})";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (job.CurrentPage.HasValue && job.TotalPages.HasValue)
|
|
||||||
{
|
|
||||||
jobText += $" - Page {job.CurrentPage}/{job.TotalPages}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobItem = new ToolStripMenuItem(jobText) { Enabled = false };
|
|
||||||
_queueStatusItem.DropDownItems.Add(jobItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.Jobs.Count > 10)
|
|
||||||
{
|
|
||||||
_queueStatusItem.DropDownItems.Add(new ToolStripSeparator());
|
|
||||||
var moreItem = new ToolStripMenuItem($"... and {status.Jobs.Count - 10} more")
|
|
||||||
{
|
|
||||||
Enabled = false
|
|
||||||
};
|
|
||||||
_queueStatusItem.DropDownItems.Add(moreItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update tooltip
|
|
||||||
_trayIcon.Text = status.Jobs.Count == 0
|
|
||||||
? "LAAPC Print Service - Queue empty"
|
|
||||||
: $"LAAPC Print Service - {status.Jobs.Count} job(s) in queue";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_serviceStatusItem.Text = "○ Service Not Running";
|
|
||||||
_serviceStatusItem.ForeColor = Color.Red;
|
|
||||||
_queueStatusItem.Text = "Queue Status: Unavailable";
|
|
||||||
_queueStatusItem.DropDownItems.Clear();
|
|
||||||
_trayIcon.Text = "LAAPC Print Service - Not running";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Error refreshing status: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TrayIcon_DoubleClick(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// Double-click opens full queue view
|
|
||||||
ViewQueue_Click(sender, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ViewQueue_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// TODO: Open queue view window
|
|
||||||
MessageBox.Show("Queue view window coming soon!", "LAAPC Print Service", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Configure_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var configForm = new Forms.ConfigurationForm();
|
|
||||||
configForm.ShowDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Exit_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
_refreshTimer.Stop();
|
|
||||||
_trayIcon.Visible = false;
|
|
||||||
Application.Exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_refreshTimer?.Stop();
|
|
||||||
_refreshTimer?.Dispose();
|
|
||||||
_trayIcon?.Dispose();
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,283 +1,2 @@
|
|||||||
# LAAPC Print Service
|
# CGW-Printing
|
||||||
|
|
||||||
A Windows service for managing print jobs from Harbor/Clipper legacy applications to modern laser printers with multi-tray support.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This service solves the problem of migrating from dot-matrix printers with carbon copy paper to laser printers with colored paper trays. It:
|
|
||||||
|
|
||||||
- ✅ **Prevents file overwrites** by immediately moving capture files to a queue with GUID-based names
|
|
||||||
- ✅ **Maintains print order** with a persistent queue system
|
|
||||||
- ✅ **Controls printer trays** via Windows print queue API for "carbon copy" simulation
|
|
||||||
- ✅ **Transforms content** with configurable rules per document type
|
|
||||||
- ✅ **Archives prints** for record-keeping
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- **PrintService**: Windows service that runs continuously, monitors for print jobs, and processes them
|
|
||||||
- **PrintServiceCLI**: Command-line tool for Harbor/Clipper to submit print jobs via IPC (named pipes)
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### 1. Configure
|
|
||||||
|
|
||||||
Edit `PrintService/appsettings.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"AppSettings": {
|
|
||||||
"CapturesPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Captures",
|
|
||||||
"DocumentTypes": [
|
|
||||||
{
|
|
||||||
"Name": "invoice",
|
|
||||||
"PrinterName": "Your_Actual_Printer_Name",
|
|
||||||
"TraySequence": [ 3, 4, 1, 2 ]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: Replace `Your_Actual_Printer_Name` with actual Windows printer name(s).
|
|
||||||
|
|
||||||
### 2. Build
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Build for 32-bit (supports both 32-bit and 64-bit Windows)
|
|
||||||
dotnet build -c Release -r win-x86
|
|
||||||
|
|
||||||
# Or publish self-contained (includes .NET runtime)
|
|
||||||
dotnet publish -c Release -r win-x86 --self-contained true -o publish/x86
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Install Service
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Run as Administrator
|
|
||||||
sc.exe create "LAAPC Print Service" binPath="C:\Path\To\PrintService.exe" start=auto
|
|
||||||
sc.exe start "LAAPC Print Service"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Test CLI
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
PrintServiceCLI.exe -f "C:\Captures\test.txt" -t invoice -o 12345
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### From Harbor/Clipper
|
|
||||||
|
|
||||||
Replace your current Rust CLI calls with:
|
|
||||||
|
|
||||||
```
|
|
||||||
PrintServiceCLI.exe -f <filepath> -t <doctype> [-o <ordernumber>]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Examples**:
|
|
||||||
```
|
|
||||||
PrintServiceCLI.exe -f "C:\Captures\inv001.txt" -t invoice
|
|
||||||
PrintServiceCLI.exe -f "C:\Captures\ord002.txt" -t order -o 600005
|
|
||||||
PrintServiceCLI.exe -f "C:\Captures\del003.txt" -t delivery
|
|
||||||
```
|
|
||||||
|
|
||||||
### CLI Options
|
|
||||||
|
|
||||||
- `-f, --file <path>`: Path to capture file (required)
|
|
||||||
- `-t, --type <type>`: Document type: invoice, order, delivery, etc. (required)
|
|
||||||
- `-o, --order <num>`: Optional order number
|
|
||||||
- `-h, --help`: Show help
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Document Types
|
|
||||||
|
|
||||||
Each document type in `appsettings.json` specifies:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Name": "invoice", // Document type identifier
|
|
||||||
"PrinterName": "HP LaserJet 500", // Windows printer name
|
|
||||||
"TraySequence": [ 3, 4, 1, 2 ], // Tray order (simulates carbon copy)
|
|
||||||
"FontName": "Courier New", // Font for rendering
|
|
||||||
"FontSize": 10.0, // Font size in points
|
|
||||||
"VerticalOffset": 0, // Adjust vertical positioning (pixels)
|
|
||||||
"HorizontalOffset": 0, // Adjust horizontal positioning (pixels)
|
|
||||||
"ArchiveAfterPrint": true, // Archive or delete after printing
|
|
||||||
"ArchivePath": "C:\\Archive\\Invoices", // Where to archive
|
|
||||||
"Transformations": [ // Text transformation rules
|
|
||||||
{
|
|
||||||
"Pattern": "W1DUPLICATE INVW0", // Regex pattern to find
|
|
||||||
"Replacement": "", // Replace with (empty = remove)
|
|
||||||
"Description": "Remove duplicate marker"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tray Mapping
|
|
||||||
|
|
||||||
The `TraySequence` specifies which physical printer trays to use for each page/copy:
|
|
||||||
|
|
||||||
- **Example**: `[3, 4, 1, 2]` prints:
|
|
||||||
- Page 1 → Tray 3 (e.g., Pink paper)
|
|
||||||
- Page 2 → Tray 4 (e.g., Orange paper)
|
|
||||||
- Page 3 → Tray 1 (e.g., Blue paper)
|
|
||||||
- Page 4 → Tray 2 (e.g., Green paper)
|
|
||||||
|
|
||||||
**Note**: Tray numbers may need adjustment per printer model. Use the included tray discovery tool (see Troubleshooting).
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
PrintService/
|
|
||||||
├── Models/
|
|
||||||
│ ├── PrintJob.cs - Print job data model
|
|
||||||
│ ├── DocumentConfig.cs - Document type configuration
|
|
||||||
│ └── AppSettings.cs - Application settings
|
|
||||||
├── Services/
|
|
||||||
│ ├── PrintQueueService.cs - Job queue management
|
|
||||||
│ ├── FileMonitorService.cs - File system monitoring
|
|
||||||
│ ├── PrinterService.cs - Windows printer integration
|
|
||||||
│ ├── DocumentProcessor.cs - Content transformation
|
|
||||||
│ └── IpcService.cs - Named pipe IPC server
|
|
||||||
├── Worker.cs - Main service coordinator
|
|
||||||
├── Program.cs - Service host configuration
|
|
||||||
└── appsettings.json - Configuration file
|
|
||||||
|
|
||||||
PrintServiceCLI/
|
|
||||||
└── Program.cs - Command-line interface
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Service won't start
|
|
||||||
|
|
||||||
1. Check Windows Event Viewer → Application logs
|
|
||||||
2. Verify paths in `appsettings.json` exist
|
|
||||||
3. Run with elevated privileges
|
|
||||||
|
|
||||||
### Printer not found
|
|
||||||
|
|
||||||
List installed printers:
|
|
||||||
```powershell
|
|
||||||
Get-Printer | Select-Object Name
|
|
||||||
```
|
|
||||||
|
|
||||||
Update `PrinterName` in config to match exactly.
|
|
||||||
|
|
||||||
### Wrong trays selected
|
|
||||||
|
|
||||||
Tray mapping varies by printer model. To discover available trays, temporarily add logging to `PrinterService.ListPaperSources()` and check service logs.
|
|
||||||
|
|
||||||
### Files being overwritten
|
|
||||||
|
|
||||||
Ensure Harbor/Clipper is calling the CLI (not writing directly to Captures folder). The service immediately moves files to prevent timestamp collisions.
|
|
||||||
|
|
||||||
### Print order issues
|
|
||||||
|
|
||||||
Check `queue_state.json` - jobs are processed FIFO. If order is wrong, check file timestamps.
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
### Service Status
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Get-Service "LAAPC Print Service"
|
|
||||||
sc.exe query "LAAPC Print Service"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
Check Windows Event Viewer or configure file logging in `appsettings.json`.
|
|
||||||
|
|
||||||
### Queue State
|
|
||||||
|
|
||||||
Inspect `queue_state.json` (location specified in `appsettings.json`) to see pending jobs.
|
|
||||||
|
|
||||||
### Folders
|
|
||||||
|
|
||||||
- `Queue/` - Files being processed (GUID-named)
|
|
||||||
- `Archive/` - Completed jobs (if archiving enabled)
|
|
||||||
- `Errors/` - Failed jobs after max retries
|
|
||||||
|
|
||||||
## Uninstall
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Run as Administrator
|
|
||||||
sc.exe stop "LAAPC Print Service"
|
|
||||||
sc.exe delete "LAAPC Print Service"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Build for debugging
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet build -c Debug
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run service locally (not as Windows Service)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet run --project PrintService/PrintService.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
### Watch mode (auto-rebuild on changes)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet watch run --project PrintService/PrintService.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
### VS Code Tasks
|
|
||||||
|
|
||||||
Use Command Palette (`Ctrl+Shift+P`) → "Tasks: Run Task":
|
|
||||||
- `build-all-x86` - Build release for 32-bit
|
|
||||||
- `publish-all-x86` - Create deployment package
|
|
||||||
- `run-service` - Run service locally for testing
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
### IPC Protocol
|
|
||||||
|
|
||||||
The CLI communicates with the service via Windows Named Pipes (`\\.\pipe\PrintServicePipe`):
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"FilePath": "C:\\Captures\\file.txt",
|
|
||||||
"DocumentType": "invoice",
|
|
||||||
"OrderNumber": "12345"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Success": true,
|
|
||||||
"Message": "Job {guid} queued successfully"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Print Queue Integration
|
|
||||||
|
|
||||||
Uses `System.Drawing.Printing.PrintDocument` with per-page `PageSettings.PaperSource` control:
|
|
||||||
|
|
||||||
- Content rendered with `Graphics.DrawString()` for pixel-perfect positioning
|
|
||||||
- Jobs go through Windows print queue (visible in Windows printer UI)
|
|
||||||
- Survives service restarts via persistent queue state
|
|
||||||
|
|
||||||
### File Safety
|
|
||||||
|
|
||||||
1. CLI sends command with file path
|
|
||||||
2. Service immediately moves file to Queue folder with GUID name
|
|
||||||
3. Original filename preserved in job metadata
|
|
||||||
4. Prevents timestamp collision overwrites
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[Your License Here]
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues or questions, contact [Your Contact Info]
|
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
# Rust Content Transformation Flows
|
|
||||||
|
|
||||||
This document describes the content transformation steps performed by the Rust CLI (`cgwprint`) that need to be ported to the C# PrintService.
|
|
||||||
|
|
||||||
## Configuration Overview
|
|
||||||
|
|
||||||
Each job type is defined in `printers.json` under the `process` section with the following possible properties:
|
|
||||||
|
|
||||||
- **label**: Display name for the job type
|
|
||||||
- **line_padding**: Left margin/indent for all lines (in units)
|
|
||||||
- **remove_ord_num**: Remove both "Order #: " label and the order number entirely
|
|
||||||
- **remove_ord_num_label**: Remove "Order #: " label but keep the order number (and make it bold)
|
|
||||||
- **indent_ord_num**: Add spacing before the order number (shifts it right)
|
|
||||||
- **row_shift**: Dictionary of line numbers → vertical adjustment (in 1/100 inch units)
|
|
||||||
- **row_trim**: Dictionary of line numbers → {start, end} (removes characters from start to end)
|
|
||||||
- **printer**: Array of printer/tray assignments
|
|
||||||
|
|
||||||
## Universal Transformation Flow (All Job Types)
|
|
||||||
|
|
||||||
Every document goes through this sequence:
|
|
||||||
|
|
||||||
### Step 1: File Reading & Page Splitting
|
|
||||||
- Read entire file content as string
|
|
||||||
- Split content by **form-feed character** (`\x0C` / ASCII 12)
|
|
||||||
- Filter out empty pages
|
|
||||||
- Each page becomes an array of lines
|
|
||||||
|
|
||||||
### Step 2: Font Code Normalization (Pre-processing)
|
|
||||||
- Prefix entire content with `\x1Bw0` (ensure normal font at start)
|
|
||||||
- Replace `\x1BW1` → `\x1Bw1` (normalize uppercase W to lowercase)
|
|
||||||
- Replace `\x1BW0` → `\x1Bw0` (normalize uppercase W to lowercase)
|
|
||||||
- Deduplicate consecutive `\x1Bw1` sequences → single `\x1Bw1`
|
|
||||||
- Deduplicate consecutive `\x1Bw0` sequences → single `\x1Bw0`
|
|
||||||
|
|
||||||
### Step 3: Order Number Manipulation (If order number provided)
|
|
||||||
**Note:** Only ONE of these operations executes per job based on configuration flags.
|
|
||||||
|
|
||||||
**3a. Remove Order Number Label** (if `remove_ord_num_label: true`)
|
|
||||||
- Find: `"Order #: {ordnum}"`
|
|
||||||
- Replace with: `" \x1Bw1{ordnum}\x1Bw0"` (spaces equal to "Order #: " length + bold order number)
|
|
||||||
|
|
||||||
**3b. Remove Order Number Completely** (if `remove_ord_num: true`)
|
|
||||||
- Find: `"Order #: {ordnum}"`
|
|
||||||
- Replace with: spaces equal to the entire string length
|
|
||||||
|
|
||||||
**3c. Indent Order Number** (if `indent_ord_num` is not empty)
|
|
||||||
- Find: `\x1Bw1{ordnum}\x1Bw0` (regex search)
|
|
||||||
- Replace with: `{indent_ord_num}\x1Bw1{ordnum}\x1Bw0`
|
|
||||||
|
|
||||||
### Step 4: Font Code Replacement
|
|
||||||
Replace ESC sequences with actual PCL font commands from printer configuration:
|
|
||||||
|
|
||||||
- `\x1Bw1` → Bold font PCL code (e.g., `\x1B(0N\x1B(s0p5h0s3b4099T`)
|
|
||||||
- `\x1Bw0` → Normal font PCL code (e.g., `\x1B(0N\x1B(s0p10h0s0b4099T`)
|
|
||||||
|
|
||||||
### Step 5: Process All Hex Escapes
|
|
||||||
Convert remaining `\xHH` escape sequences to actual bytes throughout content.
|
|
||||||
|
|
||||||
### Step 6: Page Layout Construction
|
|
||||||
For each original page, for each assigned tray:
|
|
||||||
|
|
||||||
**6a. Tray Selection**
|
|
||||||
- Insert PCL tray selection code (e.g., `\x1B&l4H` for tray 1)
|
|
||||||
|
|
||||||
**6b. Line-by-Line Rendering**
|
|
||||||
For each line (index = `pos_v`) in the page:
|
|
||||||
|
|
||||||
1. **Check for Row Shift** (Vertical positioning adjustment)
|
|
||||||
- If `row_shift["{pos_v}"]` exists, add adjustment to cumulative `line_spacing`
|
|
||||||
- Example: `row_shift["7"]: -200` means line 7 moves UP 200 units (negative = up, positive = down)
|
|
||||||
- This allows overlaying text or changing vertical line order
|
|
||||||
|
|
||||||
2. **Check for Row Trim** (Horizontal character manipulation)
|
|
||||||
- If `row_trim["{pos_v}"]` exists:
|
|
||||||
- Extract substring: `line[0..start] + line[end..]` (removes characters from start to end)
|
|
||||||
- Use trimmed line for rendering
|
|
||||||
- Example: `row_trim["8"]: {start: 0, end: 65}` removes first 65 characters
|
|
||||||
- This is used to add/remove horizontal spacing or adjust text positions on a line
|
|
||||||
|
|
||||||
3. **Position Cursor & Write Line**
|
|
||||||
- Insert PCL positioning: `\x1B&a{line_padding}h{vertical_position}V`
|
|
||||||
- `vertical_position = (pos_v × spacing) + line_spacing`
|
|
||||||
- `spacing` = printer's spacing value (100 or 150 = 1/100 inch per line)
|
|
||||||
- Append the line content
|
|
||||||
**Note:** The Rust implementation wraps content with PCL/PJL commands because it sends raw data via TCP socket. The C# implementation uses Windows printer drivers, so **most of these commands may not be needed**. We'll need to test what's actually required.
|
|
||||||
|
|
||||||
**7a. Header (based on PCL version from printer config) - MAY NOT BE NEEDED IN C#**
|
|
||||||
```
|
|
||||||
\x1B%-12345X // PCL mode enter
|
|
||||||
@PJL ENTER LANGUAGE=PCL6 // PJL language (if PCL 6)
|
|
||||||
@PJL SET RENDERMODE=GRAYSCALE // PJL grayscale (if PCL 6)
|
|
||||||
@PJL SET RESOLUTION=600 // PJL resolution (if PCL 6)
|
|
||||||
\x1B&l0O // Portrait orientation (if PCL 6)
|
|
||||||
{content here}
|
|
||||||
```
|
|
||||||
|
|
||||||
**7b. Footer - MAY NOT BE NEEDED IN C#**
|
|
||||||
```
|
|
||||||
\x1B%-12345X // PCL mode exit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 8: Send to Printer
|
|
||||||
**Rust approach:** Raw TCP socket to printer IP:port 9100
|
|
||||||
**C# approach:** Windows PrintDocument API with Graphics renderingket to printer IP:port (typically 9100)
|
|
||||||
- Send raw bytes
|
|
||||||
- Close socket
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Job Type Specific Examples
|
|
||||||
|
|
||||||
### **DELIVERY**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"label": "Delivery",
|
|
||||||
"indent_ord_num": " ", // Shift order number right 4 spaces
|
|
||||||
"line_padding": 10, // 10 units left margin
|
|
||||||
"printer": [{
|
|
||||||
"name": "HL-L6415DW",
|
|
||||||
"tray": [3, 2] // Print to tray 3, then tray 2
|
|
||||||
}],
|
|
||||||
"row_shift": {
|
|
||||||
"0": 625, // Move line 0 down 625 units
|
|
||||||
"7": -200, // Move line 7 up 200 units
|
|
||||||
"8": -100, // Move line 8 up 100 units
|
|
||||||
"14": 100, // Move line 14 down 100 units
|
|
||||||
"19": 172 // Move line 19 down 172 units
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Transformations Applied:**
|
|
||||||
- Order number gets 4 spaces prepended
|
|
||||||
- Specific lines get vertical position adjustments
|
|
||||||
- Each page prints twice (tray 3, then tray 2)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **INVOICE**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"label": "Invoice",
|
|
||||||
"remove_ord_num": true, // Remove "Order #: 12345" entirely
|
|
||||||
"line_padding": 10,
|
|
||||||
"printer": [
|
|
||||||
{
|
|
||||||
"name": "HL-L6415DW",
|
|
||||||
"tray": [2, 4, 5] // 3 copies on different trays
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "SAVIN-100",
|
|
||||||
"tray": [1] // Additional copy on different printer
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"row_shift": {
|
|
||||||
"0": 625,
|
|
||||||
"7": -200,
|
|
||||||
"8": -100,
|
|
||||||
"14": 100,
|
|
||||||
"19": 172
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Transformations Applied:**
|
|
||||||
- "Order #: 12345" → " " (spaces)
|
|
||||||
- Same row shifts as delivery
|
|
||||||
- Prints 4 times total: HL-L6415DW tray 2, 4, 5, then SAVIN tray 1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **PRE-BILL**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"label": "Pre-bill",
|
|
||||||
"remove_ord_num": false,
|
|
||||||
"remove_ord_num_label": true, // Keep number but remove label
|
|
||||||
"indent_ord_num": "",
|
|
||||||
"line_padding": 4,
|
|
||||||
"printer": [{
|
|
||||||
"name": "HP-LJP4001",
|
|
||||||
"tray": [1]
|
|
||||||
}],
|
|
||||||
"row_shift": {
|
|
||||||
"8": -2000, // Major upward shift for line 8
|
|
||||||
"12": 1550 // Major downward shift for line 12
|
|
||||||
},
|
|
||||||
"row_trim": {
|
|
||||||
"8": {
|
|
||||||
"start": 0,
|
|
||||||
"end": 65 // Remove chars 0-65 from line 8
|
|
||||||
},
|
|
||||||
"12": {
|
|
||||||
"start": 0,
|
|
||||||
"end": 6 // Remove chars 0-6 from line 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Transformations Applied:**
|
|
||||||
- "Order #: 12345" → " **12345**" (bold number, no label)
|
|
||||||
- Line 8: Characters 0-65 removed, positioned -2000 units (major upward shift)
|
|
||||||
- Line 12: Characters 0-6 removed, positioned +1550 units (major downward shift)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **PRODUCTION** / **ORDERDESK** / **BACKORDER** / **GOLDEN** / etc.
|
|
||||||
Similar patterns with variations in:
|
|
||||||
- Order number handling
|
|
||||||
- Line padding amounts
|
|
||||||
- Tray assignments
|
|
||||||
- Row shift values (some have none)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
### Coordinate System
|
|
||||||
- Horizontal: `\x1B&a{H}h{V}V` where H = horizontal position (1/300 inch), V = vertical (1/300 inch)
|
|
||||||
- `line_padding` = left margin in 1/300 inch units
|
|
||||||
- `spacing` = line height in 1/100 inch units (100 or 150)
|
|
||||||
- `row_shift` = vertical adjustment in 1/100 inch units
|
|
||||||
|
|
||||||
### Font Codes
|
|
||||||
Fonts are printer-specific PCL sequences. Example for HL-L6415DW:
|
|
||||||
- **Normal**: `\x1B(0N\x1B(s0p10h0s0b4099T`
|
|
||||||
- **Bold**: `\x1B(0N\x1B(s0p5h0s3b4099T`
|
|
||||||
|
|
||||||
### Tray Codes
|
|
||||||
Tray selection uses PCL commands. Example for HL-L6415DW:
|
|
||||||
- Tray 1: `\x1B&l4H`
|
|
||||||
- Tray 2: `\x1B&l5H`
|
|
||||||
- Tray 3: `\x1B&l8H`
|
|
||||||
- Tray 4: `\x1B&l9H`
|
|
||||||
- Tray 5: `\x1B&l10H`
|
|
||||||
|
|
||||||
### Multi-Printer Jobs
|
|
||||||
When multiple printers are specified (like Invoice), the entire transformation process runs separately for each printer with its own font codes, tray codes, and PCL version.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Questions for Review
|
|
||||||
|
|
||||||
1. Answers to Review Questions
|
|
||||||
|
|
||||||
1. **Order of Operations**: ✅ Only ONE order number manipulation happens per job based on flags, so order doesn't matter.
|
|
||||||
|
|
||||||
2. **Row Shift vs Row Trim**: ✅ Independent operations:
|
|
||||||
- **Row Shift**: Vertical movement (up/down on page) - changes line order or overlays
|
|
||||||
- **Row Trim**: Horizontal character manipulation (add/remove spacing or characters)
|
|
||||||
|
|
||||||
3. **Tray Duplication Pattern**: ✅ Current C# approach is CORRECT:
|
|
||||||
- Page 1 → Tray 1, Tray 4, Tray 5, Tray 2
|
|
||||||
- Page 2 → Tray 1, Tray 4, Tray 5, Tray 2
|
|
||||||
- This maintains carbon copy layer order
|
|
||||||
- **Future consideration**: Add optional flag to change tray ordering per job type
|
|
||||||
|
|
||||||
4. **Empty Row Shift/Trim**: ✅ Skip if not present in configuration.
|
|
||||||
|
|
||||||
5. **Hex Escape Timing**: ⚠️ May not be needed in C# since we use Windows drivers instead of raw PCL. Test to determine what's actually required.
|
|
||||||
|
|
||||||
6. **Multiple Printers**: ✅ Either sequential or parallel is fine, just maintain page order within each printer.
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
# LAAPC Print Service - Remaining Work
|
|
||||||
|
|
||||||
## ✅ Completed
|
|
||||||
- [x] Core service architecture (queue, IPC, file monitoring)
|
|
||||||
- [x] CLI with self-installation capability
|
|
||||||
- [x] Basic printing with tray control
|
|
||||||
- [x] PDF output for testing
|
|
||||||
- [x] System tray application with status display
|
|
||||||
- [x] Configuration dialog for printer/tray assignments
|
|
||||||
- [x] Fixed System.Drawing.Common platform compatibility
|
|
||||||
|
|
||||||
## 🅿️ Parking Lot - Future Enhancements
|
|
||||||
|
|
||||||
### Configuration Dialog Issues (Non-Critical)
|
|
||||||
- [ ] **Printer resets on add page:** When adding a new page to a document type, the printer dropdown resets to the first printer in the list if the configuration hasn't been saved yet
|
|
||||||
- Expected: Should remember previous page's printer selection or default intelligently
|
|
||||||
- Workaround: Save after configuring each page
|
|
||||||
|
|
||||||
- [ ] **Save closes dialog:** Clicking Save closes the configuration dialog, forcing users to reopen it to continue editing
|
|
||||||
- Expected: Save button should persist changes but keep dialog open
|
|
||||||
- Suggested: Add "Save & Close" and "Save" as separate buttons, or just make Save not close the dialog
|
|
||||||
|
|
||||||
### Printing Enhancements
|
|
||||||
- [ ] **Skip blank pages:** Add option to skip printing pages with no content
|
|
||||||
- Use case: Save paper/toner when certain pages in a document are empty
|
|
||||||
- Implementation: Check if page has content before setting `HasMorePages = true`
|
|
||||||
- Could be a global setting or per-document-type configuration
|
|
||||||
|
|
||||||
## 🔲 Immediate Testing
|
|
||||||
|
|
||||||
### Test Configuration Dialog
|
|
||||||
- [ ] Right-click tray icon → "Configure Printers..."
|
|
||||||
- [ ] Create new document type
|
|
||||||
- [ ] Add pages with printer/tray assignments
|
|
||||||
- [ ] Save configuration (test both local and global)
|
|
||||||
- [ ] Verify printer-config.json file created correctly
|
|
||||||
- [ ] Test loading existing configuration
|
|
||||||
|
|
||||||
## 🔨 High Priority - Service Integration
|
|
||||||
|
|
||||||
### 1. Integrate printer-config.json into PrintService
|
|
||||||
**Goal:** Service should read printer-config.json instead of appsettings.json
|
|
||||||
|
|
||||||
**Files to modify:**
|
|
||||||
- `PrintService/Services/ConfigurationService.cs` (CREATE NEW)
|
|
||||||
- Load printer-config.json from C:\ProgramData\LAAPC\ (global) and %LOCALAPPDATA%\LAAPC\ (local)
|
|
||||||
- Merge local over global (same logic as tray app)
|
|
||||||
- Return `PrinterConfiguration` object
|
|
||||||
|
|
||||||
- `PrintService/Models/PrinterConfig.cs` (COPY from PrintServiceTray)
|
|
||||||
- Copy the new config models into PrintService project
|
|
||||||
|
|
||||||
- `PrintService/Worker.cs` (MODIFY)
|
|
||||||
- Replace DocumentConfig loading with PrinterConfiguration loading
|
|
||||||
- Pass PageConfig[] to DocumentProcessor
|
|
||||||
|
|
||||||
- `PrintService/Services/DocumentProcessor.cs` (MODIFY - or DELETE if not needed)
|
|
||||||
- Update to work with new PageConfig format
|
|
||||||
- May need to refactor logic
|
|
||||||
|
|
||||||
- `PrintService/Services/PrinterService.cs` (MODIFY)
|
|
||||||
- Update Print() method signature to accept PageConfig[]
|
|
||||||
- Print each page to correct printer/tray based on PageConfig
|
|
||||||
|
|
||||||
**Notes:**
|
|
||||||
- Keep appsettings.json for service-level settings (queue path, retry counts, etc.)
|
|
||||||
- printer-config.json ONLY for document type → printer/tray mappings
|
|
||||||
- Support missing config gracefully (log warning, skip job)
|
|
||||||
|
|
||||||
## 🔨 High Priority - Page Duplication Pattern
|
|
||||||
|
|
||||||
### 2. Implement Multi-Tray Page Duplication
|
|
||||||
**User Requirement:** Print EACH original page to ALL trays in sequence
|
|
||||||
|
|
||||||
**Current Behavior:**
|
|
||||||
```
|
|
||||||
TraySequence: [3,4,1,2]
|
|
||||||
Original document: 4 pages
|
|
||||||
Output: 4 pages (page 1→tray3, page 2→tray4, page 3→tray1, page 4→tray2)
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Behavior:**
|
|
||||||
```
|
|
||||||
TraySequence: [3,4,1,2]
|
|
||||||
Original document: 4 pages
|
|
||||||
Output: 16 pages
|
|
||||||
Page 1 → Tray 3
|
|
||||||
Page 1 → Tray 4
|
|
||||||
Page 1 → Tray 1
|
|
||||||
Page 1 → Tray 2
|
|
||||||
Page 2 → Tray 3
|
|
||||||
Page 2 → Tray 4
|
|
||||||
Page 2 → Tray 1
|
|
||||||
Page 2 → Tray 2
|
|
||||||
... (and so on)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Files to modify:**
|
|
||||||
- `PrintService/Services/PrinterService.cs`
|
|
||||||
- Modify PrintPage event handler
|
|
||||||
- Add page duplication logic
|
|
||||||
- Track: originalPageIndex, trayIndex, currentOutputPage
|
|
||||||
- For each original page, cycle through all trays before moving to next page
|
|
||||||
|
|
||||||
**Algorithm:**
|
|
||||||
```csharp
|
|
||||||
int originalPageIndex = 0;
|
|
||||||
int trayIndex = 0;
|
|
||||||
int totalOriginalPages = CalculateTotalPages(lines);
|
|
||||||
bool hasMorePages = true;
|
|
||||||
|
|
||||||
PrintPage event:
|
|
||||||
1. Render lines for originalPageIndex
|
|
||||||
2. Set tray to pageConfigs[originalPageIndex].TrayNumber
|
|
||||||
3. Increment trayIndex
|
|
||||||
4. If trayIndex >= trays.Length:
|
|
||||||
trayIndex = 0
|
|
||||||
originalPageIndex++
|
|
||||||
5. hasMorePages = (originalPageIndex < totalOriginalPages)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔨 Medium Priority - Content Modifications
|
|
||||||
|
|
||||||
### 3. Port Rust Content Transformation Logic
|
|
||||||
**Reference:** `RUST/cgwprint/src/background.rs` and `RUST/cgwprint/data/printers.json`
|
|
||||||
|
|
||||||
**DO NOT START UNTIL:** Tray config and page duplication are complete and tested
|
|
||||||
|
|
||||||
**New config fields needed in PageConfig:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"PageNumber": 1,
|
|
||||||
"PrinterName": "Brother HL-L6415DW",
|
|
||||||
"TrayNumber": 3,
|
|
||||||
"TrayLabel": "Pink",
|
|
||||||
|
|
||||||
// NEW FIELDS:
|
|
||||||
"RemoveOrderNumber": false,
|
|
||||||
"RemoveOrderNumberLabel": false,
|
|
||||||
"IndentOrderNumber": 0,
|
|
||||||
"RowShift": {
|
|
||||||
"5": -2, // Move line 5 up by 2 positions
|
|
||||||
"12": 1 // Move line 12 down by 1 position
|
|
||||||
},
|
|
||||||
"RowTrim": {
|
|
||||||
"3": { "Start": 0, "End": 40 }, // Keep only first 40 chars of line 3
|
|
||||||
"7": { "Start": 10, "End": 50 } // Keep chars 10-50 of line 7
|
|
||||||
},
|
|
||||||
"BoldFont": "Courier New Bold",
|
|
||||||
"NormalFont": "Courier New"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Processing steps:**
|
|
||||||
1. Parse hex escapes (\xHH → bytes)
|
|
||||||
2. Split on form feed (\x0C) into pages
|
|
||||||
3. Font replacement (ESC+w+1 → bold, ESC+w+0 → normal)
|
|
||||||
4. Order number manipulation (remove/indent based on config)
|
|
||||||
5. Apply row_shift (vertical position adjustments)
|
|
||||||
6. Apply row_trim (substring extraction)
|
|
||||||
7. Add PCL/PJL header/footer wrapping
|
|
||||||
|
|
||||||
**Files to modify:**
|
|
||||||
- `PrintService/Models/PageConfig.cs` - Add new properties
|
|
||||||
- `PrintService/Services/DocumentProcessor.cs` - Add content transformation
|
|
||||||
- `PrintService/Services/ContentTransformer.cs` (CREATE NEW)
|
|
||||||
- ParseHexEscapes()
|
|
||||||
- SplitPages()
|
|
||||||
- ProcessFontCodes()
|
|
||||||
- ManipulateOrderNumber()
|
|
||||||
- ApplyRowShift()
|
|
||||||
- ApplyRowTrim()
|
|
||||||
- WrapWithPclHeaders()
|
|
||||||
|
|
||||||
**Location-specific variations:**
|
|
||||||
- "there are 3 locations where this app is used and each has a slight different layout"
|
|
||||||
- Use document type naming to differentiate: "Invoice_Location1", "Invoice_Location2", etc.
|
|
||||||
- Or add "Location" field to config and filter by it
|
|
||||||
|
|
||||||
## 📋 Testing Checklist
|
|
||||||
|
|
||||||
### Before Production Deployment
|
|
||||||
- [ ] Test service installation on clean 32-bit Windows 10 machine
|
|
||||||
- [ ] Test CLI from Harbor/Clipper integration
|
|
||||||
- [ ] Test rapid concurrent print requests (race condition prevention)
|
|
||||||
- [ ] Test multi-tray printing on Brother HL-L6415DW
|
|
||||||
- [ ] Test configuration changes without service restart
|
|
||||||
- [ ] Test queue persistence (stop service mid-job, restart, verify resume)
|
|
||||||
- [ ] Test error handling (printer offline, invalid tray, etc.)
|
|
||||||
- [ ] Test tray app startup on Windows boot (add to Startup folder?)
|
|
||||||
- [ ] Verify file cleanup after successful print
|
|
||||||
- [ ] Verify error folder gets populated on failures
|
|
||||||
|
|
||||||
## 🎯 Future Enhancements (Low Priority)
|
|
||||||
- [ ] Add logging to file for tray app (currently only service logs)
|
|
||||||
- [ ] Add "Start/Stop Service" option in tray menu
|
|
||||||
- [ ] Add "View Logs" option in tray menu
|
|
||||||
- [ ] Add notification sound for completed jobs
|
|
||||||
- [ ] Add job history view (last 50 jobs)
|
|
||||||
- [ ] Support per-page margins configuration
|
|
||||||
- [ ] Support custom font sizes per document type
|
|
||||||
- [ ] Add web UI for remote monitoring
|
|
||||||
|
|
||||||
## 📝 Important Notes
|
|
||||||
|
|
||||||
### Platform Compatibility
|
|
||||||
- **System.Drawing.Common:** Requires RuntimeHostConfigurationOption in .csproj
|
|
||||||
- **Remove explicit package reference** to avoid startup errors
|
|
||||||
- WinForms apps automatically include System.Drawing on Windows
|
|
||||||
|
|
||||||
### Configuration Locations
|
|
||||||
- **Global:** `C:\ProgramData\LAAPC\printer-config.json` (shared, requires admin)
|
|
||||||
- **Local:** `%LOCALAPPDATA%\LAAPC\printer-config.json` (per-user, no admin)
|
|
||||||
- **Merge strategy:** Local settings override global settings by document type
|
|
||||||
|
|
||||||
### Print Service Config
|
|
||||||
- Service currently uses: `appsettings.json` (old format with DocumentConfig)
|
|
||||||
- Service needs to migrate to: `printer-config.json` (new format with PageConfig[])
|
|
||||||
- Keep `appsettings.json` for non-printer settings (QueuePath, MaxRetries, etc.)
|
|
||||||
|
|
||||||
### Page Duplication Logic
|
|
||||||
- Current: One page per tray (simple mapping)
|
|
||||||
- Required: All pages to all trays (page duplication with tray cycling)
|
|
||||||
- Example: 4-page invoice × 5 trays = 20 physical pages printed
|
|
||||||
- Reason: Simulates carbon copy paper (each page needs copy in each color)
|
|
||||||
|
|
||||||
### Content Modifications
|
|
||||||
- Do LAST (most complex, least critical for initial deployment)
|
|
||||||
- Three locations have slightly different requirements
|
|
||||||
- May need per-location configuration or document type naming convention
|
|
||||||
- PCL/PJL wrapping may only apply to specific printers
|
|
||||||
|
|
||||||
### Git Status
|
|
||||||
- Last commit: cce9248
|
|
||||||
- `RUST/` folder excluded from version control
|
|
||||||
- Remember to commit after each major milestone
|
|
||||||
|
|
||||||
## 🚀 Next Steps (In Order)
|
|
||||||
1. **RIGHT NOW:** Test configuration dialog in tray app
|
|
||||||
2. Integrate printer-config.json into service
|
|
||||||
3. Implement page duplication pattern
|
|
||||||
4. Test on production hardware (Brother HL-L6415DW)
|
|
||||||
5. Deploy to first location
|
|
||||||
6. Gather feedback
|
|
||||||
7. Port content modifications from Rust
|
|
||||||
8. Deploy to remaining two locations
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Drawing.Printing;
|
|
||||||
|
|
||||||
class TrayTest
|
|
||||||
{
|
|
||||||
static void Main()
|
|
||||||
{
|
|
||||||
var printerName = "Brother HL-L6415DW series Printer";
|
|
||||||
var trayNumbers = new int[] { 1, 2, 256, 257, 270 }; // Tray 1, 2, 3, 4, 5
|
|
||||||
var currentPage = 0;
|
|
||||||
|
|
||||||
var printDoc = new PrintDocument
|
|
||||||
{
|
|
||||||
PrinterSettings = {
|
|
||||||
PrinterName = printerName,
|
|
||||||
Duplex = Duplex.Simplex // Force single-sided
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// QueryPageSettings fires BEFORE PrintPage - this is where we set the tray
|
|
||||||
printDoc.QueryPageSettings += (sender, e) =>
|
|
||||||
{
|
|
||||||
if (e.PageSettings == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var trayNumber = trayNumbers[currentPage];
|
|
||||||
|
|
||||||
// Set the tray for this page
|
|
||||||
PaperSource? paperSource = null;
|
|
||||||
foreach (PaperSource source in printDoc.PrinterSettings.PaperSources)
|
|
||||||
{
|
|
||||||
if (source.RawKind == trayNumber)
|
|
||||||
{
|
|
||||||
paperSource = source;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paperSource != null)
|
|
||||||
{
|
|
||||||
e.PageSettings.PaperSource = paperSource;
|
|
||||||
Console.WriteLine($"Page {currentPage + 1}: Using tray {trayNumber} ({paperSource.SourceName})");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Page {currentPage + 1}: WARNING - Tray {trayNumber} not found!");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
printDoc.PrintPage += (sender, e) =>
|
|
||||||
{
|
|
||||||
if (e.Graphics == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Draw the page number
|
|
||||||
var font = new Font("Courier New", 24, FontStyle.Bold);
|
|
||||||
var text = $"Page #{currentPage + 1}";
|
|
||||||
e.Graphics.DrawString(text, font, Brushes.Black, 100, 100);
|
|
||||||
|
|
||||||
currentPage++;
|
|
||||||
e.HasMorePages = currentPage < trayNumbers.Length;
|
|
||||||
};
|
|
||||||
|
|
||||||
Console.WriteLine($"Printing 5 pages to {printerName}...");
|
|
||||||
printDoc.Print();
|
|
||||||
Console.WriteLine("Done!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
{
|
|
||||||
"DocumentTypes": {
|
|
||||||
"test": {
|
|
||||||
"Name": "test",
|
|
||||||
"Pages": [
|
|
||||||
{
|
|
||||||
"PageNumber": 1,
|
|
||||||
"PrinterName": "Microsoft Print to PDF",
|
|
||||||
"TrayNumber": 1,
|
|
||||||
"TrayLabel": "Default"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"FontName": "Courier New",
|
|
||||||
"FontSize": 10.0,
|
|
||||||
"HorizontalOffset": 0,
|
|
||||||
"VerticalOffset": 0,
|
|
||||||
"ArchiveAfterPrint": true,
|
|
||||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Test",
|
|
||||||
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
|
|
||||||
"SkipPostProcessing": false,
|
|
||||||
"Transformations": [],
|
|
||||||
"LinePadding": 10,
|
|
||||||
"RemoveOrderNumber": false,
|
|
||||||
"RemoveOrderNumberLabel": true,
|
|
||||||
"IndentOrderNumber": "",
|
|
||||||
"RowShift": {
|
|
||||||
"7": -200,
|
|
||||||
"8": -100
|
|
||||||
},
|
|
||||||
"RowTrim": {
|
|
||||||
"8": {
|
|
||||||
"Start": 0,
|
|
||||||
"End": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delivery": {
|
|
||||||
"Name": "delivery",
|
|
||||||
"Pages": [
|
|
||||||
{
|
|
||||||
"PageNumber": 1,
|
|
||||||
"PrinterName": "Microsoft Print to PDF",
|
|
||||||
"TrayNumber": 3,
|
|
||||||
"TrayLabel": "Tray 3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"PageNumber": 2,
|
|
||||||
"PrinterName": "Microsoft Print to PDF",
|
|
||||||
"TrayNumber": 2,
|
|
||||||
"TrayLabel": "Tray 2"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"FontName": "Courier New",
|
|
||||||
"FontSize": 10.0,
|
|
||||||
"HorizontalOffset": 0,
|
|
||||||
"VerticalOffset": 0,
|
|
||||||
"ArchiveAfterPrint": true,
|
|
||||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Orders",
|
|
||||||
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
|
|
||||||
"SkipPostProcessing": false,
|
|
||||||
"Transformations": [],
|
|
||||||
"LinePadding": 10,
|
|
||||||
"RemoveOrderNumber": false,
|
|
||||||
"RemoveOrderNumberLabel": false,
|
|
||||||
"IndentOrderNumber": " ",
|
|
||||||
"RowShift": {
|
|
||||||
"0": 625,
|
|
||||||
"7": -200,
|
|
||||||
"8": -100,
|
|
||||||
"14": 100,
|
|
||||||
"19": 172
|
|
||||||
},
|
|
||||||
"RowTrim": {}
|
|
||||||
},
|
|
||||||
"invoice": {
|
|
||||||
"Name": "invoice",
|
|
||||||
"Pages": [
|
|
||||||
{
|
|
||||||
"PageNumber": 1,
|
|
||||||
"PrinterName": "Microsoft Print to PDF",
|
|
||||||
"TrayNumber": 2,
|
|
||||||
"TrayLabel": "White"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"PageNumber": 2,
|
|
||||||
"PrinterName": "Microsoft Print to PDF",
|
|
||||||
"TrayNumber": 4,
|
|
||||||
"TrayLabel": "Yellow"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"PageNumber": 3,
|
|
||||||
"PrinterName": "Microsoft Print to PDF",
|
|
||||||
"TrayNumber": 5,
|
|
||||||
"TrayLabel": "Pink"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"FontName": "Courier New",
|
|
||||||
"FontSize": 10.0,
|
|
||||||
"HorizontalOffset": 0,
|
|
||||||
"VerticalOffset": 0,
|
|
||||||
"ArchiveAfterPrint": true,
|
|
||||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Orders",
|
|
||||||
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
|
|
||||||
"SkipPostProcessing": false,
|
|
||||||
"Transformations": [],
|
|
||||||
"LinePadding": 10,
|
|
||||||
"RemoveOrderNumber": true,
|
|
||||||
"RemoveOrderNumberLabel": false,
|
|
||||||
"IndentOrderNumber": "",
|
|
||||||
"RowShift": {
|
|
||||||
"0": 625,
|
|
||||||
"7": -200,
|
|
||||||
"8": -100,
|
|
||||||
"14": 100,
|
|
||||||
"19": 172
|
|
||||||
},
|
|
||||||
"RowTrim": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Page 1 of 1
|
|
||||||
Reference in New Issue
Block a user