Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65d8c75bbe |
+54
-85
@@ -1,85 +1,54 @@
|
|||||||
# .NET Build Outputs
|
# ---> C
|
||||||
bin/
|
# Prerequisites
|
||||||
obj/
|
*.d
|
||||||
publish/
|
|
||||||
|
# Object files
|
||||||
# Runtime Data Folders (don't commit capture files or queues)
|
*.o
|
||||||
Captures/
|
*.ko
|
||||||
Queue/
|
*.obj
|
||||||
Archive/
|
*.elf
|
||||||
Error/
|
|
||||||
Output/
|
# Linker output
|
||||||
|
*.ilk
|
||||||
# Rust Project (reference implementation)
|
*.map
|
||||||
RUST/
|
*.exp
|
||||||
|
|
||||||
# User-specific files
|
# Precompiled Headers
|
||||||
*.suo
|
*.gch
|
||||||
*.user
|
*.pch
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
# Libraries
|
||||||
|
*.lib
|
||||||
# Visual Studio Code
|
*.a
|
||||||
.vscode/*
|
*.la
|
||||||
!.vscode/tasks.json
|
*.lo
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
# Shared objects (inc. Windows DLLs)
|
||||||
|
*.dll
|
||||||
# Visual Studio
|
*.so
|
||||||
.vs/
|
*.so.*
|
||||||
*.userprefs
|
*.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/
|
# Debug files
|
||||||
[Oo]bj/
|
*.dSYM/
|
||||||
[Ll]og/
|
*.su
|
||||||
[Ll]ogs/
|
*.idb
|
||||||
|
*.pdb
|
||||||
# NuGet Packages
|
|
||||||
*.nupkg
|
# Kernel Module Compile Results
|
||||||
*.snupkg
|
*.mod*
|
||||||
**/packages/*
|
*.cmd
|
||||||
!**/packages/build/
|
.tmp_versions/
|
||||||
*.nuget.props
|
modules.order
|
||||||
*.nuget.targets
|
Module.symvers
|
||||||
project.lock.json
|
Mkfile.old
|
||||||
project.fragment.lock.json
|
dkms.conf
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# Test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
# 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(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
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
|
|
||||||
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,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,34 +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<PrintQueueService>();
|
|
||||||
services.AddSingleton<FileMonitorService>();
|
|
||||||
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,108 +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 ILogger<DocumentProcessor> _logger;
|
|
||||||
|
|
||||||
public DocumentProcessor(IOptions<AppSettings> settings, ILogger<DocumentProcessor> logger)
|
|
||||||
{
|
|
||||||
_settings = settings.Value;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process and transform document content
|
|
||||||
/// </summary>
|
|
||||||
public string ProcessDocument(string content, DocumentConfig 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
|
|
||||||
/// </summary>
|
|
||||||
public DocumentConfig? GetDocumentConfig(string documentType)
|
|
||||||
{
|
|
||||||
return _settings.DocumentTypes.FirstOrDefault(dt =>
|
|
||||||
dt.Name.Equals(documentType, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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, DocumentConfig 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,176 +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;
|
|
||||||
|
|
||||||
public PrinterService(IOptions<AppSettings> settings, ILogger<PrinterService> logger)
|
|
||||||
{
|
|
||||||
_settings = settings.Value;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Print document to specified printer with tray sequence
|
|
||||||
/// </summary>
|
|
||||||
public void Print(string content, DocumentConfig config, string originalFileName)
|
|
||||||
{
|
|
||||||
if (config.TraySequence.Length == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("No tray sequence defined for document type");
|
|
||||||
}
|
|
||||||
|
|
||||||
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
|
|
||||||
var linesPerPage = CalculateLinesPerPage(config);
|
|
||||||
var currentPageIndex = 0;
|
|
||||||
|
|
||||||
var printDoc = new PrintDocument
|
|
||||||
{
|
|
||||||
PrinterSettings = { PrinterName = config.PrinterName }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle PDF output if using Microsoft Print to PDF
|
|
||||||
if (config.PrinterName.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 printer exists
|
|
||||||
if (!PrinterSettings.InstalledPrinters.Cast<string>().Contains(config.PrinterName))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Printer '{config.PrinterName}' not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
printDoc.PrintPage += (sender, e) =>
|
|
||||||
{
|
|
||||||
if (e.Graphics == null || e.PageSettings == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Select tray for current page
|
|
||||||
var trayIndex = config.TraySequence[currentPageIndex % config.TraySequence.Length];
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Map logical tray number to PaperSource
|
|
||||||
var paperSource = GetPaperSource(printDoc.PrinterSettings, trayIndex);
|
|
||||||
if (paperSource != null)
|
|
||||||
{
|
|
||||||
e.PageSettings.PaperSource = paperSource;
|
|
||||||
if (_settings.VerboseLogging)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Page {Page}: Using tray {Tray} ({Source})",
|
|
||||||
currentPageIndex + 1, trayIndex, paperSource.SourceName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to set tray {Tray}, using default", trayIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render page content
|
|
||||||
RenderPage(e.Graphics, lines, currentPageIndex * linesPerPage, linesPerPage, config);
|
|
||||||
|
|
||||||
currentPageIndex++;
|
|
||||||
e.HasMorePages = (currentPageIndex < config.TraySequence.Length);
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogInformation("Printing to {Printer} with {Pages} pages",
|
|
||||||
config.PrinterName, config.TraySequence.Length);
|
|
||||||
|
|
||||||
printDoc.Print();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Render page content using Graphics API
|
|
||||||
/// </summary>
|
|
||||||
private void RenderPage(Graphics graphics, string[] lines, int startLine, int linesPerPage, DocumentConfig config)
|
|
||||||
{
|
|
||||||
var font = new Font(config.FontName, config.FontSize);
|
|
||||||
var brush = Brushes.Black;
|
|
||||||
var lineHeight = font.GetHeight(graphics);
|
|
||||||
|
|
||||||
var x = (float)config.HorizontalOffset;
|
|
||||||
var y = (float)config.VerticalOffset;
|
|
||||||
|
|
||||||
var endLine = Math.Min(startLine + linesPerPage, lines.Length);
|
|
||||||
|
|
||||||
for (int i = startLine; i < endLine; i++)
|
|
||||||
{
|
|
||||||
if (i < lines.Length)
|
|
||||||
{
|
|
||||||
graphics.DrawString(lines[i], font, brush, x, y);
|
|
||||||
y += lineHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculate lines per page based on font and page size
|
|
||||||
/// </summary>
|
|
||||||
private int CalculateLinesPerPage(DocumentConfig config)
|
|
||||||
{
|
|
||||||
// Estimate: standard letter size is 11 inches, at 10pt font ~66 lines
|
|
||||||
// This is simplified; in production you'd calculate based on actual page dimensions
|
|
||||||
var estimatedLineHeight = config.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
|
|
||||||
var config = _documentProcessor.GetDocumentConfig(job.DocumentType);
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"No configuration found for document type: {job.DocumentType}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
await Task.Run(() => _printerService.Print(processedContent, config, job.OriginalFilename), 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,29 +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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.
|
|
||||||
}
|
|
||||||
@@ -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,240 +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 - Configuration Dialog Issues
|
|
||||||
|
|
||||||
### Known 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
|
|
||||||
|
|
||||||
## 🔲 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 +0,0 @@
|
|||||||
[]
|
|
||||||
Reference in New Issue
Block a user