add: 新增虚拟环境支持和调试模式功能,优化日志系统
This commit is contained in:
parent
2d4f03b7cf
commit
284f4e39d7
|
|
@ -0,0 +1,44 @@
|
|||
anyio==4.11.0
|
||||
babel==2.17.0
|
||||
beancount==3.2.0
|
||||
beangulp==0.2.0
|
||||
beanquery==0.2.0
|
||||
beautifulsoup4==4.14.0
|
||||
blinker==1.9.0
|
||||
chardet==5.2.0
|
||||
cheroot==10.0.1
|
||||
click==8.3.0
|
||||
dateparser==1.2.2
|
||||
debugpy==1.8.16
|
||||
fava==1.30.6
|
||||
Flask==3.1.2
|
||||
flask-babel==4.0.0
|
||||
idna==3.10
|
||||
iniconfig==2.1.0
|
||||
itsdangerous==2.2.0
|
||||
jaraco.functools==4.3.0
|
||||
Jinja2==3.1.6
|
||||
lxml==6.0.2
|
||||
markdown2==2.5.4
|
||||
MarkupSafe==3.0.3
|
||||
more-itertools==10.8.0
|
||||
packaging==25.0
|
||||
pluggy==1.6.0
|
||||
ply==3.11
|
||||
pycryptodomex==3.23.0
|
||||
Pygments==2.19.2
|
||||
pytest==8.4.2
|
||||
python-dateutil==2.9.0.post0
|
||||
python-magic==0.4.27
|
||||
pytz==2025.2
|
||||
pyzipper==0.3.6
|
||||
regex==2025.9.18
|
||||
simplejson==3.20.2
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
soupsieve==2.8
|
||||
TatSu-LTS==5.13.2
|
||||
typing_extensions==4.15.0
|
||||
tzlocal==5.3.1
|
||||
watchfiles==1.1.0
|
||||
Werkzeug==3.1.3
|
||||
|
|
@ -6,8 +6,10 @@ import (
|
|||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beancount-gs/utils/venv" // 添加这个导入
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
|
@ -20,6 +22,14 @@ var ledgerAccountTypesMap map[string]map[string]string
|
|||
var ledgerCurrencyMap map[string][]LedgerCurrency
|
||||
var whiteList []string
|
||||
|
||||
var (
|
||||
// 添加虚拟环境相关的变量
|
||||
venvPath string
|
||||
venvExecutor *venv.VenvExecutor
|
||||
venvPathLock sync.RWMutex
|
||||
venvExecLock sync.RWMutex
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Mail string `json:"mail,omitempty"`
|
||||
|
|
@ -30,6 +40,7 @@ type Config struct {
|
|||
IsBak bool `json:"isBak"`
|
||||
OpeningBalances string `json:"openingBalances"`
|
||||
CreateDate string `json:"createDate,omitempty"`
|
||||
DebugMode bool `json:"debugMode"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
|
|
@ -85,12 +96,15 @@ func GetServerConfig() Config {
|
|||
|
||||
func LoadServerConfig() error {
|
||||
filePath := GetServerConfigFilePath()
|
||||
LogSystemInfo("Load config file (" + filePath + ")")
|
||||
if !FileIfExist(filePath) {
|
||||
serverConfig = Config{
|
||||
OpeningBalances: "Equity:OpeningBalances",
|
||||
OpeningBalances: "Equity:Opening-Balances",
|
||||
OperatingCurrency: "CNY",
|
||||
StartDate: "1970-01-01",
|
||||
IsBak: true,
|
||||
DebugMode: false, // 添加默认值
|
||||
DataPath: GetDataPath(), // 添加默认值
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -131,15 +145,52 @@ func LoadServerConfig() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// 获取当前调试模式状态
|
||||
func IsDebugMode() bool {
|
||||
return serverConfig.DebugMode
|
||||
}
|
||||
|
||||
// 设置调试模式并保存到配置文件
|
||||
func SetDebugMode(debug bool) error {
|
||||
serverConfig.DebugMode = debug
|
||||
return UpdateServerConfig(serverConfig)
|
||||
}
|
||||
|
||||
// 为方便使用,添加调试日志函数
|
||||
func DebugLog(format string, args ...interface{}) {
|
||||
if IsDebugMode() {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
LogSystemInfo("[DEBUG] " + message)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加带上下文信息的调试日志
|
||||
func DebugLogWithContext(context string, format string, args ...interface{}) {
|
||||
if IsDebugMode() {
|
||||
message := fmt.Sprintf("[%s] "+format, append([]interface{}{context}, args...)...)
|
||||
LogSystemInfo("[DEBUG] " + message)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加带上下文信息的警告日志
|
||||
func WarnLogWithContext(context string, format string, args ...interface{}) {
|
||||
message := fmt.Sprintf("[%s] "+format, append([]interface{}{context}, args...)...)
|
||||
LogSystemInfo("[WARN] " + message)
|
||||
}
|
||||
|
||||
func UpdateServerConfig(config Config) error {
|
||||
bytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = WriteFile(GetServerConfigFilePath(), string(bytes))
|
||||
|
||||
// 使用新的带目录创建功能的写入函数
|
||||
configPath := GetServerConfigFilePath()
|
||||
err = WriteFileWithDir(configPath, string(bytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverConfig = config
|
||||
return nil
|
||||
}
|
||||
|
|
@ -335,14 +386,15 @@ func LoadLedgerAccounts(ledgerId string) error {
|
|||
key := words[2]
|
||||
temp = accountMap[key]
|
||||
account := Account{Acc: key, Type: nil, StartDate: "", EndDate: ""}
|
||||
if words[1] == "open" {
|
||||
switch words[1] {
|
||||
case "open":
|
||||
// 最晚的开户日期设置为账户开户日期
|
||||
account.StartDate = getMaxDate(words[0], temp.StartDate)
|
||||
// 货币单位
|
||||
if len(words) >= 4 {
|
||||
account.Currency = words[3]
|
||||
}
|
||||
} else if words[1] == "close" {
|
||||
case "close":
|
||||
// 账户最晚的关闭日期设置为账户关闭日期
|
||||
account.EndDate = getMaxDate(words[0], temp.EndDate)
|
||||
}
|
||||
|
|
@ -588,3 +640,31 @@ func GetAccountIconName(account string) string {
|
|||
nodes := strings.Split(account, ":")
|
||||
return strings.Join(nodes, "_")
|
||||
}
|
||||
|
||||
// 新增函数:设置虚拟环境路径
|
||||
func SetVenvPath(path string) {
|
||||
venvPathLock.Lock()
|
||||
defer venvPathLock.Unlock()
|
||||
venvPath = path
|
||||
}
|
||||
|
||||
// 新增函数:获取虚拟环境路径
|
||||
func GetVenvPath() string {
|
||||
venvPathLock.RLock()
|
||||
defer venvPathLock.RUnlock()
|
||||
return venvPath
|
||||
}
|
||||
|
||||
// 新增函数:设置虚拟环境执行器
|
||||
func SetVenvExecutor(executor *venv.VenvExecutor) {
|
||||
venvExecLock.Lock()
|
||||
defer venvExecLock.Unlock()
|
||||
venvExecutor = executor
|
||||
}
|
||||
|
||||
// 新增函数:获取虚拟环境执行器
|
||||
func GetVenvExecutor() *venv.VenvExecutor {
|
||||
venvExecLock.RLock()
|
||||
defer venvExecLock.RUnlock()
|
||||
return venvExecutor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,10 +295,7 @@ func getAccountWithNumber(str string) string {
|
|||
|
||||
func IsComment(line string) bool {
|
||||
trimmed := strings.TrimLeft(line, " ")
|
||||
if strings.HasPrefix(trimmed, ";") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return strings.HasPrefix(trimmed, ";")
|
||||
}
|
||||
|
||||
// 删除指定行范围的内容
|
||||
|
|
@ -360,3 +357,20 @@ func WriteToFile(filePath string, lines []string) error {
|
|||
LogSystemInfo("Success write content in file " + filePath)
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
// EnsureDirExists 确保目录存在,如果不存在则创建
|
||||
func EnsureDirExists(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
// WriteFileWithDir 写入文件,确保目录存在
|
||||
func WriteFileWithDir(filename string, data string) error {
|
||||
// 确保目录存在
|
||||
if err := EnsureDirExists(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
return WriteFile(filename, data)
|
||||
}
|
||||
|
|
|
|||
118
script/log.go
118
script/log.go
|
|
@ -5,18 +5,136 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// Info级别日志函数组
|
||||
|
||||
// LogInfo 记录信息级别的日志
|
||||
// ledgerName: 账本名称,用于标识日志来源
|
||||
// message: 需要记录的日志信息
|
||||
func LogInfo(ledgerName string, message string) {
|
||||
fmt.Printf("[Info] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message)
|
||||
}
|
||||
|
||||
// LogSystemInfo 记录系统信息日志
|
||||
// message: 要记录的系统信息消息
|
||||
func LogSystemInfo(message string) {
|
||||
LogInfo("System", message)
|
||||
}
|
||||
|
||||
// Warn级别日志函数组
|
||||
|
||||
// LogWarn 记录警告级别的日志
|
||||
// ledgerName: 账本名称,用于标识日志来源
|
||||
// context: 日志上下文,提供更多分类信息
|
||||
// format: 格式化字符串,定义日志消息格式
|
||||
// args: 可变参数,用于填充格式化字符串
|
||||
func LogWarn(ledgerName string, context string, format string, args ...interface{}) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("[Warn] [%s] [%s][%s]: %s\n",
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
ledgerName,
|
||||
context,
|
||||
message)
|
||||
}
|
||||
|
||||
// LogSystemWarn 记录系统级警告日志
|
||||
// context: 日志上下文标识
|
||||
// format: 格式化字符串
|
||||
// args: 格式化参数
|
||||
func LogSystemWarn(context string, format string, args ...interface{}) {
|
||||
LogWarn("System", context, format, args...)
|
||||
}
|
||||
|
||||
// Error级别日志函数组
|
||||
|
||||
// LogError 记录错误日志,格式为:[Error] [时间] [账本名称]: 错误信息
|
||||
// ledgerName: 账本名称
|
||||
// message: 需要记录的错误信息
|
||||
func LogError(ledgerName string, message string) {
|
||||
fmt.Printf("[Error] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message)
|
||||
}
|
||||
|
||||
// LogErrorDetailed 记录带有详细信息的错误日志
|
||||
// ledgerName: 账本名称,用于标识日志来源
|
||||
// context: 上下文信息,帮助定位错误发生的场景
|
||||
// format: 格式化字符串,用于构建错误信息
|
||||
// args: 格式化字符串的参数
|
||||
// 日志格式为:[Error] [时间] [账本名称][上下文]: 错误信息
|
||||
func LogErrorDetailed(ledgerName string, context string, format string, args ...interface{}) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("[Error] [%s] [%s][%s]: %s\n",
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
ledgerName,
|
||||
context,
|
||||
message)
|
||||
}
|
||||
|
||||
// LogSystemError 记录系统级错误信息
|
||||
// message: 错误描述信息
|
||||
// 该函数是对LogError的封装,专门用于记录系统模块的错误日志
|
||||
func LogSystemError(message string) {
|
||||
LogError("System", message)
|
||||
}
|
||||
|
||||
// LogSystemErrorDetailed 记录系统级错误日志,包含详细上下文信息
|
||||
// context: 错误发生的上下文环境
|
||||
// format: 格式化字符串,用于描述错误信息
|
||||
// args: 格式化字符串的参数
|
||||
func LogSystemErrorDetailed(context string, format string, args ...interface{}) {
|
||||
LogErrorDetailed("System", context, format, args...)
|
||||
}
|
||||
|
||||
// Debug级别日志函数组
|
||||
|
||||
// LogDebug 在调试模式下记录调试日志,格式为:[Debug] [时间] [账本名称]: 消息
|
||||
// 仅当 IsDebugMode() 返回 true 时才会输出日志
|
||||
func LogDebug(ledgerName string, message string) {
|
||||
if IsDebugMode() {
|
||||
fmt.Printf("[Debug] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message)
|
||||
}
|
||||
}
|
||||
|
||||
// LogDebugDetailed 在调试模式下记录详细的调试日志
|
||||
// ledgerName: 账本名称,用于标识日志来源
|
||||
// context: 上下文信息,提供额外的日志分类
|
||||
// format: 格式化字符串,用于构建日志消息
|
||||
// args: 格式化字符串的参数
|
||||
// 日志格式: [Debug] [时间] [账本名称][上下文]: 消息内容
|
||||
// 注意: 仅在调试模式(IsDebugMode返回true)下输出日志
|
||||
func LogDebugDetailed(ledgerName string, context string, format string, args ...interface{}) {
|
||||
if IsDebugMode() {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("[Debug] [%s] [%s][%s]: %s\n",
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
ledgerName,
|
||||
context,
|
||||
message)
|
||||
}
|
||||
}
|
||||
|
||||
// LogSystemDebug 记录系统级别的调试日志
|
||||
// message: 需要记录的调试信息
|
||||
func LogSystemDebug(message string) {
|
||||
LogDebug("System", message)
|
||||
}
|
||||
|
||||
// LogSystemDebugDetailed 记录系统级调试日志,包含详细上下文信息
|
||||
// context: 日志上下文标识
|
||||
// format: 日志格式字符串
|
||||
// args: 格式化参数
|
||||
func LogSystemDebugDetailed(context string, format string, args ...interface{}) {
|
||||
LogDebugDetailed("System", context, format, args...)
|
||||
}
|
||||
|
||||
// 特殊功能日志函数
|
||||
|
||||
// LogBQLQueryDebug 在调试模式下记录BQL查询日志
|
||||
// ledgerName: 账本名称
|
||||
// context: 查询上下文信息
|
||||
// format: 日志格式字符串
|
||||
// args: 格式化参数
|
||||
// 注意: 仅在调试模式开启时才会记录日志
|
||||
func LogBQLQueryDebug(ledgerName string, context string, format string, args ...interface{}) {
|
||||
if IsDebugMode() {
|
||||
LogDebugDetailed(ledgerName, "BQL:"+context, format, args...)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
90
server.go
90
server.go
|
|
@ -3,14 +3,25 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/beancount-gs/script"
|
||||
"github.com/beancount-gs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/beancount-gs/script"
|
||||
"github.com/beancount-gs/service"
|
||||
"github.com/beancount-gs/utils/venv"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 全局变量,方便其他模块使用
|
||||
var venvExecutor *venv.VenvExecutor
|
||||
var venvPath string // 新增:虚拟环境路径变量
|
||||
|
||||
/*
|
||||
* 初始化服务器文件
|
||||
* 检查账本目录是否存在,如果不存在则创建
|
||||
* 返回error,表示操作是否成功
|
||||
*/
|
||||
func InitServerFiles() error {
|
||||
dataPath := script.GetServerConfig().DataPath
|
||||
// 账本目录不存在,则创建
|
||||
|
|
@ -20,6 +31,11 @@ func InitServerFiles() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
* 加载服务器缓存
|
||||
* 加载账本配置映射和账户映射
|
||||
* 返回error,表示加载过程中是否出错
|
||||
*/
|
||||
func LoadServerCache() error {
|
||||
err := script.LoadLedgerConfigMap()
|
||||
if err != nil {
|
||||
|
|
@ -28,6 +44,11 @@ func LoadServerCache() error {
|
|||
return script.LoadLedgerAccountsMap()
|
||||
}
|
||||
|
||||
/*
|
||||
* 授权中间件
|
||||
* 检查请求头中的ledgerId是否有效
|
||||
* 如果有效则继续处理请求,否则返回未授权错误
|
||||
*/
|
||||
func AuthorizedHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ledgerId := c.GetHeader("ledgerId")
|
||||
|
|
@ -42,18 +63,24 @@ func AuthorizedHandler() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 注册路由
|
||||
* 配置静态文件服务、API路由和需要授权的路由组
|
||||
*/
|
||||
func RegisterRouter(router *gin.Engine) {
|
||||
// fix wildcard and static file router conflict, https://github.com/gin-gonic/gin/issues/360
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusMovedPermanently, "/web")
|
||||
})
|
||||
router.StaticFS("/web", http.Dir("./public"))
|
||||
// 公开API路由,无需授权
|
||||
router.GET("/api/version", service.QueryVersion)
|
||||
router.POST("/api/check", service.CheckBeancount)
|
||||
router.GET("/api/config", service.QueryServerConfig)
|
||||
router.POST("/api/config", service.UpdateServerConfig)
|
||||
router.GET("/api/ledger", service.QueryLedgerList)
|
||||
router.POST("/api/ledger", service.OpenOrCreateLedger)
|
||||
// 需要授权的API路由组
|
||||
authorized := router.Group("/api/auth/")
|
||||
authorized.Use(AuthorizedHandler())
|
||||
{
|
||||
|
|
@ -106,19 +133,71 @@ func RegisterRouter(router *gin.Engine) {
|
|||
}
|
||||
}
|
||||
|
||||
// initVenvExecutor 初始化虚拟环境执行器
|
||||
func initVenvExecutor(venvDir string) {
|
||||
venvPath = venvDir
|
||||
script.SetVenvPath(venvDir) // 设置路径到 script 包
|
||||
|
||||
// 检查虚拟环境是否存在
|
||||
if !venv.CheckVenvExists(venvPath) {
|
||||
script.LogSystemError("虚拟环境不存在,请先运行 setup script: " + venvPath)
|
||||
fmt.Println("警告: 虚拟环境不存在,某些功能可能无法正常工作")
|
||||
fmt.Println("请运行: ./start_dev.sh 或手动创建虚拟环境")
|
||||
return
|
||||
}
|
||||
|
||||
venvExecutor = venv.NewVenvExecutor(venvPath)
|
||||
script.SetVenvExecutor(venvExecutor) // 设置执行器到 script 包
|
||||
|
||||
// 测试 bean-query 是否可用
|
||||
_, err := venvExecutor.GetCommandPath("bean-query")
|
||||
if err != nil {
|
||||
script.LogSystemError("bean-query 不可用: " + err.Error())
|
||||
fmt.Println("警告: bean-query 命令不可用,价格查询功能将受限")
|
||||
} else {
|
||||
script.LogSystemInfo("虚拟环境初始化成功: bean-query 可用, 路径: " + venvPath)
|
||||
fmt.Println("虚拟环境初始化成功: " + venvPath)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var secret string
|
||||
var port int
|
||||
var debugFlag bool
|
||||
var venvDir string // 新增:虚拟环境目录参数
|
||||
|
||||
flag.StringVar(&secret, "secret", "", "服务器密钥")
|
||||
flag.IntVar(&port, "p", 10000, "端口号")
|
||||
flag.BoolVar(&debugFlag, "debug", false, "调试模式")
|
||||
flag.StringVar(&venvDir, "venv", ".env_beancount-v3", "虚拟环境目录名称,默认值为 .env_beancount-v3") // 新增参数
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// 初始化虚拟环境执行器
|
||||
initVenvExecutor(venvDir)
|
||||
|
||||
// 读取配置文件
|
||||
err := script.LoadServerConfig()
|
||||
if err != nil {
|
||||
script.LogSystemError("Failed to load server config, " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 如果命令行指定了debug参数,覆盖配置文件中的设置
|
||||
if debugFlag {
|
||||
err = script.SetDebugMode(true)
|
||||
if err != nil {
|
||||
fmt.Println("Warning: Failed to set debug mode:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 现在可以在任何地方使用 script.IsDebugMode() 来检查调试模式
|
||||
if script.IsDebugMode() {
|
||||
fmt.Println("调试模式已启用")
|
||||
} else {
|
||||
fmt.Println("调试模式未启用")
|
||||
}
|
||||
|
||||
serverConfig := script.GetServerConfig()
|
||||
// 若 DataPath == "" 则配置未初始化
|
||||
if serverConfig.DataPath != "" {
|
||||
|
|
@ -135,11 +214,13 @@ func main() {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
// gin 日志设置
|
||||
gin.DisableConsoleColor()
|
||||
fs, _ := os.Create("logs/gin.log")
|
||||
gin.DefaultWriter = io.MultiWriter(fs, os.Stdout)
|
||||
router := gin.Default()
|
||||
|
||||
// 注册路由
|
||||
RegisterRouter(router)
|
||||
|
||||
|
|
@ -151,10 +232,13 @@ func main() {
|
|||
startLog += " or http://" + ip + portStr
|
||||
}
|
||||
script.LogSystemInfo(startLog)
|
||||
|
||||
// 打开浏览器
|
||||
script.OpenBrowser(url)
|
||||
|
||||
// 打印密钥
|
||||
script.LogSystemInfo("Secret token is " + script.GenerateServerSecret(secret))
|
||||
|
||||
// 启动服务
|
||||
err = router.Run(portStr)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* @Author: liangzai450
|
||||
* @Date: 2025-09-17 20:50:11
|
||||
* @LastEditors: liangzai liangzai450@qq.com
|
||||
* @LastEditTime: 2025-09-18 08:15:42
|
||||
* @FilePath: \\cnb-beancount\\utils\\venv\\venv.go
|
||||
* @Description:
|
||||
* Copyright (c) 2025 by ${git_name_email}, All Rights Reserved.
|
||||
* ==============================================
|
||||
*/
|
||||
|
||||
package venv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// VenvExecutor 虚拟环境执行器
|
||||
type VenvExecutor struct {
|
||||
venvPath string
|
||||
}
|
||||
|
||||
// NewVenvExecutor 创建新的虚拟环境执行器
|
||||
func NewVenvExecutor(venvPath string) *VenvExecutor {
|
||||
return &VenvExecutor{venvPath: venvPath}
|
||||
}
|
||||
|
||||
// GetCommandPath 获取虚拟环境中命令的完整路径
|
||||
func (v *VenvExecutor) GetCommandPath(command string) (string, error) {
|
||||
var cmdPath string
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
cmdPath = filepath.Join(v.venvPath, "Scripts", command+".exe")
|
||||
} else {
|
||||
cmdPath = filepath.Join(v.venvPath, "bin", command)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(cmdPath); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("command %s not found at: %s", command, cmdPath)
|
||||
}
|
||||
|
||||
return cmdPath, nil
|
||||
}
|
||||
|
||||
// Execute 执行虚拟环境中的命令
|
||||
func (v *VenvExecutor) Execute(command string, args ...string) error {
|
||||
cmdPath, err := v.GetCommandPath(command)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command(cmdPath, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ExecuteWithOutput 执行命令并返回输出
|
||||
func (v *VenvExecutor) ExecuteWithOutput(command string, args ...string) ([]byte, []byte, error) {
|
||||
cmdPath, err := v.GetCommandPath(command)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cmd := exec.Command(cmdPath, args...)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err = cmd.Run()
|
||||
return stdout.Bytes(), stderr.Bytes(), err
|
||||
}
|
||||
|
||||
// BeanQueryStdout 执行 bean-query 命令,只返回纯净的标准输出
|
||||
func (v *VenvExecutor) BeanQueryStdout(beancountFile, query string) ([]byte, error) {
|
||||
stdout, stderr, err := v.ExecuteWithOutput("bean-query", beancountFile, query)
|
||||
if err != nil {
|
||||
// 你可以选择记录 stderr 或根据错误类型处理
|
||||
fmt.Printf("Bean-query stderr: %s", string(stderr))
|
||||
return nil, err
|
||||
}
|
||||
return stdout, nil
|
||||
}
|
||||
|
||||
// BeanQuery 执行 bean-query 命令
|
||||
// 保持原有的 BeanQuery 函数(如果需要兼容旧代码)
|
||||
func (v *VenvExecutor) BeanQuery(beancountFile, query string) ([]byte, error) {
|
||||
return v.BeanQueryStdout(beancountFile, query)
|
||||
}
|
||||
|
||||
// BeanCheck 执行 bean-check 语法检查
|
||||
func (v *VenvExecutor) BeanCheck(beancountFile string) error {
|
||||
_, err := v.BeanQueryStdout("bean-check", beancountFile)
|
||||
return err
|
||||
}
|
||||
|
||||
// Fava 启动 fava 服务器
|
||||
func (v *VenvExecutor) Fava(beancountFile string, port int) error {
|
||||
return v.Execute("fava", beancountFile, "--port", fmt.Sprintf("%d", port))
|
||||
}
|
||||
|
||||
// CheckVenvExists 检查虚拟环境是否存在
|
||||
func CheckVenvExists(venvPath string) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
return dirExists(filepath.Join(venvPath, "Scripts"))
|
||||
}
|
||||
return dirExists(filepath.Join(venvPath, "bin"))
|
||||
}
|
||||
|
||||
func dirExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
Loading…
Reference in New Issue