Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
4b641a33
提交
4b641a33
authored
4月 12, 2025
作者:
Will Chen
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Infra for serving sandpack locally
上级
a4a763a0
显示空白字符变更
内嵌
并排
正在显示
3 个修改的文件
包含
394 行增加
和
16 行删除
+394
-16
PreviewIframe.tsx
src/components/preview_panel/PreviewIframe.tsx
+4
-6
app_handlers.ts
src/ipc/handlers/app_handlers.ts
+114
-10
static_file_server.js
worker/static_file_server.js
+276
-0
没有找到文件。
src/components/preview_panel/PreviewIframe.tsx
浏览文件 @
4b641a33
...
@@ -442,22 +442,20 @@ const SandpackIframe = ({ reloadKey }: { reloadKey: number }) => {
...
@@ -442,22 +442,20 @@ const SandpackIframe = ({ reloadKey }: { reloadKey: number }) => {
if
(
keyRef
.
current
===
reloadKey
)
return
;
if
(
keyRef
.
current
===
reloadKey
)
return
;
keyRef
.
current
=
reloadKey
;
keyRef
.
current
=
reloadKey
;
if
(
!
iframeRef
.
current
||
!
app
||
!
selectedAppId
)
return
;
if
(
!
selectedAppId
)
return
;
const
sandboxConfig
=
await
IpcClient
.
getInstance
().
getAppSandboxConfig
(
const
sandboxConfig
=
await
IpcClient
.
getInstance
().
getAppSandboxConfig
(
selectedAppId
selectedAppId
);
);
if
(
!
iframeRef
.
current
||
!
app
)
return
;
const
sandpackConfig
:
SandboxSetup
=
mapSandpackConfig
(
sandboxConfig
);
const
sandpackConfig
:
SandboxSetup
=
mapSandpackConfig
(
sandboxConfig
);
const
options
:
ClientOptions
=
{
const
options
:
ClientOptions
=
{
// bundlerURL: "https://sandpack.dyad.sh/
",
bundlerURL
:
"http://localhost:31111
"
,
showOpenInCodeSandbox
:
false
,
showOpenInCodeSandbox
:
false
,
showLoadingScreen
:
true
,
showLoadingScreen
:
true
,
showErrorScreen
:
true
,
showErrorScreen
:
true
,
externalResources
:
[
externalResources
:
[
"https://cdn.tailwindcss.com"
],
// "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4",
"https://cdn.tailwindcss.com"
,
],
};
};
let
client
:
SandpackClient
|
undefined
;
let
client
:
SandpackClient
|
undefined
;
...
...
src/ipc/handlers/app_handlers.ts
浏览文件 @
4b641a33
...
@@ -31,25 +31,103 @@ import {
...
@@ -31,25 +31,103 @@ import {
import
{
ALLOWED_ENV_VARS
}
from
"../../constants/models"
;
import
{
ALLOWED_ENV_VARS
}
from
"../../constants/models"
;
import
{
getEnvVar
}
from
"../utils/read_env"
;
import
{
getEnvVar
}
from
"../utils/read_env"
;
import
{
readSettings
}
from
"../../main/settings"
;
import
{
readSettings
}
from
"../../main/settings"
;
import
{
Worker
}
from
"worker_threads"
;
// Keep track of the static file server worker
let
staticServerWorker
:
Worker
|
null
=
null
;
let
staticServerPort
:
number
|
null
=
null
;
// let staticServerRootDir: string | null = null; // Store the root dir it's serving - Removed
async
function
executeApp
({
async
function
executeApp
({
appPath
,
appPath
,
appId
,
appId
,
event
,
event
,
// Keep event for local-node case
}:
{
}:
{
appPath
:
string
;
appPath
:
string
;
appId
:
number
;
appId
:
number
;
event
:
Electron
.
IpcMainInvokeEvent
;
event
:
Electron
.
IpcMainInvokeEvent
;
}):
Promise
<
void
>
{
}):
Promise
<
void
>
{
// Return type is void, communication happens via event.sender.send
const
settings
=
readSettings
();
const
settings
=
readSettings
();
if
(
settings
.
runtimeMode
===
"web-sandbox"
)
{
if
(
settings
.
runtimeMode
===
"web-sandbox"
)
{
// If server is already running, do nothing.
if
(
staticServerWorker
)
{
console
.
log
(
`Static server already running on port
${
staticServerPort
}
`
);
// No need to send app:output here
return
;
return
;
}
}
// Start the worker if it's not running
console
.
log
(
`Starting static file server worker for the first time.`
);
// No need to send starting status
const
workerScriptPath
=
path
.
resolve
(
__dirname
,
"../../worker/static_file_server.js"
);
// Check if worker script exists
if
(
!
fs
.
existsSync
(
workerScriptPath
))
{
const
errorMsg
=
`Worker script not found at
${
workerScriptPath
}
. Build process might be incomplete.`
;
console
.
error
(
errorMsg
);
// No need to send error status via event
throw
new
Error
(
errorMsg
);
}
staticServerWorker
=
new
Worker
(
workerScriptPath
,
{
workerData
:
{
rootDir
:
path
.
join
(
__dirname
,
".."
,
".."
,
"sandpack-generated"
),
// Use the appPath of the first app run in this mode
// Optionally pass other config like port preference
// port: 3001 // Example
},
});
// staticServerRootDir = appPath; // Removed
staticServerWorker
.
on
(
"message"
,
(
message
)
=>
{
console
.
log
(
`Message from static server worker:
${
JSON
.
stringify
(
message
)}
`
);
if
(
message
.
status
===
"ready"
&&
message
.
port
)
{
staticServerPort
=
message
.
port
;
console
.
log
(
`Static file server ready on port
${
staticServerPort
}
`
);
// No need to send ready status
}
else
if
(
message
.
status
===
"error"
)
{
console
.
error
(
`Static file server worker error:
${
message
.
message
}
`
);
// No need to send error status
// Terminate the failed worker
staticServerWorker
?.
terminate
();
staticServerWorker
=
null
;
staticServerPort
=
null
;
}
});
staticServerWorker
.
on
(
"error"
,
(
error
)
=>
{
console
.
error
(
`Static file server worker encountered an error:`
,
error
);
// No need to send error status
staticServerWorker
=
null
;
// Worker is likely unusable
staticServerPort
=
null
;
});
staticServerWorker
.
on
(
"exit"
,
(
code
)
=>
{
console
.
log
(
`Static file server worker exited with code
${
code
}
`
);
// Clear state if the worker exits unexpectedly
if
(
staticServerWorker
)
{
// Check avoids race condition if terminated intentionally
staticServerWorker
=
null
;
staticServerPort
=
null
;
// No need to send exit status
}
});
return
;
// Return void
}
if
(
settings
.
runtimeMode
===
"local-node"
)
{
if
(
settings
.
runtimeMode
===
"local-node"
)
{
// Ensure worker isn't running if switching modes (optional, depends on desired behavior)
// if (staticServerWorker) { await staticServerWorker.terminate(); staticServerWorker = null; staticServerPort = null; }
await
executeAppLocalNode
({
appPath
,
appId
,
event
});
await
executeAppLocalNode
({
appPath
,
appId
,
event
});
return
;
return
;
}
}
throw
new
Error
(
"Invalid runtime mode"
);
throw
new
Error
(
`Invalid runtime mode:
${
settings
.
runtimeMode
}
`
);
}
}
async
function
executeAppLocalNode
({
async
function
executeAppLocalNode
({
appPath
,
appPath
,
...
@@ -350,19 +428,23 @@ export function registerAppHandlers() {
...
@@ -350,19 +428,23 @@ export function registerAppHandlers() {
ipcMain
.
handle
(
"stop-app"
,
async
(
_
,
{
appId
}:
{
appId
:
number
})
=>
{
ipcMain
.
handle
(
"stop-app"
,
async
(
_
,
{
appId
}:
{
appId
:
number
})
=>
{
console
.
log
(
console
.
log
(
`Attempting to stop app
${
appId
}
. Current running apps:
${
runningApps
.
size
}
`
`Attempting to stop app
${
appId
}
(local-node only)
. Current running apps:
${
runningApps
.
size
}
`
);
);
// Use withLock to ensure atomicity of the stop operation
// Static server worker is NOT terminated here anymore
// Use withLock for local-node apps
return
withLock
(
appId
,
async
()
=>
{
return
withLock
(
appId
,
async
()
=>
{
const
appInfo
=
runningApps
.
get
(
appId
);
const
appInfo
=
runningApps
.
get
(
appId
);
if
(
!
appInfo
)
{
if
(
!
appInfo
)
{
console
.
log
(
console
.
log
(
`App
${
appId
}
not found in running apps map
. Assuming already stopped
.`
`App
${
appId
}
not found in running apps map
(local-node). Assuming already stopped or was web-sandbox
.`
);
);
// If no local-node app was running, and we terminated the static server above, return success.
return
{
return
{
success
:
true
,
success
:
true
,
message
:
"App not running
or already stopped."
,
message
:
"App not running
in local-node mode."
,
// Simplified message
};
};
}
}
...
@@ -406,6 +488,8 @@ export function registerAppHandlers() {
...
@@ -406,6 +488,8 @@ export function registerAppHandlers() {
event
:
Electron
.
IpcMainInvokeEvent
,
event
:
Electron
.
IpcMainInvokeEvent
,
{
appId
}:
{
appId
:
number
}
{
appId
}:
{
appId
:
number
}
)
=>
{
)
=>
{
// Static server worker is NOT terminated here anymore
return
withLock
(
appId
,
async
()
=>
{
return
withLock
(
appId
,
async
()
=>
{
try
{
try
{
// First stop the app if it's running
// First stop the app if it's running
...
@@ -413,7 +497,7 @@ export function registerAppHandlers() {
...
@@ -413,7 +497,7 @@ export function registerAppHandlers() {
if
(
appInfo
)
{
if
(
appInfo
)
{
const
{
process
,
processId
}
=
appInfo
;
const
{
process
,
processId
}
=
appInfo
;
console
.
log
(
console
.
log
(
`Stopping
app
${
appId
}
(processId
${
processId
}
) before restart`
`Stopping
local-node app
${
appId
}
(processId
${
processId
}
) before restart`
// Adjusted log
);
);
// Use the killProcess utility to stop the process
// Use the killProcess utility to stop the process
...
@@ -421,6 +505,10 @@ export function registerAppHandlers() {
...
@@ -421,6 +505,10 @@ export function registerAppHandlers() {
// Remove from running apps
// Remove from running apps
runningApps
.
delete
(
appId
);
runningApps
.
delete
(
appId
);
}
else
{
console
.
log
(
`App
${
appId
}
not running in local-node mode, proceeding to start.`
);
}
}
// Now start the app again
// Now start the app again
...
@@ -433,9 +521,11 @@ export function registerAppHandlers() {
...
@@ -433,9 +521,11 @@ export function registerAppHandlers() {
}
}
const
appPath
=
getDyadAppPath
(
app
.
path
);
const
appPath
=
getDyadAppPath
(
app
.
path
);
console
.
debug
(
`Starting app
${
appId
}
in path
${
app
.
path
}
`
);
console
.
debug
(
`Executing app
${
appId
}
in path
${
app
.
path
}
after restart request`
);
// Adjusted log
await
executeApp
({
appPath
,
appId
,
event
});
await
executeApp
({
appPath
,
appId
,
event
});
// This will handle starting either mode
return
{
success
:
true
};
return
{
success
:
true
};
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -717,6 +807,8 @@ export function registerAppHandlers() {
...
@@ -717,6 +807,8 @@ export function registerAppHandlers() {
);
);
ipcMain
.
handle
(
"delete-app"
,
async
(
_
,
{
appId
}:
{
appId
:
number
})
=>
{
ipcMain
.
handle
(
"delete-app"
,
async
(
_
,
{
appId
}:
{
appId
:
number
})
=>
{
// Static server worker is NOT terminated here anymore
return
withLock
(
appId
,
async
()
=>
{
return
withLock
(
appId
,
async
()
=>
{
// Check if app exists
// Check if app exists
const
app
=
await
db
.
query
.
apps
.
findFirst
({
const
app
=
await
db
.
query
.
apps
.
findFirst
({
...
@@ -731,10 +823,14 @@ export function registerAppHandlers() {
...
@@ -731,10 +823,14 @@ export function registerAppHandlers() {
if
(
runningApps
.
has
(
appId
))
{
if
(
runningApps
.
has
(
appId
))
{
const
appInfo
=
runningApps
.
get
(
appId
)
!
;
const
appInfo
=
runningApps
.
get
(
appId
)
!
;
try
{
try
{
console
.
log
(
`Stopping local-node app
${
appId
}
before deletion.`
);
// Adjusted log
await
killProcess
(
appInfo
.
process
);
await
killProcess
(
appInfo
.
process
);
runningApps
.
delete
(
appId
);
runningApps
.
delete
(
appId
);
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
console
.
error
(
`Error stopping app
${
appId
}
before deletion:`
,
error
);
console
.
error
(
`Error stopping local-node app
${
appId
}
before deletion:`
,
error
);
// Adjusted log
// Continue with deletion even if stopping fails
// Continue with deletion even if stopping fails
}
}
}
}
...
@@ -876,6 +972,14 @@ export function registerAppHandlers() {
...
@@ -876,6 +972,14 @@ export function registerAppHandlers() {
);
);
ipcMain
.
handle
(
"reset-all"
,
async
()
=>
{
ipcMain
.
handle
(
"reset-all"
,
async
()
=>
{
// Terminate static server worker if it's running
if
(
staticServerWorker
)
{
console
.
log
(
`Terminating static server worker on reset-all command.`
);
await
staticServerWorker
.
terminate
();
staticServerWorker
=
null
;
staticServerPort
=
null
;
staticServerRootDir
=
null
;
}
// Stop all running apps first
// Stop all running apps first
const
runningAppIds
=
Array
.
from
(
runningApps
.
keys
());
const
runningAppIds
=
Array
.
from
(
runningApps
.
keys
());
for
(
const
appId
of
runningAppIds
)
{
for
(
const
appId
of
runningAppIds
)
{
...
...
worker/static_file_server.js
0 → 100644
浏览文件 @
4b641a33
import
*
as
http
from
"http"
;
import
*
as
fs
from
"fs"
;
import
*
as
path
from
"path"
;
import
*
as
net
from
"net"
;
import
{
promisify
}
from
"util"
;
import
{
parentPort
,
isMainThread
,
workerData
}
from
"worker_threads"
;
// Promisify file system operations
const
statAsync
=
promisify
(
fs
.
stat
);
const
readFileAsync
=
promisify
(
fs
.
readFile
);
// Configuration interface with types
// export interface ServerConfig {
// port: number;
// rootDir: string;
// cacheMaxAge: number;
// maxPortRetries?: number;
// }
// Default configuration
const
DEFAULT_CONFIG
=
{
port
:
31
_111
,
rootDir
:
"."
,
cacheMaxAge
:
86400
,
// 1 day in seconds
maxPortRetries
:
5
,
};
// MIME types mapping
const
MIME_TYPES
=
{
".html"
:
"text/html"
,
".css"
:
"text/css"
,
".js"
:
"application/javascript"
,
".ts"
:
"application/typescript"
,
".json"
:
"application/json"
,
".png"
:
"image/png"
,
".jpg"
:
"image/jpeg"
,
".jpeg"
:
"image/jpeg"
,
".gif"
:
"image/gif"
,
".svg"
:
"image/svg+xml"
,
".ico"
:
"image/x-icon"
,
".txt"
:
"text/plain"
,
".pdf"
:
"application/pdf"
,
".woff"
:
"font/woff"
,
".woff2"
:
"font/woff2"
,
".ttf"
:
"font/ttf"
,
".eot"
:
"application/vnd.ms-fontobject"
,
".otf"
:
"font/otf"
,
".mp4"
:
"video/mp4"
,
".webm"
:
"video/webm"
,
".mp3"
:
"audio/mpeg"
,
".wav"
:
"audio/wav"
,
".webp"
:
"image/webp"
,
};
/**
* Checks if a port is available
* @param port Port number to check
* @returns Promise that resolves to true if port is available, false otherwise
*/
const
isPortAvailable
=
(
port
)
=>
{
return
new
Promise
((
resolve
)
=>
{
const
tester
=
net
.
createServer
()
.
once
(
"error"
,
()
=>
{
// Port is in use
resolve
(
false
);
})
.
once
(
"listening"
,
()
=>
{
// Port is available
tester
.
close
(()
=>
resolve
(
true
));
})
.
listen
(
port
);
});
};
/**
* Finds the first available port starting from the specified port
* @param startPort Starting port number
* @param maxRetries Maximum number of ports to try
* @returns Promise resolving to the first available port, or undefined if none found
*/
const
findAvailablePort
=
async
(
startPort
,
maxRetries
)
=>
{
for
(
let
i
=
0
;
i
<
maxRetries
;
i
++
)
{
const
port
=
startPort
+
i
;
// eslint-disable-next-line no-await-in-loop
if
(
await
isPortAvailable
(
port
))
{
return
port
;
}
}
return
undefined
;
};
/**
* Handles HTTP requests, serving static files with caching
*/
const
handleRequest
=
(
config
)
=>
async
(
req
,
res
)
=>
{
try
{
if
(
!
req
.
url
)
{
res
.
statusCode
=
400
;
res
.
end
(
"Bad Request"
);
return
;
}
// Only allow GET requests
if
(
req
.
method
!==
"GET"
)
{
res
.
statusCode
=
405
;
res
.
setHeader
(
"Allow"
,
"GET"
);
res
.
end
(
"Method Not Allowed"
);
return
;
}
// Parse URL and sanitize path
const
parsedUrl
=
new
URL
(
req
.
url
,
`http://
${
req
.
headers
.
host
}
`
);
let
filePath
=
path
.
normalize
(
path
.
join
(
config
.
rootDir
,
parsedUrl
.
pathname
)
);
console
.
log
(
`filePath:
${
filePath
}
`
,
"request"
,
req
.
url
);
// Handle root path or directory paths, serve index.html
if
(
filePath
===
path
.
normalize
(
config
.
rootDir
)
||
filePath
.
endsWith
(
"/"
))
{
filePath
=
path
.
join
(
filePath
,
"index.html"
);
}
// Check if file exists and get its stats
let
stats
;
try
{
console
.
log
(
`filePath:
${
filePath
}
`
,
"stats"
);
stats
=
await
statAsync
(
filePath
);
}
catch
(
error
)
{
// File not found
res
.
statusCode
=
404
;
res
.
end
(
"Not Found"
);
return
;
}
// Handle directory requests
if
(
stats
.
isDirectory
())
{
try
{
// Redirect to directory with trailing slash if needed
if
(
!
req
.
url
.
endsWith
(
"/"
))
{
res
.
statusCode
=
301
;
res
.
setHeader
(
"Location"
,
`
${
req
.
url
}
/`
);
res
.
end
();
return
;
}
// Try to serve index.html from directory
filePath
=
path
.
join
(
filePath
,
"index.html"
);
stats
=
await
statAsync
(
filePath
);
}
catch
(
error
)
{
res
.
statusCode
=
404
;
res
.
end
(
"Not Found"
);
return
;
}
}
// Get file extension and MIME type
const
ext
=
path
.
extname
(
filePath
).
toLowerCase
();
const
contentType
=
MIME_TYPES
[
ext
]
||
"application/octet-stream"
;
// Handle caching - check if file has been modified
const
ifModifiedSince
=
req
.
headers
[
"if-modified-since"
];
if
(
ifModifiedSince
)
{
const
modifiedSinceDate
=
new
Date
(
ifModifiedSince
);
// Check if the file hasn't been modified since the client's last request
if
(
modifiedSinceDate
&&
stats
.
mtime
<=
modifiedSinceDate
)
{
res
.
statusCode
=
304
;
// Not Modified
res
.
end
();
return
;
}
}
// Set cache headers
const
lastModified
=
stats
.
mtime
.
toUTCString
();
res
.
setHeader
(
"Last-Modified"
,
lastModified
);
res
.
setHeader
(
"Cache-Control"
,
`public, max-age=
${
config
.
cacheMaxAge
}
`
);
res
.
setHeader
(
"Content-Type"
,
contentType
);
res
.
setHeader
(
"Content-Length"
,
stats
.
size
);
// Read and send file
const
fileContent
=
await
readFileAsync
(
filePath
);
res
.
end
(
fileContent
);
}
catch
(
error
)
{
console
.
error
(
`[Worker
${
process
.
pid
}
] Server error:`
,
error
);
// Only attempt to send error response if headers haven't been sent
if
(
!
res
.
headersSent
)
{
res
.
statusCode
=
500
;
res
.
end
(
"Internal Server Error"
);
}
}
};
// Create and start the server
// Modified to return the port and not handle process exit
export
const
startServer
=
async
(
userConfig
=
{})
=>
{
// Merge default config with user provided config
const
config
=
{
...
DEFAULT_CONFIG
,
...
userConfig
};
// Try to find an available port
const
maxRetries
=
config
.
maxPortRetries
||
5
;
const
availablePort
=
await
findAvailablePort
(
config
.
port
,
maxRetries
);
if
(
!
availablePort
)
{
throw
new
Error
(
`Could not find an available port after trying
${
maxRetries
}
ports starting from
${
config
.
port
}
`
);
}
config
.
port
=
availablePort
;
// Update config with the actual port
// Create server with the handler
const
server
=
http
.
createServer
(
handleRequest
(
config
));
// Start the server
await
new
Promise
((
resolve
,
reject
)
=>
{
server
.
on
(
"error"
,
(
err
)
=>
{
console
.
error
(
`[Worker
${
process
.
pid
}
] Server error:`
,
err
);
reject
(
err
);
// Reject promise on server error during startup
});
server
.
listen
(
config
.
port
,
()
=>
{
console
.
log
(
`[Worker
${
process
.
pid
}
] 🚀 Static file server running at http://localhost:
${
config
.
port
}
/`
);
console
.
log
(
`[Worker
${
process
.
pid
}
] 📁 Serving files from:
${
path
.
resolve
(
config
.
rootDir
)}
`
);
console
.
log
(
`[Worker
${
process
.
pid
}
] 🔄 Cache max age:
${
config
.
cacheMaxAge
}
seconds`
);
resolve
();
});
});
// Don't handle SIGINT here, let the main thread manage the worker lifecycle
return
{
server
,
port
:
config
.
port
};
};
// --- Worker Logic ---
if
(
!
isMainThread
&&
parentPort
)
{
const
run
=
async
()
=>
{
try
{
if
(
!
workerData
||
!
workerData
.
rootDir
)
{
throw
new
Error
(
"rootDir must be provided in workerData"
);
}
const
config
=
{
...
DEFAULT_CONFIG
,
...
workerData
};
const
{
port
}
=
await
startServer
(
config
);
parentPort
?.
postMessage
({
status
:
"ready"
,
port
});
}
catch
(
error
)
{
console
.
error
(
`[Worker
${
process
.
pid
}
] Failed to start server:`
,
error
);
parentPort
?.
postMessage
({
status
:
"error"
,
message
:
error
.
message
});
}
};
run
().
catch
((
err
)
=>
{
// Catch unhandled promise rejections during startup
console
.
error
(
`[Worker
${
process
.
pid
}
] Unhandled error during startup:`
,
err
);
parentPort
?.
postMessage
({
status
:
"error"
,
message
:
err
.
message
||
"Unknown startup error"
,
});
});
// Keep the worker alive
// The server itself will keep the event loop running
}
else
if
(
!
isMainThread
)
{
// Should not happen if used correctly, but good to handle
console
.
error
(
"Running as worker but parentPort is not available."
);
process
.
exit
(
1
);
}
// If it IS the main thread, exporting startServer allows for potential direct use or testing
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论