Skip to content

Commit 239e177

Browse files
committed
feat(filestore): 优化文件存储缓存刷新机制并改进应用关闭流程
- 添加了 FlushNotifyCh 通道用于通知刷新操作 - 实现了防抖动和定期刷新机制,提高性能 - 添加了 ErrFlushInProgress 错误类型处理并发控制 - 修改了 WriteMeta、WriteFile、AppendData 等方法返回错误并触发刷新通知 - 重构了前端代码中的 Monaco 编辑器为懒加载模式 - 优化了应用关闭时的 WaveSrv 进程管理 - 添加了未处理 Promise 拒绝的错误处理 - 实现了优雅关闭时的文件存储刷新逻辑 - 修复了 Windows 平台进程关闭问题 - 添加了 WCloud 端点配置错误时的日志记录和自动禁用功能
1 parent 2df61d4 commit 239e177

12 files changed

Lines changed: 202 additions & 54 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ docsite/
4444
docs/superpowers
4545
.claude
4646
.idea/
47+
/make/

assets/appicon-windows.ico

28.4 KB
Binary file not shown.

cmd/server/main-server.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"log"
1011
"os"
@@ -67,6 +68,22 @@ const DiagnosticTick = 10 * time.Minute
6768

6869
var shutdownOnce sync.Once
6970

71+
func flushFilestoreOnShutdown(ctx context.Context) {
72+
stats, err := filestore.WFS.FlushCache(ctx)
73+
for errors.Is(err, filestore.ErrFlushInProgress) && ctx.Err() == nil {
74+
log.Printf("filestore flush already in progress during shutdown, waiting for it to finish\n")
75+
time.Sleep(100 * time.Millisecond)
76+
stats, err = filestore.WFS.FlushCache(ctx)
77+
}
78+
if err != nil {
79+
log.Printf("error flushing filestore during shutdown: %v\n", err)
80+
return
81+
}
82+
if stats.NumDirtyEntries > 0 {
83+
log.Printf("filestore shutdown flush: %d/%d entries flushed\n", stats.NumCommitted, stats.NumDirtyEntries)
84+
}
85+
}
86+
7087
func init() {
7188
envFilePath := os.Getenv("WAVETERM_ENVFILE")
7289
if envFilePath != "" {
@@ -85,7 +102,7 @@ func doShutdown(reason string) {
85102
sendTelemetryWrapper()
86103
// TODO deal with flush in progress
87104
clearTempFiles()
88-
filestore.WFS.FlushCache(ctx)
105+
flushFilestoreOnShutdown(ctx)
89106
watcher := wconfig.GetWatcher()
90107
if watcher != nil {
91108
watcher.Close()

electron-builder.config.cjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const windowsTargets = windowsShouldBuildInstallers ? ["nsis", "msi", "zip"] : [
1010
const localWindowsElectronDist = path.resolve(__dirname, "node_modules", "electron", "dist");
1111
const useLocalWindowsElectronDist =
1212
process.platform === "win32" && fs.existsSync(path.join(localWindowsElectronDist, "electron.exe"));
13+
const windowsIconPath = path.resolve(__dirname, "assets", "appicon-windows.ico");
1314

1415
/**
1516
* @type {import('electron-builder').Configuration}
@@ -104,6 +105,7 @@ const config = {
104105
},
105106
win: {
106107
target: windowsTargets,
108+
icon: windowsIconPath,
107109
signAndEditExecutable: windowsShouldEditExecutable,
108110
signtoolOptions: windowsShouldSign && {
109111
signingHashAlgorithms: ["sha256"],
@@ -112,6 +114,11 @@ const config = {
112114
certificateSha1: process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
113115
},
114116
},
117+
nsis: {
118+
installerIcon: windowsIconPath,
119+
uninstallerIcon: windowsIconPath,
120+
installerHeaderIcon: windowsIconPath,
121+
},
115122
appImage: {
116123
license: "LICENSE",
117124
},

emain/emain-wavesrv.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis
7676
cwd: getWaveSrvCwd(),
7777
env: envCopy,
7878
});
79-
proc.on("exit", (e) => {
79+
proc.on("exit", (code, signal) => {
8080
if (updater?.status == "installing") {
8181
return;
8282
}
83-
console.log("wavesrv exited, shutting down");
83+
console.log("wavesrv exited, shutting down", "code=", code, "signal=", signal);
8484
setForceQuit(true);
8585
isWaveSrvDead = true;
8686
electron.app.quit();
@@ -107,9 +107,7 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis
107107
});
108108
rlStderr.on("line", (line) => {
109109
if (line.includes("WAVESRV-ESTART")) {
110-
const startParams = /ws:([a-z0-9.:]+) web:([a-z0-9.:]+) version:([a-z0-9.-]+) buildtime:(\d+)/gm.exec(
111-
line
112-
);
110+
const startParams = /ws:([a-z0-9.:]+) web:([a-z0-9.:]+) version:([a-z0-9.-]+) buildtime:(\d+)/gm.exec(line);
113111
if (startParams == null) {
114112
console.log("error parsing WAVESRV-ESTART line", line);
115113
setUserConfirmedQuit(true);

emain/emain.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,23 @@ function hideWindowWithCatch(window: WaveBrowserWindow) {
254254
}
255255
}
256256

257+
function requestWaveSrvShutdown() {
258+
shutdownWshrpc();
259+
const waveSrvProc = getWaveSrvProc();
260+
if (waveSrvProc == null) {
261+
return;
262+
}
263+
if (unamePlatform === "win32") {
264+
try {
265+
waveSrvProc.stdin.end();
266+
} catch (e) {
267+
console.log("error closing wavesrv stdin", e);
268+
}
269+
return;
270+
}
271+
waveSrvProc.kill("SIGINT");
272+
}
273+
257274
electronApp.on("window-all-closed", () => {
258275
if (getGlobalIsRelaunching()) {
259276
return;
@@ -292,13 +309,7 @@ electronApp.on("before-quit", (e) => {
292309
}
293310
setGlobalIsQuitting(true);
294311
updater?.stop();
295-
if (unamePlatform == "win32") {
296-
// win32 doesn't have a SIGINT, so we just let electron die, which
297-
// ends up killing wavesrv via closing it's stdin.
298-
return;
299-
}
300-
getWaveSrvProc()?.kill("SIGINT");
301-
shutdownWshrpc();
312+
requestWaveSrvShutdown();
302313
if (getForceQuit()) {
303314
return;
304315
}
@@ -356,6 +367,12 @@ process.on("uncaughtException", (error) => {
356367
setUserConfirmedQuit(true);
357368
electronApp.quit();
358369
});
370+
process.on("unhandledRejection", (reason) => {
371+
console.log("Unhandled Rejection:", reason);
372+
if (reason instanceof Error) {
373+
console.log("Stack Trace:", reason.stack);
374+
}
375+
});
359376

360377
let lastWaveWindowCount = 0;
361378
let lastIsBuilderWindowActive = false;

frontend/app/view/codeeditor/codeeditor.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { MonacoCodeEditor } from "@/app/monaco/monaco-react";
54
import { useOverrideConfigAtom } from "@/app/store/global";
65
import { boundNumber } from "@/util/util";
76
import type * as MonacoTypes from "monaco-editor";
8-
import * as MonacoModule from "monaco-editor";
97
import React, { useMemo, useRef } from "react";
108

9+
const LazyMonacoCodeEditor = React.lazy(async () => {
10+
const mod = await import("@/app/monaco/monaco-react");
11+
return { default: mod.MonacoCodeEditor };
12+
});
13+
type MonacoModuleType = typeof import("monaco-editor");
14+
1115
function defaultEditorOptions(): MonacoTypes.editor.IEditorOptions {
1216
const opts: MonacoTypes.editor.IEditorOptions = {
1317
scrollBeyondLastLine: false,
@@ -36,7 +40,7 @@ interface CodeEditorProps {
3640
language?: string;
3741
fileName?: string;
3842
onChange?: (text: string) => void;
39-
onMount?: (monacoPtr: MonacoTypes.editor.IStandaloneCodeEditor, monaco: typeof MonacoModule) => () => void;
43+
onMount?: (monacoPtr: MonacoTypes.editor.IStandaloneCodeEditor, monaco: MonacoModuleType) => () => void;
4044
}
4145

4246
export function CodeEditor({ blockId, text, language, fileName, readonly, onChange, onMount }: CodeEditorProps) {
@@ -72,7 +76,7 @@ export function CodeEditor({ blockId, text, language, fileName, readonly, onChan
7276

7377
function handleEditorOnMount(
7478
editor: MonacoTypes.editor.IStandaloneCodeEditor,
75-
monaco: typeof MonacoModule
79+
monaco: MonacoModuleType
7680
): () => void {
7781
if (onMount) {
7882
const cleanup = onMount(editor, monaco);
@@ -95,15 +99,17 @@ export function CodeEditor({ blockId, text, language, fileName, readonly, onChan
9599
return (
96100
<div className="flex flex-col w-full h-full items-center justify-center">
97101
<div className="flex flex-col h-full w-full" ref={divRef}>
98-
<MonacoCodeEditor
99-
readonly={readonly}
100-
text={text}
101-
options={editorOpts}
102-
onChange={handleEditorChange}
103-
onMount={handleEditorOnMount}
104-
path={editorPath}
105-
language={language}
106-
/>
102+
<React.Suspense fallback={<div className="flex flex-col h-full w-full" />}>
103+
<LazyMonacoCodeEditor
104+
readonly={readonly}
105+
text={text}
106+
options={editorOpts}
107+
onChange={handleEditorChange}
108+
onMount={handleEditorOnMount}
109+
path={editorPath}
110+
language={language}
111+
/>
112+
</React.Suspense>
107113
</div>
108114
</div>
109115
);

frontend/app/view/codeeditor/diffviewer.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { MonacoDiffViewer } from "@/app/monaco/monaco-react";
54
import { useOverrideConfigAtom } from "@/app/store/global";
65
import { boundNumber } from "@/util/util";
76
import type * as MonacoTypes from "monaco-editor";
8-
import { useMemo, useRef } from "react";
7+
import React, { useMemo, useRef } from "react";
8+
9+
const LazyMonacoDiffViewer = React.lazy(async () => {
10+
const mod = await import("@/app/monaco/monaco-react");
11+
return { default: mod.MonacoDiffViewer };
12+
});
913

1014
interface DiffViewerProps {
1115
blockId: string;
@@ -62,13 +66,15 @@ export function DiffViewer({ blockId, original, modified, language, fileName }:
6266
return (
6367
<div className="flex flex-col w-full h-full overflow-hidden items-center justify-center">
6468
<div className="flex flex-col h-full w-full">
65-
<MonacoDiffViewer
66-
path={editorPath}
67-
original={original}
68-
modified={modified}
69-
options={editorOpts}
70-
language={language}
71-
/>
69+
<React.Suspense fallback={<div className="flex flex-col h-full w-full" />}>
70+
<LazyMonacoDiffViewer
71+
path={editorPath}
72+
original={original}
73+
modified={modified}
74+
options={editorOpts}
75+
language={language}
76+
/>
77+
</React.Suspense>
7278
</div>
7379
</div>
7480
);

frontend/wave.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { App } from "@/app/app";
5-
import { loadMonaco } from "@/app/monaco/monaco-env";
65
import { loadBadges } from "@/app/store/badge";
76
import { GlobalModel } from "@/app/store/global-model";
87
import {
@@ -40,6 +39,7 @@ const platform = getApi().getPlatform();
4039
document.title = `Wave Terminal`;
4140
let savedInitOpts: WaveInitOpts = null;
4241
let tabTitleUnsub: (() => void) | null = null;
42+
let monacoLoadPromise: Promise<void> | null = null;
4343

4444
(window as any).WOS = WOS;
4545
(window as any).globalStore = globalStore;
@@ -57,6 +57,27 @@ function updateZoomFactor(zoomFactor: number) {
5757
document.documentElement.style.setProperty("--zoomfactor-inv", String(1 / zoomFactor));
5858
}
5959

60+
function ensureMonacoLoaded(): Promise<void> {
61+
if (monacoLoadPromise == null) {
62+
monacoLoadPromise = import("@/app/monaco/monaco-env").then(({ loadMonaco }) => {
63+
loadMonaco();
64+
});
65+
}
66+
return monacoLoadPromise;
67+
}
68+
69+
function preloadMonaco() {
70+
fireAndForget(async () => {
71+
try {
72+
await ensureMonacoLoaded();
73+
} catch (e) {
74+
const error = e instanceof Error ? e : new Error(String(e));
75+
getApi().sendLog("Error preloading Monaco " + error.message + "\n" + error.stack);
76+
console.error("Error preloading Monaco", e);
77+
}
78+
});
79+
}
80+
6081
function formatWaveWindowTitle(tabName?: string | null) {
6182
const trimmedTabName = tabName?.trim();
6283
return trimmedTabName ? `Wave Terminal - ${trimmedTabName}` : "Wave Terminal";
@@ -182,11 +203,15 @@ async function initWave(initOpts: WaveInitOpts) {
182203
const globalWS = initWshrpc(makeTabRouteId(initOpts.tabId));
183204
(window as any).globalWS = globalWS;
184205
(window as any).TabRpcClient = TabRpcClient;
206+
const startupConfigPromise = Promise.all([
207+
RpcApi.GetFullConfigCommand(TabRpcClient),
208+
RpcApi.GetWaveAIModeConfigCommand(TabRpcClient),
209+
]);
210+
startupConfigPromise.catch(() => undefined);
185211

186212
// ensures client/window/workspace are loaded into the cache before rendering
187213
try {
188-
await loadConnStatus();
189-
await loadBadges();
214+
await Promise.all([loadConnStatus(), loadBadges()]);
190215
initGlobalWaveEventSubs(initOpts);
191216
subscribeToConnEvents();
192217
if (isMacOS()) {
@@ -212,11 +237,9 @@ async function initWave(initOpts: WaveInitOpts) {
212237
registerGlobalKeys();
213238
registerElectronReinjectKeyHandler();
214239
registerControlShiftStateUpdateHandler();
215-
await loadMonaco();
216-
const fullConfig = await RpcApi.GetFullConfigCommand(TabRpcClient);
240+
const [fullConfig, waveaiModeConfig] = await startupConfigPromise;
217241
console.log("fullconfig", fullConfig);
218242
globalStore.set(atoms.fullConfigAtom, fullConfig);
219-
const waveaiModeConfig = await RpcApi.GetWaveAIModeConfigCommand(TabRpcClient);
220243
globalStore.set(atoms.waveaiModeConfigAtom, waveaiModeConfig.configs);
221244
console.log("Wave First Render");
222245
let firstRenderResolveFn: () => void = null;
@@ -230,6 +253,7 @@ async function initWave(initOpts: WaveInitOpts) {
230253
await firstRenderPromise;
231254
console.log("Wave First Render Done");
232255
getApi().setWindowInitStatus("wave-ready");
256+
preloadMonaco();
233257
}
234258

235259
async function initBuilderWrap(initOpts: BuilderInitOpts) {
@@ -283,7 +307,7 @@ async function initBuilder(initOpts: BuilderInitOpts) {
283307

284308
registerBuilderGlobalKeys();
285309
registerElectronReinjectKeyHandler();
286-
await loadMonaco();
310+
await ensureMonacoLoaded();
287311
const fullConfig = await RpcApi.GetFullConfigCommand(TabRpcClient);
288312
console.log("fullconfig", fullConfig);
289313
globalStore.set(atoms.fullConfigAtom, fullConfig);

0 commit comments

Comments
 (0)