初始化
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
export type MainWorkflowMode = 'ready' | 'distributing' | 'usbCopying'
|
||||
|
||||
export const mainAppState = {
|
||||
initialized: false,
|
||||
mode: 'ready' as MainWorkflowMode,
|
||||
activeJobId: ''
|
||||
}
|
||||
|
||||
export function assertReady(): void {
|
||||
if (!mainAppState.initialized) throw new Error('NOT_INITIALIZED')
|
||||
}
|
||||
|
||||
export function assertNotBusy(): void {
|
||||
if (mainAppState.mode === 'distributing' || mainAppState.mode === 'usbCopying') {
|
||||
throw new Error('BUSY')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Store from 'electron-store'
|
||||
import { app } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
interface AppConfig {
|
||||
sharedDir: string
|
||||
templateDir: string
|
||||
/** 开发默认 true:不调用 SAPI_Init,避免无打印机时 DLL 刷错 */
|
||||
skipDllInit: boolean
|
||||
}
|
||||
|
||||
const defaultShared = path.join(app.getPath('userData'), 'Cardsoon', 'tasks')
|
||||
|
||||
export const configStore = new Store<AppConfig>({
|
||||
name: 'cardsoon-config',
|
||||
defaults: {
|
||||
sharedDir: defaultShared,
|
||||
templateDir: path.join(app.getPath('userData'), 'Cardsoon', 'templates'),
|
||||
skipDllInit: !app.isPackaged
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import { app } from 'electron'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import log from 'electron-log'
|
||||
|
||||
const RUNTIME_CONFIG_FILES = ['CapSettings.json'] as const
|
||||
const DEFAULT_PRINT_TASKS = path.join('C:', 'PrintTasks')
|
||||
|
||||
const DEFAULT_CAP_SETTINGS = `{
|
||||
"printerList": [],
|
||||
"Img": [],
|
||||
"Text": []
|
||||
}
|
||||
`
|
||||
|
||||
export function getNativeDir(): string {
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, 'native')
|
||||
}
|
||||
return path.join(app.getAppPath(), 'resources', 'native')
|
||||
}
|
||||
|
||||
export function getProcessExecDir(): string {
|
||||
return path.dirname(process.execPath)
|
||||
}
|
||||
|
||||
function ensureBundledConfig(nativeDir: string): void {
|
||||
fs.mkdirSync(nativeDir, { recursive: true })
|
||||
for (const name of RUNTIME_CONFIG_FILES) {
|
||||
const filePath = path.join(nativeDir, name)
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, DEFAULT_CAP_SETTINGS, 'utf8')
|
||||
log.info(`Created default native config: ${filePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deployConfigFile(src: string, destDir: string): void {
|
||||
const dest = path.join(destDir, path.basename(src))
|
||||
try {
|
||||
fs.mkdirSync(destDir, { recursive: true })
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.copyFileSync(src, dest)
|
||||
log.debug(`Deployed ${path.basename(src)} -> ${dest}`)
|
||||
return
|
||||
}
|
||||
const srcStat = fs.statSync(src)
|
||||
const destStat = fs.statSync(dest)
|
||||
if (srcStat.mtimeMs > destStat.mtimeMs) {
|
||||
fs.copyFileSync(src, dest)
|
||||
log.debug(`Updated ${path.basename(src)} -> ${dest}`)
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn(`Deploy ${path.basename(src)} to ${destDir} failed`, e)
|
||||
}
|
||||
}
|
||||
|
||||
function deployRuntimeConfigs(nativeDir: string): void {
|
||||
const deployDirs = new Set<string>([
|
||||
nativeDir,
|
||||
getProcessExecDir(),
|
||||
DEFAULT_PRINT_TASKS,
|
||||
path.join(app.getPath('userData'), 'Cardsoon', 'runtime')
|
||||
])
|
||||
|
||||
for (const name of RUNTIME_CONFIG_FILES) {
|
||||
const src = path.join(nativeDir, name)
|
||||
if (!fs.existsSync(src)) continue
|
||||
deployDirs.forEach((dir) => deployConfigFile(src, dir))
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(DEFAULT_PRINT_TASKS, { recursive: true })
|
||||
} catch (e) {
|
||||
log.warn(`Cannot create ${DEFAULT_PRINT_TASKS}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
export function setupNativeWorkingDir(): void {
|
||||
const nativeDir = getNativeDir()
|
||||
if (!fs.existsSync(path.join(nativeDir, 'workDll.dll'))) {
|
||||
throw new Error(`Native DLL directory not found or incomplete: ${nativeDir}`)
|
||||
}
|
||||
ensureBundledConfig(nativeDir)
|
||||
deployRuntimeConfigs(nativeDir)
|
||||
process.chdir(nativeDir)
|
||||
log.debug(`Native working directory: ${nativeDir}`)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { BrowserWindow } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { POLL_INTERVAL_MS } from '../constants'
|
||||
import { mainAppState } from './app-state'
|
||||
import { dllGetJobStateById, dllGetUsbCopyState } from './work-dll.service'
|
||||
|
||||
let jobTimer: ReturnType<typeof setInterval> | null = null
|
||||
let usbTimer: ReturnType<typeof setInterval> | null = null
|
||||
let jobId = ''
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
export function setPollMainWindow(win: BrowserWindow): void {
|
||||
mainWindow = win
|
||||
}
|
||||
|
||||
function send(channel: string, payload: unknown): void {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(channel, payload)
|
||||
}
|
||||
}
|
||||
|
||||
export function stopJobPoll(): void {
|
||||
if (jobTimer) {
|
||||
clearInterval(jobTimer)
|
||||
jobTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
export function stopUsbPoll(): void {
|
||||
if (usbTimer) {
|
||||
clearInterval(usbTimer)
|
||||
usbTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAllPolls(): void {
|
||||
stopJobPoll()
|
||||
stopUsbPoll()
|
||||
}
|
||||
|
||||
export function startJobPoll(id: string): void {
|
||||
stopJobPoll()
|
||||
jobId = id
|
||||
jobTimer = setInterval(() => {
|
||||
try {
|
||||
const r = dllGetJobStateById(jobId)
|
||||
const failed = r.jobState === 4
|
||||
const cancelled = r.jobState === 6
|
||||
const finished = r.jobState === 100
|
||||
const terminal = failed || cancelled
|
||||
send('job:poll-tick', {
|
||||
jobId,
|
||||
queryErrorCode: r.queryErrorCode,
|
||||
jobState: r.jobState,
|
||||
progress: r.progress,
|
||||
terminal,
|
||||
failed,
|
||||
cancelled,
|
||||
finished
|
||||
})
|
||||
if (r.queryErrorCode !== 0) {
|
||||
log.warn('GetJobStateById query failed', r.queryErrorCode)
|
||||
stopJobPoll()
|
||||
return
|
||||
}
|
||||
if (failed || cancelled) {
|
||||
stopJobPoll()
|
||||
if (cancelled) mainAppState.mode = 'ready'
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('job poll error', e)
|
||||
stopJobPoll()
|
||||
}
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
export function startUsbPoll(): void {
|
||||
stopUsbPoll()
|
||||
usbTimer = setInterval(() => {
|
||||
try {
|
||||
const r = dllGetUsbCopyState()
|
||||
const failed = r.taskStatus === 3
|
||||
const success = r.taskStatus === 2
|
||||
const terminal = failed || success
|
||||
send('usb:poll-tick', {
|
||||
taskStatus: r.taskStatus,
|
||||
progress: r.progress,
|
||||
terminal,
|
||||
failed,
|
||||
success
|
||||
})
|
||||
if (terminal) {
|
||||
stopUsbPoll()
|
||||
mainAppState.mode = 'ready'
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('usb poll error', e)
|
||||
stopUsbPoll()
|
||||
mainAppState.mode = 'ready'
|
||||
}
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
export function getActiveJobId(): string {
|
||||
return jobId
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import path from 'path'
|
||||
import koffi from 'koffi'
|
||||
import log from 'electron-log'
|
||||
import { CS_OK, JOB_ID_BUF_SIZE, LOG_FATAL_FLAG } from '../constants'
|
||||
import { getNativeDir } from './native-path'
|
||||
|
||||
export interface InitParams {
|
||||
sharedDir: string
|
||||
keepCombinedImage?: boolean
|
||||
stopOnFailure?: boolean
|
||||
cleanTaskFile?: boolean
|
||||
autoRetryTimes?: number
|
||||
rejectConfig?: boolean
|
||||
logLevel?: number
|
||||
outBack?: boolean
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let lib: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_Init: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_GetPrinterInfo: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_GetPrinterErrorStr: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_RestJobEx: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_GetJobStateById: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_AdminJobCancel: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_CopyFromUsb: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_GetUsbCopyState: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_PrinterResetprinter: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_PrinterMovetoreject: any = null
|
||||
let hasRejectApi = false
|
||||
let hasCancelApi = false
|
||||
let loggedCancelMissing = false
|
||||
let loggedRejectMissing = false
|
||||
|
||||
function loadLibrary(): void {
|
||||
if (lib) return
|
||||
const dllPath = path.join(getNativeDir(), 'workDll.dll')
|
||||
lib = koffi.load(dllPath)
|
||||
|
||||
SAPI_Init = lib.func('int __stdcall SAPI_Init(str, bool, bool, bool, int, bool, int, bool)')
|
||||
SAPI_GetPrinterInfo = lib.func('int __stdcall SAPI_GetPrinterInfo(_Out_ void **)')
|
||||
SAPI_GetPrinterErrorStr = lib.func('str __stdcall SAPI_GetPrinterErrorStr(int)')
|
||||
SAPI_RestJobEx = lib.func('int __stdcall SAPI_RestJobEx(str, _Out_ char *, int)')
|
||||
SAPI_GetJobStateById = lib.func(
|
||||
'int __stdcall SAPI_GetJobStateById(str, _Out_ int *, _Out_ int *)'
|
||||
)
|
||||
SAPI_CopyFromUsb = lib.func('int __stdcall SAPI_CopyFromUsb(str, int)')
|
||||
SAPI_GetUsbCopyState = lib.func('int __stdcall SAPI_GetUsbCopyState(_Out_ int *, _Out_ int *)')
|
||||
SAPI_PrinterResetprinter = lib.func('int __stdcall SAPI_PrinterResetprinter()')
|
||||
|
||||
try {
|
||||
SAPI_AdminJobCancel = lib.func('int __stdcall SAPI_AdminJobCancel(str)')
|
||||
hasCancelApi = true
|
||||
} catch {
|
||||
SAPI_AdminJobCancel = null
|
||||
hasCancelApi = false
|
||||
if (!loggedCancelMissing) {
|
||||
loggedCancelMissing = true
|
||||
log.info('SAPI_AdminJobCancel not in workDll (optional); stop uses poll-stop only')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
SAPI_PrinterMovetoreject = lib.func('int __stdcall SAPI_PrinterMovetoreject()')
|
||||
hasRejectApi = true
|
||||
} catch {
|
||||
hasRejectApi = false
|
||||
if (!loggedRejectMissing) {
|
||||
loggedRejectMissing = true
|
||||
log.info('SAPI_PrinterMovetoreject not in workDll (optional); reject card disabled')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isRejectApiAvailable(): boolean {
|
||||
loadLibrary()
|
||||
return hasRejectApi
|
||||
}
|
||||
|
||||
export function isCancelApiAvailable(): boolean {
|
||||
loadLibrary()
|
||||
return hasCancelApi
|
||||
}
|
||||
|
||||
export function dllInit(params: InitParams): number {
|
||||
loadLibrary()
|
||||
return SAPI_Init!(
|
||||
params.sharedDir,
|
||||
params.keepCombinedImage ?? true,
|
||||
params.stopOnFailure ?? false,
|
||||
params.cleanTaskFile ?? true,
|
||||
params.autoRetryTimes ?? 0,
|
||||
params.rejectConfig ?? true,
|
||||
params.logLevel ?? LOG_FATAL_FLAG,
|
||||
params.outBack ?? false
|
||||
) as number
|
||||
}
|
||||
|
||||
export function dllGetPrinterInfo(): { code: number; json?: Record<string, unknown> } {
|
||||
loadLibrary()
|
||||
const outPtr = koffi.alloc('void *', 8)
|
||||
try {
|
||||
const len = SAPI_GetPrinterInfo!(outPtr) as number
|
||||
if (len <= 0) return { code: len }
|
||||
const ptr = koffi.decode(outPtr, 0, 'void *') as number
|
||||
const jsonStr = koffi.decode(ptr, 'char', len) as string
|
||||
koffi.free(ptr)
|
||||
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
|
||||
} finally {
|
||||
koffi.free(outPtr)
|
||||
}
|
||||
}
|
||||
|
||||
export function dllGetPrinterErrorStr(errorNo = -1): string {
|
||||
loadLibrary()
|
||||
const s = SAPI_GetPrinterErrorStr!(errorNo) as string
|
||||
return s || ''
|
||||
}
|
||||
|
||||
export function dllRestJobEx(json: string): { code: number; jobId: string } {
|
||||
loadLibrary()
|
||||
const buf = Buffer.alloc(JOB_ID_BUF_SIZE)
|
||||
const code = SAPI_RestJobEx!(json, buf, JOB_ID_BUF_SIZE) as number
|
||||
const jobId = buf.toString('utf8').replace(/\0.*$/, '').trim()
|
||||
return { code, jobId }
|
||||
}
|
||||
|
||||
export function dllGetJobStateById(jobId: string): {
|
||||
queryErrorCode: number
|
||||
jobState: number
|
||||
progress: number
|
||||
} {
|
||||
loadLibrary()
|
||||
const jobState = [0]
|
||||
const copyScheduler = [0]
|
||||
const queryErrorCode = SAPI_GetJobStateById!(jobId, jobState, copyScheduler) as number
|
||||
return {
|
||||
queryErrorCode,
|
||||
jobState: jobState[0],
|
||||
progress: copyScheduler[0]
|
||||
}
|
||||
}
|
||||
|
||||
export function dllAdminJobCancel(jobId: string): number {
|
||||
loadLibrary()
|
||||
if (!SAPI_AdminJobCancel) throw new Error('CANCEL_API_UNAVAILABLE')
|
||||
return SAPI_AdminJobCancel(jobId) as number
|
||||
}
|
||||
|
||||
export function dllCopyFromUsb(destFolder: string, cardOutput: number): number {
|
||||
loadLibrary()
|
||||
return SAPI_CopyFromUsb!(destFolder, cardOutput) as number
|
||||
}
|
||||
|
||||
export function dllGetUsbCopyState(): { taskStatus: number; progress: number } {
|
||||
loadLibrary()
|
||||
const taskStatus = [0]
|
||||
const progress = [0]
|
||||
SAPI_GetUsbCopyState!(taskStatus, progress)
|
||||
return { taskStatus: taskStatus[0], progress: progress[0] }
|
||||
}
|
||||
|
||||
export function dllPrinterReset(): number {
|
||||
loadLibrary()
|
||||
return SAPI_PrinterResetprinter!() as number
|
||||
}
|
||||
|
||||
export function dllPrinterReject(): number {
|
||||
loadLibrary()
|
||||
if (!SAPI_PrinterMovetoreject) throw new Error('REJECT_API_UNAVAILABLE')
|
||||
return SAPI_PrinterMovetoreject() as number
|
||||
}
|
||||
Reference in New Issue
Block a user