Skip to content

Commit 089a307

Browse files
author
timepp
committed
exit elegantly
1 parent 918c60f commit 089a307

12 files changed

+132
-59
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.vite
2+
/frontend/dist

backend/api_impl.ts

+7-38
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,16 @@
11
import { BackendAPI } from '../api.ts'
2-
import staticAssets from '../static_assets.json' with { type: "json" }
3-
4-
let scriptProcess: Deno.ChildProcess
5-
6-
const infoFile = Deno.env.get('TEMP') + '\\excel_info.txt'
7-
function launchScript() {
8-
// check if excel.js is present
9-
let scriptFile = `./excel.js`
10-
try {
11-
Deno.statSync(scriptFile)
12-
} catch (_e) {
13-
scriptFile = Deno.env.get('TEMP') + '\\excel.js'
14-
const content = staticAssets['excel.js']
15-
Deno.writeTextFileSync(scriptFile, content)
16-
}
17-
const cmd = new Deno.Command('cscript.exe', {
18-
args: ['//nologo', scriptFile, infoFile],
19-
stdout: 'inherit',
20-
stderr: 'inherit',
21-
})
22-
const p = cmd.spawn()
23-
return p
24-
}
2+
import * as et from '../excel_tracker.ts'
253

264
export const apiImpl: BackendAPI = {
275
launchExcel: async () => {
28-
if (scriptProcess) {
29-
try {
30-
scriptProcess.kill()
31-
} catch (_e) {
32-
// ignore
33-
}
34-
}
35-
scriptProcess = launchScript()
6+
await et.startNewTracker()
367
return ''
378
},
389
getActiveExcelRow: async () => {
39-
// read output from script
40-
const s = Deno.readTextFileSync(infoFile)
41-
const [hs, vs] = s.split('_@@RS@@_')
42-
return {
43-
headings: hs.split('_@@HS@@_'),
44-
data: vs.split('_@@VS@@_')
45-
}
10+
return await et.getActiveExcelRow()
4611
}
4712
}
13+
14+
export async function cleanUp() {
15+
await et.stopTracker()
16+
}

build.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// wrap some static assest to a single json file
22
// so that they are be imported to main app and can be created on demand on a http import use case
33

4+
import * as vite from 'npm:[email protected]'
5+
6+
await vite.build()
7+
48
const htmlContent = Deno.readTextFileSync('./frontend/dist/index.html')
59
const jsContent = Deno.readTextFileSync('./frontend/dist/assets/index.js')
610

@@ -15,7 +19,6 @@ const wshScriptContent = Deno.readTextFileSync('./excel.js')
1519

1620
const staticAssets = {
1721
'/index.html': newHtmlContent,
18-
'/index2.html': newHtmlContent,
1922
'excel.js': wshScriptContent,
2023
}
2124

doc/screenshot.png

-212 KB
Binary file not shown.

dwa/dwa_service.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { extname } from 'jsr:@std/[email protected]'
33
import staticAssets from '../static_assets.json' with { type: "json" }
44

55
export function startDenoWebApp(root: string, port: number, apiImpl: {[key: string]: Function}) {
6+
const ac = new AbortController();
67
const corsHeaders = {
78
"Access-Control-Allow-Origin": "*",
89
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
@@ -28,7 +29,7 @@ export function startDenoWebApp(root: string, port: number, apiImpl: {[key: stri
2829
if (cmd === 'closeBackend') {
2930
setTimeout(() => {
3031
console.log('backend closed')
31-
Deno.exit()
32+
ac.abort()
3233
}, 1000)
3334
return new Response(JSON.stringify('OK'), { status: 200 });
3435
}
@@ -63,6 +64,6 @@ export function startDenoWebApp(root: string, port: number, apiImpl: {[key: stri
6364
}
6465
};
6566

66-
Deno.serve({ port }, handlerCORS);
67+
return Deno.serve({ port, signal: ac.signal }, handlerCORS);
6768
}
6869

excel.js

+9-6
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function saveActiveRow(excel, path) {
3333
var headingsStr = headings.join('_@@HS@@_');
3434
var rowValuesStr = rowValues.join('_@@VS@@_');
3535
var content = [headingsStr, rowValuesStr].join('_@@RS@@_');
36-
WScript.Echo(content);
36+
// WScript.Echo(content);
3737
WriteTextFile(content, path, 'utf-8');
3838
}
3939
}
@@ -44,15 +44,18 @@ excel.Visible = true;
4444

4545
var path = WScript.Arguments(0);
4646

47-
// for test, open a workbook
48-
// excel.Workbooks.Open('d:\\src\\excelview\\test.xlsx');
49-
5047
for (;;) {
48+
var cmd = WScript.StdIn.ReadLine();
49+
if (cmd == "exit") {
50+
WScript.Echo("received exit");
51+
break;
52+
}
53+
5154
try {
5255
saveActiveRow(excel, path);
5356
} catch (e) {
5457
WScript.Echo(e.message);
5558
}
56-
57-
WScript.Sleep(1000);
5859
}
60+
61+
excel.Quit();

excel_tracker.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Using "Excel.Application" through WScript to track active cell in Excel
2+
// It's difficult to call COM (but possible) from FFI, so we use WScript for now
3+
// However WScript(cscript.exe) has it's own problem:
4+
// - It's impossible to receive unicode output from stdout pipe, we have to use file to communicate data
5+
// - Need to create child process, make it a little bit complex
6+
7+
import staticAssets from './static_assets.json' with { type: "json" }
8+
9+
let scriptProcess: Deno.ChildProcess
10+
11+
const infoFile = Deno.env.get('TEMP') + '\\excel_info.txt'
12+
function launchScript() {
13+
// check if excel.js is present
14+
let scriptFile = `./excel.js`
15+
try {
16+
Deno.statSync(scriptFile)
17+
} catch (_e) {
18+
scriptFile = Deno.env.get('TEMP') + '\\excel.js'
19+
const content = staticAssets['excel.js']
20+
Deno.writeTextFileSync(scriptFile, content)
21+
}
22+
const cmd = new Deno.Command('cscript.exe', {
23+
args: ['//nologo', scriptFile, infoFile],
24+
stdin: 'piped'
25+
})
26+
const p = cmd.spawn()
27+
return p
28+
}
29+
30+
async function sendCommand(cmd: string) {
31+
if (scriptProcess) {
32+
const writer = scriptProcess.stdin.getWriter()
33+
await writer.write(new TextEncoder().encode(cmd + '\n'))
34+
await writer.releaseLock()
35+
}
36+
}
37+
38+
export async function stopTracker() {
39+
if (scriptProcess) {
40+
sendCommand('exit')
41+
await scriptProcess.status
42+
}
43+
}
44+
45+
export async function startNewTracker() {
46+
await stopTracker()
47+
scriptProcess = launchScript()
48+
}
49+
50+
export async function getActiveExcelRow() {
51+
if (scriptProcess) {
52+
await sendCommand('get active row')
53+
}
54+
// wait the file to be created
55+
await new Promise(resolve => setTimeout(resolve, 500))
56+
// read output from script
57+
const s = Deno.readTextFileSync(infoFile)
58+
const [hs, vs] = s.split('_@@RS@@_')
59+
return {
60+
headings: hs.split('_@@HS@@_'),
61+
data: vs.split('_@@VS@@_')
62+
}
63+
}

frontend/index.html

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
<head>
33
<title>Excel Navigate Helper</title>
44
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
5-
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.js"></script>
6-
<link href="https://cdn.datatables.net/2.0.8/css/dataTables.dataTables.css" rel="stylesheet">
75
<script src="./ui.ts" type="module"></script>
86
</head>
97
<body style="padding:10px;">

frontend/ui.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,27 @@ function updateStyle(app: HTMLElement, styleText: string) {
1515
})
1616
}
1717

18+
async function exit() {
19+
try {
20+
await api.closeBackend()
21+
} catch (_e) {
22+
// ignore
23+
}
24+
window.close()
25+
}
26+
27+
async function getActiveExcelRow() {
28+
try {
29+
return await api.getActiveExcelRow()
30+
} catch (e) {
31+
console.error('backend exit', e)
32+
window.close()
33+
throw 'unreachable'
34+
}
35+
}
36+
1837
async function updateUI(app: HTMLElement, force: boolean = false) {
19-
const activeRow = await api.getActiveExcelRow()
38+
const activeRow = await getActiveExcelRow()
2039
if (!force && currentRowData && JSON.stringify(currentRowData) === JSON.stringify(activeRow)) {
2140
return
2241
}
@@ -111,8 +130,7 @@ async function main() {
111130
close.classList.add('btn', 'btn-danger')
112131
close.textContent = 'Close';
113132
close.onclick = async () => {
114-
await api.closeBackend()
115-
window.close()
133+
await exit()
116134
}
117135
app.append(close)
118136

jsr.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"name": "@timepp/ev",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"exports": "./launch.ts"
55
}

launch.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import * as vite from 'npm:[email protected]'
22
import { parseArgs } from "jsr:@std/[email protected]/parse-args";
3-
import { apiImpl } from "./backend/api_impl.ts";
3+
import * as apiImpl from "./backend/api_impl.ts";
44
import { startDenoWebApp } from "./dwa/dwa_service.ts";
55

66
async function main() {
77
const args = parseArgs(Deno.args)
88
const apiPort = 22311
9-
startDenoWebApp('./frontend', apiPort, apiImpl);
9+
const backend = startDenoWebApp('./frontend', apiPort, apiImpl.apiImpl);
1010

1111
let webPort = apiPort
12+
let frontend: vite.ViteDevServer | null = null
1213
// Use Vite for local development
1314
if (!args.release && import.meta.url.startsWith('file://')) {
14-
const frontend = await vite.createServer()
15+
frontend = await vite.createServer()
1516
webPort = 5173
1617
frontend.listen(webPort)
1718
}
@@ -22,6 +23,23 @@ async function main() {
2223
})
2324
const cp = cmd.spawn()
2425
console.log('browser started, pid:', cp.pid)
26+
27+
const cleanUp = async () => {
28+
console.log('cleaning up...')
29+
if (frontend) {
30+
await frontend.close()
31+
}
32+
await apiImpl.cleanUp()
33+
}
34+
35+
Deno.addSignalListener('SIGINT', () => {
36+
console.log('SIGINT received, shutting down backend')
37+
backend.shutdown()
38+
})
39+
40+
await backend.finished
41+
await cleanUp()
42+
console.log('App Exit')
2543
}
2644

2745
await main()

0 commit comments

Comments
 (0)