更新
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import log from 'electron-log'
|
||||
import { getProcessExecDir } from './native-path'
|
||||
|
||||
export const APP_CONFIG_FILENAME = 'cardsoon.config.json'
|
||||
|
||||
/** 与 cardsoon.config.json 键名一致,后续配置在此扩展 */
|
||||
export interface AppFileConfig {
|
||||
designAppPath: string
|
||||
}
|
||||
|
||||
const defaults: AppFileConfig = {
|
||||
designAppPath: ''
|
||||
}
|
||||
|
||||
let cached: AppFileConfig | null = null
|
||||
let loadedFrom = ''
|
||||
|
||||
function bundledConfigPath(): string {
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, APP_CONFIG_FILENAME)
|
||||
}
|
||||
return path.join(app.getAppPath(), 'resources', APP_CONFIG_FILENAME)
|
||||
}
|
||||
|
||||
function configSearchPaths(): string[] {
|
||||
const besideExe = path.join(getProcessExecDir(), APP_CONFIG_FILENAME)
|
||||
const bundled = bundledConfigPath()
|
||||
if (besideExe === bundled) return [besideExe]
|
||||
return [besideExe, bundled]
|
||||
}
|
||||
|
||||
function parseConfigFile(filePath: string): AppFileConfig {
|
||||
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>
|
||||
return {
|
||||
designAppPath: String(raw.designAppPath ?? '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
export function loadAppFileConfig(): AppFileConfig {
|
||||
if (cached) return cached
|
||||
|
||||
for (const filePath of configSearchPaths()) {
|
||||
if (!fs.existsSync(filePath)) continue
|
||||
try {
|
||||
cached = parseConfigFile(filePath)
|
||||
loadedFrom = filePath
|
||||
log.info(`Loaded ${APP_CONFIG_FILENAME} from ${filePath}`)
|
||||
return cached
|
||||
} catch (e) {
|
||||
log.warn(`Skip invalid ${APP_CONFIG_FILENAME}: ${filePath}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
cached = { ...defaults }
|
||||
loadedFrom = ''
|
||||
log.warn(
|
||||
`${APP_CONFIG_FILENAME} not found (checked: ${configSearchPaths().join(', ')}), using defaults`
|
||||
)
|
||||
return cached
|
||||
}
|
||||
|
||||
export function getAppConfigLoadedPath(): string {
|
||||
loadAppFileConfig()
|
||||
return loadedFrom
|
||||
}
|
||||
|
||||
export function getDesignAppPath(): string {
|
||||
return loadAppFileConfig().designAppPath
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
import Store from 'electron-store'
|
||||
import { app } from 'electron'
|
||||
import path from 'path'
|
||||
import type { PrinterStatusSnapshot } from '@shared/printer-info'
|
||||
|
||||
interface AppConfig {
|
||||
sharedDir: string
|
||||
templateDir: string
|
||||
/** G2 门禁 false:启动即 SAPI_Init;仅调试可改 true */
|
||||
skipDllInit: boolean
|
||||
/** true:IPC/DLL 等调用输出到 DevTools 控制台 */
|
||||
traceEnabled: boolean
|
||||
/** 上次成功的 GetPrinterInfo 解析结果,供离线/失败时展示 */
|
||||
lastPrinterStatus?: PrinterStatusSnapshot
|
||||
}
|
||||
|
||||
const defaultShared = path.join('C:', 'PrintTasks')
|
||||
@@ -17,6 +22,7 @@ export const configStore = new Store<AppConfig>({
|
||||
sharedDir: defaultShared,
|
||||
templateDir: path.join(app.getPath('userData'), 'Cardsoon', 'templates'),
|
||||
// 正式版始终 Init;仅开发时可通过 --skip-dll-init 临时跳过
|
||||
skipDllInit: false
|
||||
skipDllInit: false,
|
||||
traceEnabled: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { shell } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function validateDesignAppPath(exePath: string): { ok: true } | { ok: false; message: string } {
|
||||
const p = exePath.trim()
|
||||
if (!p) {
|
||||
return { ok: false, message: '请在 cardsoon.config.json 中配置 designAppPath' }
|
||||
}
|
||||
const resolved = path.resolve(p)
|
||||
if (!fs.existsSync(resolved)) {
|
||||
return { ok: false, message: `设计软件不存在: ${resolved}` }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/** 由系统启动外部程序;空字符串表示成功,非空为失败原因 */
|
||||
export async function openDesignApp(
|
||||
exePath: string
|
||||
): Promise<{ ok: true } | { ok: false; message: string }> {
|
||||
const check = validateDesignAppPath(exePath)
|
||||
if (!check.ok) return check
|
||||
|
||||
const target = path.resolve(exePath.trim())
|
||||
const err = await shell.openPath(target)
|
||||
if (err) {
|
||||
return { ok: false, message: err }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { BrowserWindow } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { POLL_INTERVAL_MS } from '../constants'
|
||||
import { mainAppState } from './app-state'
|
||||
import { emitTrace } from '../utils/trace-bridge'
|
||||
import { dllGetJobStateById, dllGetUsbCopyState } from './work-dll.service'
|
||||
|
||||
let jobTimer: ReturnType<typeof setInterval> | null = null
|
||||
@@ -56,7 +57,7 @@ export function startJobPoll(id: string): void {
|
||||
const cancelled = r.jobState === 6
|
||||
const finished = r.jobState === 100
|
||||
const terminal = failed || cancelled
|
||||
send('job:poll-tick', {
|
||||
const tick = {
|
||||
jobId,
|
||||
queryErrorCode: r.queryErrorCode,
|
||||
jobState: r.jobState,
|
||||
@@ -65,7 +66,9 @@ export function startJobPoll(id: string): void {
|
||||
failed,
|
||||
cancelled,
|
||||
finished
|
||||
})
|
||||
}
|
||||
emitTrace('[poll] job:poll-tick', tick)
|
||||
send('job:poll-tick', tick)
|
||||
if (r.queryErrorCode !== 0) {
|
||||
log.warn('GetJobStateById query failed', r.queryErrorCode)
|
||||
stopJobPoll(true)
|
||||
@@ -89,13 +92,15 @@ export function startUsbPoll(): void {
|
||||
const failed = r.taskStatus === 3
|
||||
const success = r.taskStatus === 2
|
||||
const terminal = failed || success
|
||||
send('usb:poll-tick', {
|
||||
const tick = {
|
||||
taskStatus: r.taskStatus,
|
||||
progress: r.progress,
|
||||
terminal,
|
||||
failed,
|
||||
success
|
||||
})
|
||||
}
|
||||
emitTrace('[poll] usb:poll-tick', tick)
|
||||
send('usb:poll-tick', tick)
|
||||
if (terminal) {
|
||||
stopUsbPoll()
|
||||
mainAppState.mode = 'ready'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { emitTrace, isTraceEnabled } from '../utils/trace-bridge'
|
||||
import { getNativeDir } from './native-path'
|
||||
|
||||
export interface InitParams {
|
||||
@@ -42,6 +42,27 @@ let hasCancelApi = false
|
||||
let loggedCancelMissing = false
|
||||
let loggedRejectMissing = false
|
||||
|
||||
function tracePayload(r: unknown): Record<string, unknown> {
|
||||
if (r === null || r === undefined) return {}
|
||||
if (typeof r !== 'object') return { value: r }
|
||||
return { ...(r as Record<string, unknown>) }
|
||||
}
|
||||
|
||||
function traceCall<T>(name: string, args: Record<string, unknown> | undefined, fn: () => T): T {
|
||||
if (!isTraceEnabled()) return fn()
|
||||
const tag = `[dll] ${name}`
|
||||
const start = Date.now()
|
||||
emitTrace(`${tag} →`, args)
|
||||
try {
|
||||
const r = fn()
|
||||
emitTrace(`${tag} ←`, { ms: Date.now() - start, ...tracePayload(r) })
|
||||
return r
|
||||
} catch (e) {
|
||||
emitTrace(`${tag} ✗`, { ms: Date.now() - start, error: String(e) }, 'error')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function loadLibrary(): void {
|
||||
if (lib) return
|
||||
const dllPath = path.join(getNativeDir(), 'workDll.dll')
|
||||
@@ -66,7 +87,7 @@ function loadLibrary(): void {
|
||||
hasCancelApi = false
|
||||
if (!loggedCancelMissing) {
|
||||
loggedCancelMissing = true
|
||||
log.info('SAPI_AdminJobCancel not in workDll (optional); stop uses poll-stop only')
|
||||
emitTrace('[dll] SAPI_AdminJobCancel not in workDll (optional)')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +98,7 @@ function loadLibrary(): void {
|
||||
hasRejectApi = false
|
||||
if (!loggedRejectMissing) {
|
||||
loggedRejectMissing = true
|
||||
log.info('SAPI_PrinterMovetoreject not in workDll (optional); reject card disabled')
|
||||
emitTrace('[dll] SAPI_PrinterMovetoreject not in workDll (optional)')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,46 +114,73 @@ export function isCancelApiAvailable(): boolean {
|
||||
}
|
||||
|
||||
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 ?? false,
|
||||
params.logLevel ?? LOG_FATAL_FLAG,
|
||||
params.outBack ?? false
|
||||
) as number
|
||||
return traceCall(
|
||||
'SAPI_Init',
|
||||
{
|
||||
sharedDir: params.sharedDir,
|
||||
keepCombinedImage: params.keepCombinedImage ?? true,
|
||||
stopOnFailure: params.stopOnFailure ?? false,
|
||||
cleanTaskFile: params.cleanTaskFile ?? true,
|
||||
autoRetryTimes: params.autoRetryTimes ?? 0,
|
||||
rejectConfig: params.rejectConfig ?? false,
|
||||
logLevel: params.logLevel ?? LOG_FATAL_FLAG,
|
||||
outBack: params.outBack ?? false
|
||||
},
|
||||
() => {
|
||||
loadLibrary()
|
||||
return SAPI_Init!(
|
||||
params.sharedDir,
|
||||
params.keepCombinedImage ?? true,
|
||||
params.stopOnFailure ?? false,
|
||||
params.cleanTaskFile ?? true,
|
||||
params.autoRetryTimes ?? 0,
|
||||
params.rejectConfig ?? false,
|
||||
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)
|
||||
}
|
||||
return traceCall('SAPI_GetPrinterInfo', undefined, () => {
|
||||
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
|
||||
if (!ptr) return { code: len }
|
||||
const jsonStr = koffi.decode(ptr, 'char', len) as string
|
||||
koffi.free(ptr)
|
||||
if (!jsonStr?.trim()) return { code: len }
|
||||
try {
|
||||
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
|
||||
} catch {
|
||||
return { code: len }
|
||||
}
|
||||
} finally {
|
||||
koffi.free(outPtr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function dllGetPrinterErrorStr(errorNo = -1): string {
|
||||
loadLibrary()
|
||||
const s = SAPI_GetPrinterErrorStr!(errorNo) as string
|
||||
return s || ''
|
||||
return traceCall('SAPI_GetPrinterErrorStr', { errorNo }, () => {
|
||||
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 }
|
||||
return traceCall('SAPI_RestJobEx', { jsonBytes: Buffer.byteLength(json ?? '', 'utf8') }, () => {
|
||||
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): {
|
||||
@@ -140,43 +188,55 @@ export function dllGetJobStateById(jobId: string): {
|
||||
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]
|
||||
}
|
||||
return traceCall('SAPI_GetJobStateById', { jobId }, () => {
|
||||
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
|
||||
return traceCall('SAPI_AdminJobCancel', { jobId }, () => {
|
||||
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
|
||||
return traceCall('SAPI_CopyFromUsb', { destFolder, cardOutput }, () => {
|
||||
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] }
|
||||
return traceCall('SAPI_GetUsbCopyState', undefined, () => {
|
||||
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
|
||||
return traceCall('SAPI_PrinterResetprinter', undefined, () => {
|
||||
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
|
||||
return traceCall('SAPI_PrinterMovetoreject', undefined, () => {
|
||||
loadLibrary()
|
||||
if (!SAPI_PrinterMovetoreject) throw new Error('REJECT_API_UNAVAILABLE')
|
||||
return SAPI_PrinterMovetoreject() as number
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user