初始化
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
export const CS_OK = 0
|
||||
export const CS_FAIL = -1
|
||||
|
||||
export const JOB_STATUS_FINISHED = 100
|
||||
export const JOB_STATUS_FAILED = 4
|
||||
export const JOB_STATUS_CANCELLED = 6
|
||||
export const JOB_STATUS_WAITCARD = 7
|
||||
export const JOB_STATUS_PRINTING = 2
|
||||
export const JOB_STATUS_COPYING = 3
|
||||
|
||||
export const POLL_INTERVAL_MS = 1000
|
||||
export const JOB_ID_BUF_SIZE = 64
|
||||
|
||||
/** workDll 日志级别:仅致命,屏蔽无打印机时的 E/W 刷屏 */
|
||||
export const LOG_FATAL_FLAG = 3
|
||||
@@ -0,0 +1,121 @@
|
||||
import { app, BrowserWindow, globalShortcut, screen } from 'electron'
|
||||
import { join } from 'path'
|
||||
|
||||
app.commandLine.appendSwitch('disable-gpu-shader-disk-cache')
|
||||
|
||||
import log from 'electron-log'
|
||||
import { suppressKnownDllStderr } from './utils/suppress-dll-stderr'
|
||||
import { setupNativeWorkingDir } from './services/native-path'
|
||||
|
||||
suppressKnownDllStderr()
|
||||
import { registerIpcHandlers, handleBeforeQuit } from './ipc/register-handlers'
|
||||
import { setPollMainWindow } from './services/poll-manager'
|
||||
import {
|
||||
DESIGN_WIDTH,
|
||||
contentHeightForWidth,
|
||||
CONTENT_VIEWPORT_HEIGHT
|
||||
} from '@shared/viewport'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
const MIN_CONTENT_WIDTH = 960
|
||||
|
||||
/** 默认内容区:约 85% 工作区宽,高按 720:390 */
|
||||
function getDefaultWindowSize(): { width: number; height: number } {
|
||||
const { width: sw, height: sh } = screen.getPrimaryDisplay().workAreaSize
|
||||
let w = Math.max(1280, Math.min(Math.floor(sw * 0.85), 1600))
|
||||
let h = contentHeightForWidth(w)
|
||||
const maxH = Math.floor(sh * 0.85)
|
||||
if (h > maxH) {
|
||||
h = Math.max(contentHeightForWidth(MIN_CONTENT_WIDTH), maxH)
|
||||
w = Math.round((h * DESIGN_WIDTH) / CONTENT_VIEWPORT_HEIGHT)
|
||||
}
|
||||
return { width: w, height: h }
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
const { width, height } = getDefaultWindowSize()
|
||||
mainWindow = new BrowserWindow({
|
||||
useContentSize: true,
|
||||
width,
|
||||
height,
|
||||
minWidth: MIN_CONTENT_WIDTH,
|
||||
minHeight: contentHeightForWidth(MIN_CONTENT_WIDTH),
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false
|
||||
}
|
||||
})
|
||||
|
||||
setPollMainWindow(mainWindow)
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow?.center()
|
||||
mainWindow?.show()
|
||||
if (!app.isPackaged) {
|
||||
mainWindow?.webContents.openDevTools({ mode: 'detach' })
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('before-input-event', (_event, input) => {
|
||||
if (input.type === 'keyDown' && input.key === 'F12') {
|
||||
mainWindow?.webContents.toggleDevTools()
|
||||
}
|
||||
})
|
||||
|
||||
// 内容区 720:390,画布仍 360 高,按宽缩放后底部可完整显示
|
||||
mainWindow.on('resize', () => {
|
||||
if (!mainWindow) return
|
||||
const [cw, ch] = mainWindow.getContentSize()
|
||||
const wantH = contentHeightForWidth(cw)
|
||||
if (Math.abs(ch - wantH) > 2) {
|
||||
mainWindow.setContentSize(cw, wantH)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
if (process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
try {
|
||||
setupNativeWorkingDir()
|
||||
registerIpcHandlers()
|
||||
createWindow()
|
||||
if (!app.isPackaged) {
|
||||
globalShortcut.register('CommandOrControl+Shift+I', () => {
|
||||
const win = BrowserWindow.getFocusedWindow()
|
||||
win?.webContents.toggleDevTools()
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('startup failed', e)
|
||||
app.quit()
|
||||
}
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (!app.isPackaged) {
|
||||
globalShortcut.unregisterAll()
|
||||
}
|
||||
handleBeforeQuit()
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
@@ -0,0 +1,244 @@
|
||||
import { app, dialog, ipcMain, shell } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import log from 'electron-log'
|
||||
import { CS_FAIL, CS_OK } from '../constants'
|
||||
import { assertNotBusy, assertReady, mainAppState } from '../services/app-state'
|
||||
import { configStore } from '../services/config-store'
|
||||
import {
|
||||
getActiveJobId,
|
||||
startJobPoll,
|
||||
startUsbPoll,
|
||||
stopAllPolls,
|
||||
stopJobPoll,
|
||||
stopUsbPoll
|
||||
} from '../services/poll-manager'
|
||||
import {
|
||||
dllAdminJobCancel,
|
||||
dllCopyFromUsb,
|
||||
dllGetPrinterErrorStr,
|
||||
dllGetPrinterInfo,
|
||||
dllInit,
|
||||
dllPrinterReject,
|
||||
dllPrinterReset,
|
||||
dllRestJobEx,
|
||||
isCancelApiAvailable,
|
||||
isRejectApiAvailable
|
||||
} from '../services/work-dll.service'
|
||||
|
||||
function ok<T>(data?: T) {
|
||||
return { ok: true as const, code: CS_OK, data }
|
||||
}
|
||||
|
||||
function fail(code: number, message: string) {
|
||||
return { ok: false as const, code, message }
|
||||
}
|
||||
|
||||
export function registerIpcHandlers(): void {
|
||||
ipcMain.handle('dll:init', (_e, params) => {
|
||||
stopAllPolls()
|
||||
try {
|
||||
const sharedDir = params?.sharedDir || (configStore.get('sharedDir') as string)
|
||||
fs.mkdirSync(sharedDir, { recursive: true })
|
||||
const code = dllInit({
|
||||
sharedDir,
|
||||
keepCombinedImage: params?.keepCombinedImage,
|
||||
stopOnFailure: params?.stopOnFailure,
|
||||
cleanTaskFile: params?.cleanTaskFile,
|
||||
autoRetryTimes: params?.autoRetryTimes,
|
||||
rejectConfig: params?.rejectConfig,
|
||||
logLevel: params?.logLevel,
|
||||
outBack: params?.outBack
|
||||
})
|
||||
if (code === CS_OK) {
|
||||
mainAppState.initialized = true
|
||||
configStore.set('sharedDir', sharedDir)
|
||||
return ok({ printerDetected: true })
|
||||
}
|
||||
mainAppState.initialized = true
|
||||
configStore.set('sharedDir', sharedDir)
|
||||
log.warn(`SAPI_Init returned ${code}; UI ready, printer ops may fail until device connected`)
|
||||
return ok({
|
||||
printerDetected: false,
|
||||
warning: '打印机未连接或驱动未就绪,界面可浏览,业务操作需接真机后重试 Init'
|
||||
})
|
||||
} catch (err) {
|
||||
mainAppState.initialized = false
|
||||
log.error('dll:init', err)
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('dll:printer-info', () => {
|
||||
try {
|
||||
assertReady()
|
||||
const r = dllGetPrinterInfo()
|
||||
if (!r.json) return fail(0, '未连接打印机')
|
||||
return ok(r.json)
|
||||
} catch (err) {
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('dll:printer-reset', () => {
|
||||
try {
|
||||
assertReady()
|
||||
const code = dllPrinterReset()
|
||||
return code === CS_OK ? ok() : fail(code, '重置失败')
|
||||
} catch (err) {
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('dll:printer-reject', () => {
|
||||
try {
|
||||
assertReady()
|
||||
if (!isRejectApiAvailable()) return fail(CS_FAIL, 'REJECT_API_UNAVAILABLE')
|
||||
const code = dllPrinterReject()
|
||||
return code === CS_OK ? ok() : fail(code, '废卡失败')
|
||||
} catch (err) {
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('dll:printer-error-str', (_e, errorNo?: number) => {
|
||||
try {
|
||||
assertReady()
|
||||
return ok({ text: dllGetPrinterErrorStr(errorNo ?? -1) })
|
||||
} catch (err) {
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('dll:job-create', (_e, json: string) => {
|
||||
try {
|
||||
assertReady()
|
||||
assertNotBusy()
|
||||
const r = dllRestJobEx(json)
|
||||
if (r.code !== CS_OK) return fail(r.code, 'RestJobEx 失败')
|
||||
mainAppState.mode = 'distributing'
|
||||
mainAppState.activeJobId = r.jobId
|
||||
return ok({ jobId: r.jobId })
|
||||
} catch (err) {
|
||||
if (String(err).includes('BUSY')) return fail(CS_FAIL, '已有任务在执行')
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('dll:job-cancel', (_e, jobId: string) => {
|
||||
try {
|
||||
assertReady()
|
||||
const id = jobId || mainAppState.activeJobId
|
||||
stopJobPoll()
|
||||
let code = CS_OK
|
||||
if (isCancelApiAvailable()) {
|
||||
code = dllAdminJobCancel(id)
|
||||
} else {
|
||||
log.info('dll:job-cancel: SAPI_AdminJobCancel not in DLL, poll stopped only')
|
||||
}
|
||||
mainAppState.mode = 'ready'
|
||||
mainAppState.activeJobId = ''
|
||||
return code === CS_OK ? ok() : fail(code, '取消失败')
|
||||
} catch (err) {
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('dll:usb-copy', (_e, req: { destFolder: string; cardOutput: number }) => {
|
||||
try {
|
||||
assertReady()
|
||||
assertNotBusy()
|
||||
const code = dllCopyFromUsb(req.destFolder, req.cardOutput)
|
||||
if (code !== CS_OK) {
|
||||
return fail(code, '可能已有任务在执行')
|
||||
}
|
||||
mainAppState.mode = 'usbCopying'
|
||||
return ok()
|
||||
} catch (err) {
|
||||
if (String(err).includes('BUSY')) return fail(CS_FAIL, '已有任务在执行')
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('poll:job-start', (_e, jobId: string) => {
|
||||
startJobPoll(jobId)
|
||||
return ok()
|
||||
})
|
||||
|
||||
ipcMain.handle('poll:job-stop', () => {
|
||||
stopJobPoll()
|
||||
return ok()
|
||||
})
|
||||
|
||||
ipcMain.handle('poll:usb-start', () => {
|
||||
startUsbPoll()
|
||||
return ok()
|
||||
})
|
||||
|
||||
ipcMain.handle('poll:usb-stop', () => {
|
||||
stopUsbPoll()
|
||||
mainAppState.mode = 'ready'
|
||||
return ok()
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:open-directory', async () => {
|
||||
const r = await dialog.showOpenDialog({ properties: ['openDirectory', 'multiSelections'] })
|
||||
if (r.canceled || !r.filePaths.length) return ok({ paths: [] as string[] })
|
||||
return ok({ paths: r.filePaths })
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:open-file', async (_e, filters?: { name: string; extensions: string[] }[]) => {
|
||||
const r = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: filters ?? [{ name: 'Soon', extensions: ['soon'] }]
|
||||
})
|
||||
if (r.canceled || !r.filePaths[0]) return ok({ path: '' })
|
||||
return ok({ path: r.filePaths[0] })
|
||||
})
|
||||
|
||||
ipcMain.handle('fs:path-exists', (_e, paths: string[]) => {
|
||||
const missing = paths.filter((p) => {
|
||||
const clean = p.replace(/\\\*\\.\\*$/i, '').replace(/\/\*\.\*$/i, '')
|
||||
return !fs.existsSync(clean)
|
||||
})
|
||||
return ok({ missing })
|
||||
})
|
||||
|
||||
ipcMain.handle('config:get', () =>
|
||||
ok({
|
||||
sharedDir: configStore.get('sharedDir'),
|
||||
templateDir: configStore.get('templateDir'),
|
||||
skipDllInit: configStore.get('skipDllInit', !app.isPackaged)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle('config:set', (_e, patch: Record<string, string>) => {
|
||||
Object.entries(patch).forEach(([k, v]) => configStore.set(k, v))
|
||||
return ok(configStore.store)
|
||||
})
|
||||
|
||||
ipcMain.handle('shell:open-path', (_e, target: string) => {
|
||||
const dir = target || (configStore.get('templateDir') as string)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
shell.openPath(dir)
|
||||
return ok()
|
||||
})
|
||||
|
||||
ipcMain.handle('dll:reject-available', () => ok({ available: isRejectApiAvailable() }))
|
||||
}
|
||||
|
||||
export async function handleBeforeQuit(): Promise<void> {
|
||||
stopAllPolls()
|
||||
if (
|
||||
mainAppState.mode === 'distributing' &&
|
||||
mainAppState.activeJobId &&
|
||||
isCancelApiAvailable()
|
||||
) {
|
||||
try {
|
||||
dllAdminJobCancel(mainAppState.activeJobId)
|
||||
} catch (e) {
|
||||
log.warn('before-quit cancel', e)
|
||||
}
|
||||
}
|
||||
mainAppState.mode = 'ready'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { app } from 'electron'
|
||||
|
||||
const SUPPRESS_PATTERNS = [
|
||||
'Card Printer not detected',
|
||||
'PrinterAdaptor.cpp',
|
||||
'ServerAPI.cpp:654',
|
||||
'using time over',
|
||||
'Fail to read template file'
|
||||
]
|
||||
|
||||
function shouldSuppress(chunk: string | Uint8Array): boolean {
|
||||
const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
|
||||
return SUPPRESS_PATTERNS.some((p) => text.includes(p))
|
||||
}
|
||||
|
||||
export function suppressKnownDllStderr(): void {
|
||||
if (app.isPackaged) return
|
||||
|
||||
const stderr = process.stderr
|
||||
const original = stderr.write.bind(stderr)
|
||||
|
||||
stderr.write = ((chunk: string | Uint8Array, ...args: unknown[]) => {
|
||||
if (shouldSuppress(chunk)) {
|
||||
return true
|
||||
}
|
||||
return (original as (...a: unknown[]) => boolean)(chunk, ...args)
|
||||
}) as typeof stderr.write
|
||||
}
|
||||
Reference in New Issue
Block a user