Set up electron-builder for new app (#113)

Adds electron-builder, which we will use to package and distribute our
application, same as in the existing app.
Replaces explicit port assignments with dynamic ones, which are then
stored into environment variables.
Adds a ~/.w2-dev folder for use when running a dev build.

The build-helper pipeline from the old repo is included here too, but it
is not updated yet so it will fail.

Also removes some redundant utility functions and cleans up some let vs.
const usage.

The packaging can be run using the `package:prod` and `package:dev`
tasks.

---------

Co-authored-by: sawka <mike.sawka@gmail.com>
This commit is contained in:
Evan Simkowitz 2024-07-17 18:42:49 -07:00 committed by GitHub
parent e4204b96d8
commit 8971e2feba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1512 additions and 276 deletions

75
.github/workflows/build-helper.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: "Build Helper"
on: workflow_dispatch
env:
GO_VERSION: "1.22.0"
NODE_VERSION: "21.5.0"
jobs:
runbuild:
strategy:
matrix:
include:
- platform: "darwin"
arch: "universal"
runner: "macos-latest-xlarge"
task: "build-package"
- platform: "linux"
arch: "amd64"
runner: "ubuntu-latest"
scripthaus: "build-package-linux"
- platform: "linux"
arch: "arm64"
runner: "ubuntu-latest"
scripthaus: "build-package-linux"
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: scripthaus-dev/scripthaus
path: scripthaus
- name: Install Linux Build Dependencies (Linux only)
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y libarchive-tools libopenjp2-tools rpm
- uses: actions/setup-go@v5
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: |
wavesrv/go.sum
waveshell/go.sum
scripthaus/go.sum
- name: Install Scripthaus
run: |
go work use ./scripthaus;
cd scripthaus;
go get ./...;
CGO_ENABLED=1 go build -o scripthaus cmd/main.go
echo $PWD >> $GITHUB_PATH
- uses: actions/setup-node@v4
with:
node-version: ${{env.NODE_VERSION}}
- name: Install yarn
run: |
corepack enable
yarn install
- name: Set Version
id: set-version
run: |
VERSION=$(node -e 'console.log(require("./version.js"))')
echo "WAVETERM_VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Build ${{ matrix.platform }}/${{ matrix.arch }}
run: scripthaus run ${{ matrix.scripthaus }}
env:
GOARCH: ${{ matrix.arch }}
CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}}
CSC_KEY_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE_PWD }}
APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_PWD }}
APPLE_TEAM_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
- name: Upload to S3 staging
run: aws s3 cp make/ s3://waveterm-github-artifacts/staging/${{ steps.set-version.outputs.WAVETERM_VERSION }}/ --recursive --exclude "*/*" --exclude "builder-*.yml"
env:
AWS_ACCESS_KEY_ID: "${{ secrets.S3_USERID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.S3_SECRETKEY }}"
AWS_DEFAULT_REGION: us-west-2

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ bin/
.DS_Store .DS_Store
*~ *~
out/ out/
make/
# Yarn Modern # Yarn Modern
.pnp.* .pnp.*

View File

@ -10,6 +10,39 @@ vars:
sh: node version.cjs sh: node version.cjs
tasks: tasks:
package:dev:
cmds:
- yarn build:dev && yarn electron-builder -c electron-builder.config.cjs -p never
deps:
- generate
- build:server
- build:wsh
package:prod:
cmds:
- yarn build:prod && yarn electron-builder -c electron-builder.config.cjs -p never
deps:
- generate
- build:server
- build:wsh
electron:dev:
cmds:
- WAVETERM_DEV=1 yarn dev
deps:
- generate
- build:server
- build:wsh
electron:start:
cmds:
- WAVETERM_DEV=1 yarn start
deps:
- generate
- build:server
- build:wsh
generate: generate:
cmds: cmds:
- go run cmd/generate/main-generate.go - go run cmd/generate/main-generate.go
@ -20,41 +53,78 @@ tasks:
- "pkg/wstore/*.go" - "pkg/wstore/*.go"
- "pkg/wshrpc/**/*.go" - "pkg/wshrpc/**/*.go"
electron:dev:
cmds:
- WAVETERM_DEV=1 yarn dev
deps:
- build:server
- build:wsh
electron:start:
cmds:
- WAVETERM_DEV=1 yarn start
deps:
- build:server
- build:wsh
build:server: build:server:
deps:
- task: build:server:internal
vars:
GOARCH: arm64
- task: build:server:internal
vars:
GOARCH: amd64
build:server:internal:
requires:
vars:
- GOARCH
cmds: cmds:
- go build -o dist/bin/wavesrv{{exeExt}} cmd/server/main-server.go - CGO_ENABLED=1 GOARCH={{.GOARCH}} go build -tags "osusergo,netgo,sqlite_omit_load_extension" -ldflags "-X main.BuildTime=$(date +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wavesrv.{{.GOARCH}}{{exeExt}} cmd/server/main-server.go
sources: sources:
- "cmd/server/*.go" - "cmd/server/*.go"
- "pkg/**/*.go" - "pkg/**/*.go"
generates: generates:
- dist/bin/wavesrv{{exeExt}} - dist/bin/wavesrv.{{.GOARCH}}{{exeExt}}
deps: deps:
- go:mod:tidy - go:mod:tidy
internal: true
build:wsh: build:wsh:
cmds: deps:
- go build -o dist/bin/wsh{{exeExt}} cmd/wsh/main-wsh.go - task: build:wsh:internal
vars:
GOOS: darwin
GOARCH: arm64
- task: build:wsh:internal
vars:
GOOS: darwin
GOARCH: amd64
- task: build:wsh:internal
vars:
GOOS: linux
GOARCH: arm64
- task: build:wsh:internal
vars:
GOOS: linux
GOARCH: amd64
- task: build:wsh:internal
vars:
GOOS: windows
GOARCH: amd64
- task: build:wsh:internal
vars:
GOOS: windows
GOARCH: arm64
build:wsh:internal:
vars:
GO_LDFLAGS:
sh: echo "-s -w -X main.BuildTime=$(date +'%Y%m%d%H%M')"
EXT:
sh: echo {{if eq .GOOS "windows"}}.exe{{end}}
requires:
vars:
- GOOS
- GOARCH
- VERSION
sources: sources:
- "cmd/wsh/**/*.go" - "cmd/wsh/**/*.go"
- "pkg/**/*.go" - "pkg/**/*.go"
generates: generates:
- dist/bin/wsh{{exeExt}} - dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.GOARCH}}{{.EXT}}
cmds:
- (CGO_ENABLED=0 GOOS={{.GOOS}} GOARCH={{.GOARCH}} go build -ldflags="{{.GO_LDFLAGS}}" -o dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.GOARCH}}{{.EXT}} cmd/wsh/main-wsh.go)
deps: deps:
- go:mod:tidy - go:mod:tidy
internal: true
go:mod:tidy: go:mod:tidy:
summary: Runs `go mod tidy` summary: Runs `go mod tidy`

View File

@ -124,12 +124,17 @@ func main() {
go stdinReadWatch() go stdinReadWatch()
configWatcher() configWatcher()
go web.RunWebSocketServer() webListener, err := web.MakeTCPListener("web")
webListener, err := web.MakeTCPListener()
if err != nil { if err != nil {
log.Printf("error creating web listener: %v\n", err) log.Printf("error creating web listener: %v\n", err)
return return
} }
wsListener, err := web.MakeTCPListener("websocket")
if err != nil {
log.Printf("error creating websocket listener: %v\n", err)
return
}
go web.RunWebSocketServer(wsListener)
unixListener, err := web.MakeUnixListener() unixListener, err := web.MakeUnixListener()
if err != nil { if err != nil {
log.Printf("error creating unix listener: %v\n", err) log.Printf("error creating unix listener: %v\n", err)
@ -141,7 +146,7 @@ func main() {
_, err := strconv.Atoi(pidStr) _, err := strconv.Atoi(pidStr)
if err == nil { if err == nil {
// use fmt instead of log here to make sure it goes directly to stderr // use fmt instead of log here to make sure it goes directly to stderr
fmt.Fprintf(os.Stderr, "WAVESRV-ESTART\n") fmt.Fprintf(os.Stderr, "WAVESRV-ESTART ws:%s web:%s\n", wsListener.Addr(), webListener.Addr())
} }
} }
}() }()

View File

@ -0,0 +1,79 @@
const pkg = require("./package.json");
const fs = require("fs");
const path = require("path");
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
*/
const config = {
appId: pkg.build.appId,
productName: pkg.productName,
artifactName: "${productName}-${platform}-${arch}-${version}.${ext}",
npmRebuild: false,
nodeGypRebuild: false,
electronCompile: false,
files: [
{
from: "./dist",
to: "./dist",
filter: ["**/*"],
},
{
from: ".",
to: ".",
filter: ["package.json"],
},
],
directories: {
output: "make",
},
asarUnpack: ["dist/bin/**/*"],
mac: {
target: [
{
target: "zip",
arch: "universal",
},
{
target: "dmg",
arch: "universal",
},
],
icon: "build/icons.icns",
category: "public.app-category.developer-tools",
minimumSystemVersion: "10.15.0",
notarize: process.env.APPLE_TEAM_ID
? {
teamId: process.env.APPLE_TEAM_ID,
}
: false,
binaries: fs
.readdirSync("dist/bin", { recursive: true, withFileTypes: true })
.filter((f) => f.isFile() && (f.name.startsWith("wavesrv") || f.name.includes("darwin")))
.map((f) => path.resolve(f.path, f.name)),
},
linux: {
executableName: pkg.productName,
category: "TerminalEmulator",
icon: "build/icons.icns",
target: ["zip", "deb", "rpm", "AppImage", "pacman"],
synopsis: pkg.description,
description: null,
desktop: {
Name: pkg.productName,
Comment: pkg.description,
Keywords: "developer;terminal;emulator;",
category: "Development;Utility;",
},
},
appImage: {
license: "LICENSE",
},
publish: {
provider: "generic",
url: "https://dl.waveterm.dev/releases",
},
};
module.exports = config;

View File

@ -8,32 +8,34 @@ import os from "os";
import * as path from "path"; import * as path from "path";
import * as readline from "readline"; import * as readline from "readline";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import { getBackendHostPort } from "../frontend/app/store/global"; import * as util from "util";
import winston from "winston";
import * as services from "../frontend/app/store/services"; import * as services from "../frontend/app/store/services";
import * as keyutil from "../frontend/util/keyutil"; import * as keyutil from "../frontend/util/keyutil";
import { fireAndForget } from "../frontend/util/util"; import { fireAndForget } from "../frontend/util/util";
import { getServerWebEndpoint, WebServerEndpointVarName, WSServerEndpointVarName } from "@/util/endpoints";
import { WaveDevVarName, WaveDevViteVarName } from "@/util/isdev";
import { sprintf } from "sprintf-js";
const electronApp = electron.app; const electronApp = electron.app;
const isDev = process.env.WAVETERM_DEV;
const isDevServer = !electronApp.isPackaged && process.env.ELECTRON_RENDERER_URL;
const WaveAppPathVarName = "WAVETERM_APP_PATH"; const WaveAppPathVarName = "WAVETERM_APP_PATH";
const WaveDevVarName = "WAVETERM_DEV";
const WaveSrvReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID"; const WaveSrvReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID";
const AuthKeyFile = "waveterm.authkey"; const AuthKeyFile = "waveterm.authkey";
const DevServerEndpoint = "http://127.0.0.1:8190";
const ProdServerEndpoint = "http://127.0.0.1:1719";
electron.nativeTheme.themeSource = "dark"; electron.nativeTheme.themeSource = "dark";
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> }; type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> };
let waveSrvReadyResolve = (value: boolean) => {}; let waveSrvReadyResolve = (value: boolean) => {};
let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => { const waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
waveSrvReadyResolve = resolve; waveSrvReadyResolve = resolve;
}); });
let globalIsQuitting = false; let globalIsQuitting = false;
let globalIsStarting = true; let globalIsStarting = true;
const isDev = !electron.app.isPackaged;
const isDevVite = isDev && process.env.ELECTRON_RENDERER_URL;
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null; let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave"); electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave");
const unamePlatform = process.platform; const unamePlatform = process.platform;
@ -43,16 +45,50 @@ if (unameArch == "x64") {
} }
keyutil.setKeyUtilPlatform(unamePlatform); keyutil.setKeyUtilPlatform(unamePlatform);
function getBaseHostPort(): string {
if (isDev) {
return DevServerEndpoint;
}
return ProdServerEndpoint;
}
// must match golang // must match golang
function getWaveHomeDir() { function getWaveHomeDir() {
return path.join(os.homedir(), ".w2"); return path.join(os.homedir(), isDev ? ".w2-dev" : ".w2");
}
const waveHome = getWaveHomeDir();
const oldConsoleLog = console.log;
const loggerTransports: winston.transport[] = [
new winston.transports.File({ filename: path.join(waveHome, "waveterm-app.log"), level: "info" }),
];
if (isDev) {
loggerTransports.push(new winston.transports.Console());
}
const loggerConfig = {
level: "info",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf((info) => `${info.timestamp} ${info.message}`)
),
transports: loggerTransports,
};
const logger = winston.createLogger(loggerConfig);
function log(...msg: any[]) {
try {
logger.info(util.format(...msg));
} catch (e) {
oldConsoleLog(...msg);
}
}
console.log = log;
console.log(
sprintf(
"waveterm-app starting, WAVETERM_HOME=%s, electronpath=%s gopath=%s arch=%s/%s",
waveHome,
getElectronAppBasePath(),
getGoAppBasePath(),
unamePlatform,
unameArch
)
);
if (isDev) {
console.log("waveterm-app WAVETERM_DEV set");
} }
function getElectronAppBasePath(): string { function getElectronAppBasePath(): string {
@ -60,20 +96,18 @@ function getElectronAppBasePath(): string {
} }
function getGoAppBasePath(): string { function getGoAppBasePath(): string {
const appDir = getElectronAppBasePath(); return getElectronAppBasePath().replace("app.asar", "app.asar.unpacked");
if (appDir.endsWith(".asar")) {
return `${appDir}.unpacked`;
} else {
return appDir;
}
} }
const wavesrvBinName = `wavesrv.${unameArch}`;
function getWaveSrvPath(): string { function getWaveSrvPath(): string {
return path.join(getGoAppBasePath(), "bin", "wavesrv"); return path.join(getGoAppBasePath(), "bin", wavesrvBinName);
} }
function getWaveSrvPathWin(): string { function getWaveSrvPathWin(): string {
const appPath = path.join(getGoAppBasePath(), "bin", "wavesrv.exe"); const winBinName = `${wavesrvBinName}.exe`;
const appPath = path.join(getGoAppBasePath(), "bin", winBinName);
return `& "${appPath}"`; return `& "${appPath}"`;
} }
@ -93,11 +127,16 @@ function runWaveSrv(): Promise<boolean> {
pResolve = argResolve; pResolve = argResolve;
pReject = argReject; pReject = argReject;
}); });
if (isDev) {
process.env[WaveDevVarName] = "1";
}
if (isDevVite) {
process.env[WaveDevViteVarName] = "1";
}
const envCopy = { ...process.env }; const envCopy = { ...process.env };
envCopy[WaveAppPathVarName] = getGoAppBasePath(); envCopy[WaveAppPathVarName] = getGoAppBasePath();
if (isDev) {
envCopy[WaveDevVarName] = "1";
}
envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString(); envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString();
let waveSrvCmd: string; let waveSrvCmd: string;
if (process.platform === "win32") { if (process.platform === "win32") {
@ -139,6 +178,14 @@ function runWaveSrv(): Promise<boolean> {
}); });
rlStderr.on("line", (line) => { rlStderr.on("line", (line) => {
if (line.includes("WAVESRV-ESTART")) { if (line.includes("WAVESRV-ESTART")) {
const addrs = /ws:([a-z0-9.:]+) web:([a-z0-9.:]+)/gm.exec(line);
if (addrs == null) {
console.log("error parsing WAVESRV-ESTART line", line);
electron.app.quit();
return;
}
process.env[WSServerEndpointVarName] = addrs[1];
process.env[WebServerEndpointVarName] = addrs[2];
waveSrvReadyResolve(true); waveSrvReadyResolve(true);
return; return;
} }
@ -159,12 +206,12 @@ function runWaveSrv(): Promise<boolean> {
async function handleWSEvent(evtMsg: WSEventType) { async function handleWSEvent(evtMsg: WSEventType) {
if (evtMsg.eventtype == "electron:newwindow") { if (evtMsg.eventtype == "electron:newwindow") {
let windowId: string = evtMsg.data; const windowId: string = evtMsg.data;
let windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow; const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
if (windowData == null) { if (windowData == null) {
return; return;
} }
let clientData = await services.ClientService.GetClientData(); const clientData = await services.ClientService.GetClientData();
const newWin = createBrowserWindow(clientData.oid, windowData); const newWin = createBrowserWindow(clientData.oid, windowData);
await newWin.readyPromise; await newWin.readyPromise;
newWin.show(); newWin.show();
@ -221,7 +268,7 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
} }
if ( if (
event.frame.name == "pdfview" && event.frame.name == "pdfview" &&
(url.startsWith("blob:file:///") || url.startsWith(getBaseHostPort() + "/wave/stream-file?")) (url.startsWith("blob:file:///") || url.startsWith(getServerWebEndpoint() + "/wave/stream-file?"))
) { ) {
// allowed // allowed
return; return;
@ -266,12 +313,11 @@ function createBrowserWindow(clientId: string, waveWindow: WaveWindow): WaveBrow
readyResolve = resolve; readyResolve = resolve;
}); });
const win: WaveBrowserWindow = bwin as WaveBrowserWindow; const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
// const indexHtml = isDev ? "index-dev.html" : "index.html"; const usp = new URLSearchParams();
let usp = new URLSearchParams();
usp.set("clientid", clientId); usp.set("clientid", clientId);
usp.set("windowid", waveWindow.oid); usp.set("windowid", waveWindow.oid);
const indexHtml = "index.html"; const indexHtml = "index.html";
if (isDevServer) { if (isDevVite) {
console.log("running as dev server"); console.log("running as dev server");
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html?${usp.toString()}`); win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html?${usp.toString()}`);
} else { } else {
@ -343,7 +389,7 @@ function isWindowFullyVisible(bounds: electron.Rectangle): boolean {
// Helper function to check if a point is inside any display // Helper function to check if a point is inside any display
function isPointInDisplay(x, y) { function isPointInDisplay(x, y) {
for (let display of displays) { for (const display of displays) {
const { x: dx, y: dy, width, height } = display.bounds; const { x: dx, y: dy, width, height } = display.bounds;
if (x >= dx && x < dx + width && y >= dy && y < dy + height) { if (x >= dx && x < dx + width && y >= dy && y < dy + height) {
return true; return true;
@ -418,18 +464,9 @@ function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rectangle
return bounds; return bounds;
} }
electron.ipcMain.on("isDev", (event) => {
event.returnValue = isDev;
});
electron.ipcMain.on("isDevServer", (event) => {
event.returnValue = isDevServer;
});
electron.ipcMain.on("getPlatform", (event, url) => { electron.ipcMain.on("getPlatform", (event, url) => {
event.returnValue = unamePlatform; event.returnValue = unamePlatform;
}); });
// Listen for the open-external event from the renderer process // Listen for the open-external event from the renderer process
electron.ipcMain.on("open-external", (event, url) => { electron.ipcMain.on("open-external", (event, url) => {
if (url && typeof url === "string") { if (url && typeof url === "string") {
@ -443,8 +480,7 @@ electron.ipcMain.on("open-external", (event, url) => {
electron.ipcMain.on("download", (event, payload) => { electron.ipcMain.on("download", (event, payload) => {
const window = electron.BrowserWindow.fromWebContents(event.sender); const window = electron.BrowserWindow.fromWebContents(event.sender);
const baseName = payload.filePath.split(/[\\/]/).pop(); const streamingUrl = getServerWebEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(payload.filePath);
const streamingUrl = getBackendHostPort() + "/wave/stream-file?path=" + encodeURIComponent(payload.filePath);
window.webContents.downloadURL(streamingUrl); window.webContents.downloadURL(streamingUrl);
}); });
@ -459,8 +495,12 @@ electron.ipcMain.on("getCursorPoint", (event) => {
event.returnValue = retVal; event.returnValue = retVal;
}); });
electron.ipcMain.on("getEnv", (event, varName) => {
event.returnValue = process.env[varName] ?? null;
});
async function createNewWaveWindow() { async function createNewWaveWindow() {
let clientData = await services.ClientService.GetClientData(); const clientData = await services.ClientService.GetClientData();
const newWindow = await services.ClientService.MakeWindow(); const newWindow = await services.ClientService.MakeWindow();
const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow); const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow);
newBrowserWindow.show(); newBrowserWindow.show();
@ -498,7 +538,7 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
} }
function makeAppMenu() { function makeAppMenu() {
let fileMenu: Electron.MenuItemConstructorOptions[] = []; const fileMenu: Electron.MenuItemConstructorOptions[] = [];
fileMenu.push({ fileMenu.push({
label: "New Window", label: "New Window",
accelerator: "CommandOrControl+N", accelerator: "CommandOrControl+N",
@ -557,12 +597,12 @@ async function appMain() {
const ready = await waveSrvReady; const ready = await waveSrvReady;
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms"); console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
console.log("get client data"); console.log("get client data");
let clientData = await services.ClientService.GetClientData(); const clientData = await services.ClientService.GetClientData();
console.log("client data ready"); console.log("client data ready");
await electronApp.whenReady(); await electronApp.whenReady();
let wins: WaveBrowserWindow[] = []; const wins: WaveBrowserWindow[] = [];
for (let windowId of clientData.windowids.slice().reverse()) { for (const windowId of clientData.windowids.slice().reverse()) {
let windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow; const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
if (windowData == null) { if (windowData == null) {
services.WindowService.CloseWindow(windowId).catch((e) => { services.WindowService.CloseWindow(windowId).catch((e) => {
/* ignore */ /* ignore */
@ -572,7 +612,7 @@ async function appMain() {
const win = createBrowserWindow(clientData.oid, windowData); const win = createBrowserWindow(clientData.oid, windowData);
wins.push(win); wins.push(win);
} }
for (let win of wins) { for (const win of wins) {
await win.readyPromise; await win.readyPromise;
console.log("show", win.waveWindowId); console.log("show", win.waveWindowId);
win.show(); win.show();

View File

@ -4,8 +4,6 @@
let { contextBridge, ipcRenderer } = require("electron"); let { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", { contextBridge.exposeInMainWorld("api", {
isDev: () => ipcRenderer.sendSync("isDev"),
isDevServer: () => ipcRenderer.sendSync("isDevServer"),
getPlatform: () => ipcRenderer.sendSync("getPlatform"), getPlatform: () => ipcRenderer.sendSync("getPlatform"),
getCursorPoint: () => ipcRenderer.sendSync("getCursorPoint"), getCursorPoint: () => ipcRenderer.sendSync("getCursorPoint"),
openNewWindow: () => ipcRenderer.send("openNewWindow"), openNewWindow: () => ipcRenderer.send("openNewWindow"),
@ -19,6 +17,7 @@ contextBridge.exposeInMainWorld("api", {
console.error("Invalid URL passed to openExternal:", url); console.error("Invalid URL passed to openExternal:", url);
} }
}, },
getEnv: (varName) => ipcRenderer.sendSync("getEnv", varName),
}); });
// Custom event for "new-window" // Custom event for "new-window"

View File

@ -6,6 +6,7 @@ import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { layoutTreeStateReducer } from "@/faraday/lib/layoutState"; import { layoutTreeStateReducer } from "@/faraday/lib/layoutState";
import { handleIncomingRpcMessage } from "@/app/store/wshrpc"; import { handleIncomingRpcMessage } from "@/app/store/wshrpc";
import { getServerWebEndpoint, getServerWSEndpoint } from "@/util/endpoints";
import * as layoututil from "@/util/layoututil"; import * as layoututil from "@/util/layoututil";
import { produce } from "immer"; import { produce } from "immer";
import * as jotai from "jotai"; import * as jotai from "jotai";
@ -193,15 +194,6 @@ function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom
return atom as jotai.Atom<T>; return atom as jotai.Atom<T>;
} }
function getBackendHostPort(): string {
// TODO deal with dev/production
return "http://127.0.0.1:8190";
}
function getBackendWSHostPort(): string {
return "ws://127.0.0.1:8191";
}
let globalWS: WSControl = null; let globalWS: WSControl = null;
function handleWSEventMessage(msg: WSEventType) { function handleWSEventMessage(msg: WSEventType) {
@ -278,7 +270,7 @@ function handleWSMessage(msg: any) {
} }
function initWS() { function initWS() {
globalWS = new WSControl(getBackendWSHostPort(), globalStore, globalWindowId, "", (msg) => { globalWS = new WSControl(getServerWSEndpoint(), globalStore, globalWindowId, "", (msg) => {
handleWSMessage(msg); handleWSMessage(msg);
}); });
globalWS.connectNow("initWS"); globalWS.connectNow("initWS");
@ -332,7 +324,7 @@ async function fetchWaveFile(
if (offset != null) { if (offset != null) {
usp.set("offset", offset.toString()); usp.set("offset", offset.toString());
} }
const resp = await fetch(getBackendHostPort() + "/wave/file?" + usp.toString()); const resp = await fetch(getServerWebEndpoint() + "/wave/file?" + usp.toString());
if (!resp.ok) { if (!resp.ok) {
if (resp.status === 404) { if (resp.status === 404) {
return { data: null, fileInfo: null }; return { data: null, fileInfo: null };
@ -375,13 +367,10 @@ function getObjectId(obj: any): number {
} }
export { export {
PLATFORM,
WOS,
atoms, atoms,
createBlock, createBlock,
fetchWaveFile, fetchWaveFile,
getApi, getApi,
getBackendHostPort,
getEventORefSubject, getEventORefSubject,
getEventSubject, getEventSubject,
getFileSubject, getFileSubject,
@ -389,10 +378,12 @@ export {
globalStore, globalStore,
globalWS, globalWS,
initWS, initWS,
PLATFORM,
sendWSCommand, sendWSCommand,
setBlockFocus, setBlockFocus,
setPlatform, setPlatform,
useBlockAtom, useBlockAtom,
useBlockCache, useBlockCache,
useSettingsAtom, useSettingsAtom,
WOS,
}; };

View File

@ -4,10 +4,11 @@
// WaveObjectStore // WaveObjectStore
import { sendRpcCommand } from "@/app/store/wshrpc"; import { sendRpcCommand } from "@/app/store/wshrpc";
import { getServerWebEndpoint } from "@/util/endpoints";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as React from "react"; import * as React from "react";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { atoms, getBackendHostPort, globalStore } from "./global"; import { atoms, globalStore } from "./global";
import * as services from "./services"; import * as services from "./services";
const IsElectron = true; const IsElectron = true;
@ -67,22 +68,23 @@ function callBackendService(service: string, method: string, args: any[], noUICo
if (!noUIContext) { if (!noUIContext) {
uiContext = globalStore.get(atoms.uiContext); uiContext = globalStore.get(atoms.uiContext);
} }
let waveCall: WebCallType = { const waveCall: WebCallType = {
service: service, service: service,
method: method, method: method,
args: args, args: args,
uicontext: uiContext, uicontext: uiContext,
}; };
// usp is just for debugging (easier to filter URLs) // usp is just for debugging (easier to filter URLs)
let methodName = service + "." + method; const methodName = `${service}.${method}`;
let usp = new URLSearchParams(); const usp = new URLSearchParams();
usp.set("service", service); usp.set("service", service);
usp.set("method", method); usp.set("method", method);
let fetchPromise = fetch(getBackendHostPort() + "/wave/service?" + usp.toString(), { const url = getServerWebEndpoint() + "/wave/service?" + usp.toString();
const fetchPromise = fetch(url, {
method: "POST", method: "POST",
body: JSON.stringify(waveCall), body: JSON.stringify(waveCall),
}); });
let prtn = fetchPromise const prtn = fetchPromise
.then((resp) => { .then((resp) => {
if (!resp.ok) { if (!resp.ok) {
throw new Error(`call ${methodName} failed: ${resp.status} ${resp.statusText}`); throw new Error(`call ${methodName} failed: ${resp.status} ${resp.statusText}`);

View File

@ -3,7 +3,6 @@
import { WindowDrag } from "@/element/windowdrag"; import { WindowDrag } from "@/element/windowdrag";
import { deleteLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; import { deleteLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { debounce } from "@/faraday/lib/utils";
import { atoms } from "@/store/global"; import { atoms } from "@/store/global";
import * as services from "@/store/services"; import * as services from "@/store/services";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
@ -12,6 +11,7 @@ import React, { createRef, useCallback, useEffect, useRef, useState } from "reac
import { Tab } from "./tab"; import { Tab } from "./tab";
import { debounce } from "throttle-debounce";
import "./tabbar.less"; import "./tabbar.less";
const TAB_DEFAULT_WIDTH = 130; const TAB_DEFAULT_WIDTH = 130;
@ -111,7 +111,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
// const debouncedSetTabWidth = debounce((width) => setTabWidth(width), 100); // const debouncedSetTabWidth = debounce((width) => setTabWidth(width), 100);
// const debouncedSetScrollable = debounce((scrollable) => setScrollable(scrollable), 100); // const debouncedSetScrollable = debounce((scrollable) => setScrollable(scrollable), 100);
const debouncedUpdateTabPositions = debounce(() => updateTabPositions(), 100); const debouncedUpdateTabPositions = debounce(100, () => updateTabPositions());
const handleResizeTabs = useCallback(() => { const handleResizeTabs = useCallback(() => {
const tabBar = tabBarRef.current; const tabBar = tabBarRef.current;

View File

@ -3,9 +3,10 @@
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { Markdown } from "@/element/markdown"; import { Markdown } from "@/element/markdown";
import { getBackendHostPort, globalStore, useBlockAtom } from "@/store/global"; import { globalStore, useBlockAtom } from "@/store/global";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import { getServerWebEndpoint } from "@/util/endpoints";
import * as util from "@/util/util"; import * as util from "@/util/util";
import clsx from "clsx"; import clsx from "clsx";
import * as jotai from "jotai"; import * as jotai from "jotai";
@ -274,7 +275,7 @@ function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom<Promise<stri
function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) { function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) {
const filePath = fileInfo.path; const filePath = fileInfo.path;
const streamingUrl = getBackendHostPort() + "/wave/stream-file?path=" + encodeURIComponent(filePath); const streamingUrl = getServerWebEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(filePath);
if (fileInfo.mimetype == "application/pdf") { if (fileInfo.mimetype == "application/pdf") {
return ( return (
<div className="view-preview view-preview-pdf"> <div className="view-preview view-preview-pdf">

View File

@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { WshServer } from "@/app/store/wshserver"; import { WshServer } from "@/app/store/wshserver";
import { createBlock, getBackendHostPort } from "@/store/global"; import { createBlock } from "@/store/global";
import { getServerWebEndpoint } from "@/util/endpoints";
import clsx from "clsx"; import clsx from "clsx";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as React from "react"; import * as React from "react";
@ -116,7 +117,7 @@ function TermSticker({ sticker, config }: { sticker: StickerType; config: Sticke
if (sticker.imgsrc == null) { if (sticker.imgsrc == null) {
return null; return null;
} }
const streamingUrl = getBackendHostPort() + "/wave/stream-file?path=" + encodeURIComponent(sticker.imgsrc); const streamingUrl = getServerWebEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(sticker.imgsrc);
return ( return (
<div className="term-sticker term-sticker-image" style={style} onClick={clickHandler}> <div className="term-sticker term-sticker-image" style={style} onClick={clickHandler}>
<img src={streamingUrl} /> <img src={streamingUrl} />

View File

@ -1,8 +1,9 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { getCrypto } from "@/util/util";
import { DefaultNodeSize, LayoutNode } from "./model"; import { DefaultNodeSize, LayoutNode } from "./model";
import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils"; import { FlexDirection, reverseFlexDirection } from "./utils";
const crypto = getCrypto(); const crypto = getCrypto();

View File

@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { lazy } from "@/util/util";
import { import {
addChildAt, addChildAt,
addIntermediateNode, addIntermediateNode,
@ -25,7 +26,7 @@ import {
LayoutTreeSwapNodeAction, LayoutTreeSwapNodeAction,
MoveOperation, MoveOperation,
} from "./model"; } from "./model";
import { DropDirection, FlexDirection, lazy } from "./utils"; import { DropDirection, FlexDirection } from "./utils";
/** /**
* Initializes a layout tree state. * Initializes a layout tree state.

View File

@ -97,43 +97,3 @@ export function setTransform({ top, left, width, height }: Dimensions, setSize:
position: "absolute", position: "absolute",
}; };
} }
export const debounce = <T extends (...args: any[]) => any>(callback: T, waitFor: number) => {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>): ReturnType<T> => {
let result: any;
clearTimeout(timeout);
timeout = setTimeout(() => {
result = callback(...args);
}, waitFor);
return result;
};
};
/**
* Simple wrapper function that lazily evaluates the provided function and caches its result for future calls.
* @param callback The function to lazily run.
* @returns The result of the function.
*/
export const lazy = <T extends (...args: any[]) => any>(callback: T) => {
let res: ReturnType<T>;
let processed = false;
return (...args: Parameters<T>): ReturnType<T> => {
if (processed) return res;
res = callback(...args);
processed = true;
return res;
};
};
/**
* Workaround for NodeJS compatibility. Will attempt to resolve the Crypto API from the browser and fallback to NodeJS if it isn't present.
* @returns The Crypto API.
*/
export function getCrypto() {
try {
return window.crypto;
} catch {
return crypto;
}
}

View File

@ -17,23 +17,10 @@ declare global {
}; };
type ElectronApi = { type ElectronApi = {
/**
* Determines whether the current app instance is a development build.
* @returns True if the current app instance is a development build.
*/
isDev: () => boolean;
/**
* Determines whether the current app instance is hosted in a Vite dev server.
* @returns True if the current app instance is hosted in a Vite dev server.
*/
isDevServer: () => boolean;
/**
* Get a point value representing the cursor's position relative to the calling BrowserWindow
* @returns A point value.
*/
getCursorPoint: () => Electron.Point; getCursorPoint: () => Electron.Point;
getPlatform: () => NodeJS.Platform; getPlatform: () => NodeJS.Platform;
getEnv: (varName: string) => string;
showContextMenu: (menu: ElectronContextMenuItem[], position: { x: number; y: number }) => void; showContextMenu: (menu: ElectronContextMenuItem[], position: { x: number; y: number }) => void;
onContextMenuClick: (callback: (id: string) => void) => void; onContextMenuClick: (callback: (id: string) => void) => void;

View File

@ -15,24 +15,6 @@ declare global {
meta: MetaType; meta: MetaType;
}; };
// wshutil.BlockAppendFileCommand
type BlockAppendFileCommand = {
command: "blockfile:append";
filename: string;
data: number[];
};
// wshutil.BlockAppendIJsonCommand
type BlockAppendIJsonCommand = {
command: "blockfile:appendijson";
filename: string;
data: MetaType;
};
type BlockCommand = {
command: string;
} & ( BlockAppendFileCommand | BlockAppendIJsonCommand | BlockInputCommand | BlockRestartCommand | CreateBlockCommand | BlockGetMetaCommand | BlockMessageCommand | ResolveIdsCommand | BlockSetMetaCommand | BlockSetViewCommand );
// blockcontroller.BlockControllerRuntimeStatus // blockcontroller.BlockControllerRuntimeStatus
type BlockControllerRuntimeStatus = { type BlockControllerRuntimeStatus = {
blockid: string; blockid: string;
@ -48,26 +30,11 @@ declare global {
meta?: MetaType; meta?: MetaType;
}; };
// wshutil.BlockGetMetaCommand
type BlockGetMetaCommand = {
command: "getmeta";
oref: string;
};
// wconfig.BlockHeaderOpts // wconfig.BlockHeaderOpts
type BlockHeaderOpts = { type BlockHeaderOpts = {
showblockids: boolean; showblockids: boolean;
}; };
// wshutil.BlockInputCommand
type BlockInputCommand = {
blockid: string;
command: "controller:input";
inputdata64?: string;
signame?: string;
termsize?: TermSize;
};
// webcmd.BlockInputWSCommand // webcmd.BlockInputWSCommand
type BlockInputWSCommand = { type BlockInputWSCommand = {
wscommand: "blockinput"; wscommand: "blockinput";
@ -75,31 +42,6 @@ declare global {
inputdata64: string; inputdata64: string;
}; };
// wshutil.BlockMessageCommand
type BlockMessageCommand = {
command: "message";
message: string;
};
// wshutil.BlockRestartCommand
type BlockRestartCommand = {
command: "controller:restart";
blockid: string;
};
// wshutil.BlockSetMetaCommand
type BlockSetMetaCommand = {
command: "setmeta";
oref?: string;
meta: MetaType;
};
// wshutil.BlockSetViewCommand
type BlockSetViewCommand = {
command: "setview";
view: string;
};
// wstore.Client // wstore.Client
type Client = WaveObj & { type Client = WaveObj & {
mainwindowid: string; mainwindowid: string;
@ -174,14 +116,6 @@ declare global {
meta: MetaType; meta: MetaType;
}; };
// wshutil.CreateBlockCommand
type CreateBlockCommand = {
command: "createblock";
tabid: string;
blockdef: BlockDef;
rtopts?: RuntimeOpts;
};
// wstore.FileDef // wstore.FileDef
type FileDef = { type FileDef = {
filetype?: string; filetype?: string;
@ -248,12 +182,6 @@ declare global {
y: number; y: number;
}; };
// wshutil.ResolveIdsCommand
type ResolveIdsCommand = {
command: "resolveids";
ids: string[];
};
// wshutil.RpcMessage // wshutil.RpcMessage
type RpcMessage = { type RpcMessage = {
command?: string; command?: string;

View File

@ -0,0 +1,9 @@
import { getEnv } from "./getenv";
import { lazy } from "./util";
export const WebServerEndpointVarName = "WAVE_SERVER_WEB_ENDPOINT";
export const WSServerEndpointVarName = "WAVE_SERVER_WS_ENDPOINT";
export const getServerWebEndpoint = lazy(() => `http://${getEnv(WebServerEndpointVarName)}`);
export const getServerWSEndpoint = lazy(() => `ws://${getEnv(WSServerEndpointVarName)}`);

26
frontend/util/getenv.ts Normal file
View File

@ -0,0 +1,26 @@
import { getApi } from "@/app/store/global";
function getWindow(): Window {
return globalThis.window;
}
function getProcess(): NodeJS.Process {
return globalThis.process;
}
/**
* Gets an environment variable from the host process, either directly or via IPC if called from the browser.
* @param paramName The name of the environment variable to attempt to retrieve.
* @returns The value of the environment variable or null if not present.
*/
export function getEnv(paramName: string): string {
const win = getWindow();
if (win != null) {
return getApi().getEnv(paramName);
}
const proc = getProcess();
if (proc != null) {
return proc.env[paramName];
}
return null;
}

17
frontend/util/isdev.ts Normal file
View File

@ -0,0 +1,17 @@
import { getEnv } from "./getenv";
import { lazy } from "./util";
export const WaveDevVarName = "WAVETERM_DEV";
export const WaveDevViteVarName = "WAVETERM_DEV_VITE";
/**
* Determines whether the current app instance is a development build.
* @returns True if the current app instance is a development build.
*/
export const isDev = lazy(() => !!getEnv(WaveDevVarName));
/**
* Determines whether the current app instance is running via the Vite dev server.
* @returns True if the app is running via the Vite dev server.
*/
export const isDevVite = lazy(() => !!getEnv(WaveDevViteVarName));

View File

@ -162,15 +162,45 @@ function useAtomValueSafe<T>(atom: jotai.Atom<T>): T {
return jotai.useAtomValue(atom); return jotai.useAtomValue(atom);
} }
/**
* Simple wrapper function that lazily evaluates the provided function and caches its result for future calls.
* @param callback The function to lazily run.
* @returns The result of the function.
*/
const lazy = <T extends (...args: any[]) => any>(callback: T) => {
let res: ReturnType<T>;
let processed = false;
return (...args: Parameters<T>): ReturnType<T> => {
if (processed) return res;
res = callback(...args);
processed = true;
return res;
};
};
/**
* Workaround for NodeJS compatibility. Will attempt to resolve the Crypto API from the browser and fallback to NodeJS if it isn't present.
* @returns The Crypto API.
*/
function getCrypto() {
try {
return window.crypto;
} catch {
return crypto;
}
}
export { export {
base64ToArray, base64ToArray,
base64ToString, base64ToString,
fireAndForget, fireAndForget,
getCrypto,
getPromiseState, getPromiseState,
getPromiseValue, getPromiseValue,
isBlank, isBlank,
jotaiLoadableValue, jotaiLoadableValue,
jsonDeepEqual, jsonDeepEqual,
lazy,
makeIconClass, makeIconClass,
stringToBase64, stringToBase64,
useAtomValueSafe, useAtomValueSafe,

View File

@ -12,13 +12,13 @@ import { App } from "./app/app";
import { loadFonts } from "./util/fontutil"; import { loadFonts } from "./util/fontutil";
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
let windowId = urlParams.get("windowid"); const windowId = urlParams.get("windowid");
let clientId = urlParams.get("clientid"); const clientId = urlParams.get("clientid");
console.log("Wave Starting"); console.log("Wave Starting");
console.log("clientid", clientId, "windowid", windowId); console.log("clientid", clientId, "windowid", windowId);
let platform = getApi().getPlatform(); const platform = getApi().getPlatform();
setPlatform(platform); setPlatform(platform);
keyutil.setKeyUtilPlatform(platform); keyutil.setKeyUtilPlatform(platform);

View File

@ -1,7 +1,18 @@
{ {
"name": "thenextwave", "name": "thenextwave",
"private": true, "author": {
"name": "Command Line Inc",
"email": "info@commandline.dev"
},
"productName": "TheNextWave",
"description": "An Open-Source, AI-Native, Terminal Built for Seamless Workflows",
"license": "Apache-2.0",
"version": "0.0.0", "version": "0.0.0",
"homepage": "https://waveterm.dev",
"build": {
"appId": "dev.commandline.thenextwave"
},
"private": true,
"main": "./dist/main/index.js", "main": "./dist/main/index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -12,7 +23,8 @@
"storybook": "storybook dev -p 6006 --no-open", "storybook": "storybook dev -p 6006 --no-open",
"build-storybook": "storybook build", "build-storybook": "storybook build",
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"test": "vitest" "test": "vitest",
"postinstall": "electron-builder install-app-deps"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^1.5.0", "@chromatic-com/storybook": "^1.5.0",
@ -27,11 +39,14 @@
"@types/node": "^20.12.12", "@types/node": "^20.12.12",
"@types/papaparse": "^5", "@types/papaparse": "^5",
"@types/react": "^18.3.2", "@types/react": "^18.3.2",
"@types/sprintf-js": "^1",
"@types/throttle-debounce": "^5", "@types/throttle-debounce": "^5",
"@types/tinycolor2": "^1", "@types/tinycolor2": "^1",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.3.0", "@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-istanbul": "^1.6.0", "@vitest/coverage-istanbul": "^1.6.0",
"electron": "^31.1.0",
"electron-builder": "^24.13.3",
"electron-vite": "^2.2.0", "electron-vite": "^2.2.0",
"eslint": "^9.2.0", "eslint": "^9.2.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -65,7 +80,6 @@
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"electron": "^31.1.0",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"immer": "^10.1.1", "immer": "^10.1.1",
"jotai": "^2.8.0", "jotai": "^2.8.0",
@ -83,10 +97,12 @@
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sprintf-js": "^1.1.3",
"throttle-debounce": "^5.0.0", "throttle-debounce": "^5.0.0",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"use-device-pixel-ratio": "^1.1.2", "use-device-pixel-ratio": "^1.1.2",
"uuid": "^9.0.1" "uuid": "^9.0.1",
"winston": "^3.13.1"
}, },
"packageManager": "yarn@4.3.1" "packageManager": "yarn@4.3.1"
} }

View File

@ -22,6 +22,7 @@ import (
const WaveVersion = "v0.1.0" const WaveVersion = "v0.1.0"
const DefaultWaveHome = "~/.w2" const DefaultWaveHome = "~/.w2"
const DevWaveHome = "~/.w2-dev"
const WaveHomeVarName = "WAVETERM_HOME" const WaveHomeVarName = "WAVETERM_HOME"
const WaveDevVarName = "WAVETERM_DEV" const WaveDevVarName = "WAVETERM_DEV"
const WaveLockFile = "waveterm.lock" const WaveLockFile = "waveterm.lock"
@ -74,6 +75,9 @@ func GetWaveHomeDir() string {
if homeVar != "" { if homeVar != "" {
return ExpandHomeDir(homeVar) return ExpandHomeDir(homeVar)
} }
if IsDevMode() {
return ExpandHomeDir(DevWaveHome)
}
return ExpandHomeDir(DefaultWaveHome) return ExpandHomeDir(DefaultWaveHome)
} }

View File

@ -47,10 +47,6 @@ const HttpWriteTimeout = 21 * time.Second
const HttpMaxHeaderBytes = 60000 const HttpMaxHeaderBytes = 60000
const HttpTimeoutDuration = 21 * time.Second const HttpTimeoutDuration = 21 * time.Second
const MainServerAddr = "127.0.0.1:1719" // wavesrv, P=16+1, S=19, PS=1719
const WebSocketServerAddr = "127.0.0.1:1723" // wavesrv:websocket, P=16+1, W=23, PW=1723
const MainServerDevAddr = "127.0.0.1:8190"
const WebSocketServerDevAddr = "127.0.0.1:8191"
const WSStateReconnectTime = 30 * time.Second const WSStateReconnectTime = 30 * time.Second
const WSStatePacketChSize = 20 const WSStatePacketChSize = 20
@ -213,16 +209,13 @@ func WebFnWrap(opts WebFnOpts, fn WebFnType) WebFnType {
} }
} }
func MakeTCPListener() (net.Listener, error) { func MakeTCPListener(serviceName string) (net.Listener, error) {
serverAddr := MainServerAddr serverAddr := "127.0.0.1:"
if wavebase.IsDevMode() {
serverAddr = MainServerDevAddr
}
rtn, err := net.Listen("tcp", serverAddr) rtn, err := net.Listen("tcp", serverAddr)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating listener at %v: %v", serverAddr, err) return nil, fmt.Errorf("error creating listener at %v: %v", serverAddr, err)
} }
log.Printf("Server listening on %s\n", serverAddr) log.Printf("Server [%s] listening on %s\n", serviceName, rtn.Addr())
return rtn, nil return rtn, nil
} }
@ -234,7 +227,7 @@ func MakeUnixListener() (net.Listener, error) {
return nil, fmt.Errorf("error creating listener at %v: %v", serverAddr, err) return nil, fmt.Errorf("error creating listener at %v: %v", serverAddr, err)
} }
os.Chmod(serverAddr, 0700) os.Chmod(serverAddr, 0700)
log.Printf("Server listening on %s\n", serverAddr) log.Printf("Server [unix-domain] listening on %s\n", serverAddr)
return rtn, nil return rtn, nil
} }
@ -244,15 +237,15 @@ func RunWebServer(listener net.Listener) {
gr.HandleFunc("/wave/stream-file", WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamFile)) gr.HandleFunc("/wave/stream-file", WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamFile))
gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile)) gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile))
gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService)) gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService))
var allowedOrigins handlers.CORSOption handler := http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout")
if wavebase.IsDevMode() { if wavebase.IsDevMode() {
allowedOrigins = handlers.AllowedOrigins([]string{"*"}) handler = handlers.CORS(handlers.AllowedOrigins([]string{"*"}))(handler)
} }
server := &http.Server{ server := &http.Server{
ReadTimeout: HttpReadTimeout, ReadTimeout: HttpReadTimeout,
WriteTimeout: HttpWriteTimeout, WriteTimeout: HttpWriteTimeout,
MaxHeaderBytes: HttpMaxHeaderBytes, MaxHeaderBytes: HttpMaxHeaderBytes,
Handler: handlers.CORS(allowedOrigins)(http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout")), Handler: handler,
} }
err := server.Serve(listener) err := server.Serve(listener)
if err != nil { if err != nil {

View File

@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"net"
"net/http" "net/http"
"runtime/debug" "runtime/debug"
"sync" "sync"
@ -31,20 +32,18 @@ const wsInitialPingTime = 1 * time.Second
const DefaultCommandTimeout = 2 * time.Second const DefaultCommandTimeout = 2 * time.Second
func RunWebSocketServer() { func RunWebSocketServer(listener net.Listener) {
gr := mux.NewRouter() gr := mux.NewRouter()
gr.HandleFunc("/ws", HandleWs) gr.HandleFunc("/ws", HandleWs)
serverAddr := WebSocketServerDevAddr
server := &http.Server{ server := &http.Server{
Addr: serverAddr,
ReadTimeout: HttpReadTimeout, ReadTimeout: HttpReadTimeout,
WriteTimeout: HttpWriteTimeout, WriteTimeout: HttpWriteTimeout,
MaxHeaderBytes: HttpMaxHeaderBytes, MaxHeaderBytes: HttpMaxHeaderBytes,
Handler: gr, Handler: gr,
} }
server.SetKeepAlivesEnabled(false) server.SetKeepAlivesEnabled(false)
log.Printf("Running websocket server on %s\n", serverAddr) log.Printf("Running websocket server on %s\n", listener.Addr())
err := server.ListenAndServe() err := server.Serve(listener)
if err != nil { if err != nil {
log.Printf("[error] trying to run websocket server: %v\n", err) log.Printf("[error] trying to run websocket server: %v\n", err)
} }

1041
yarn.lock

File diff suppressed because it is too large Load Diff