123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122 |
- // SiYuan - Refactor your thinking
- // Copyright (c) 2020-present, b3log.org
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- package model
- import (
- "bytes"
- "fmt"
- "os"
- "path/filepath"
- "runtime"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/88250/go-humanize"
- "github.com/88250/gulu"
- "github.com/88250/lute"
- "github.com/88250/lute/ast"
- "github.com/Xuanwo/go-locale"
- "github.com/getsentry/sentry-go"
- "github.com/sashabaranov/go-openai"
- "github.com/siyuan-note/eventbus"
- "github.com/siyuan-note/filelock"
- "github.com/siyuan-note/logging"
- "github.com/siyuan-note/siyuan/kernel/conf"
- "github.com/siyuan-note/siyuan/kernel/sql"
- "github.com/siyuan-note/siyuan/kernel/treenode"
- "github.com/siyuan-note/siyuan/kernel/util"
- "golang.org/x/mod/semver"
- "golang.org/x/text/language"
- )
- var Conf *AppConf
- // AppConf 维护应用元数据,保存在 ~/.siyuan/conf.json。
- type AppConf struct {
- LogLevel string `json:"logLevel"` // 日志级别:Off, Trace, Debug, Info, Warn, Error, Fatal
- Appearance *conf.Appearance `json:"appearance"` // 外观
- Langs []*conf.Lang `json:"langs"` // 界面语言列表
- Lang string `json:"lang"` // 选择的界面语言,同 Appearance.Lang
- FileTree *conf.FileTree `json:"fileTree"` // 文档面板
- Tag *conf.Tag `json:"tag"` // 标签面板
- Editor *conf.Editor `json:"editor"` // 编辑器配置
- Export *conf.Export `json:"export"` // 导出配置
- Graph *conf.Graph `json:"graph"` // 关系图配置
- UILayout *conf.UILayout `json:"uiLayout"` // 界面布局。不要直接使用,使用 GetUILayout() 和 SetUILayout() 方法
- UserData string `json:"userData"` // 社区用户信息,对 User 加密存储
- User *conf.User `json:"-"` // 社区用户内存结构,不持久化。不要直接使用,使用 GetUser() 和 SetUser() 方法
- Account *conf.Account `json:"account"` // 帐号配置
- ReadOnly bool `json:"readonly"` // 是否是以只读模式运行
- LocalIPs []string `json:"localIPs"` // 本地 IP 列表
- AccessAuthCode string `json:"accessAuthCode"` // 访问授权码
- System *conf.System `json:"system"` // 系统配置
- Keymap *conf.Keymap `json:"keymap"` // 快捷键配置
- Sync *conf.Sync `json:"sync"` // 同步配置
- Search *conf.Search `json:"search"` // 搜索配置
- Flashcard *conf.Flashcard `json:"flashcard"` // 闪卡配置
- AI *conf.AI `json:"ai"` // 人工智能配置
- Bazaar *conf.Bazaar `json:"bazaar"` // 集市配置
- Stat *conf.Stat `json:"stat"` // 统计
- Api *conf.API `json:"api"` // API
- Repo *conf.Repo `json:"repo"` // 数据仓库
- Publish *conf.Publish `json:"publish"` // 发布服务
- OpenHelp bool `json:"openHelp"` // 启动后是否需要打开用户指南
- ShowChangelog bool `json:"showChangelog"` // 是否显示版本更新日志
- CloudRegion int `json:"cloudRegion"` // 云端区域,0:中国大陆,1:北美
- Snippet *conf.Snpt `json:"snippet"` // 代码片段
- State int `json:"state"` // 运行状态,0:已经正常退出,1:运行中
- m *sync.Mutex
- }
- func (conf *AppConf) GetUILayout() *conf.UILayout {
- conf.m.Lock()
- defer conf.m.Unlock()
- return conf.UILayout
- }
- func (conf *AppConf) SetUILayout(uiLayout *conf.UILayout) {
- conf.m.Lock()
- defer conf.m.Unlock()
- conf.UILayout = uiLayout
- }
- func (conf *AppConf) GetUser() *conf.User {
- conf.m.Lock()
- defer conf.m.Unlock()
- return conf.User
- }
- func (conf *AppConf) SetUser(user *conf.User) {
- conf.m.Lock()
- defer conf.m.Unlock()
- conf.User = user
- }
- func InitConf() {
- initLang()
- Conf = &AppConf{LogLevel: "debug", m: &sync.Mutex{}}
- confPath := filepath.Join(util.ConfDir, "conf.json")
- if gulu.File.IsExist(confPath) {
- if data, err := os.ReadFile(confPath); nil != err {
- logging.LogErrorf("load conf [%s] failed: %s", confPath, err)
- } else {
- if err = gulu.JSON.UnmarshalJSON(data, Conf); err != nil {
- logging.LogErrorf("parse conf [%s] failed: %s", confPath, err)
- } else {
- logging.LogInfof("loaded conf [%s]", confPath)
- }
- }
- }
- if "" != util.Lang {
- initialized := false
- if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container {
- // 移动端以上次设置的外观语言为准
- if "" != Conf.Lang && util.Lang != Conf.Lang {
- util.Lang = Conf.Lang
- logging.LogInfof("use the last specified language [%s]", util.Lang)
- initialized = true
- }
- }
- if !initialized {
- Conf.Lang = util.Lang
- logging.LogInfof("initialized the specified language [%s]", util.Lang)
- }
- } else {
- if "" == Conf.Lang {
- // 未指定外观语言时使用系统语言
- if userLang, err := locale.Detect(); nil == err {
- var supportLangs []language.Tag
- for lang := range util.Langs {
- if tag, err := language.Parse(lang); nil == err {
- supportLangs = append(supportLangs, tag)
- } else {
- logging.LogErrorf("load language [%s] failed: %s", lang, err)
- }
- }
- matcher := language.NewMatcher(supportLangs)
- lang, _, _ := matcher.Match(userLang)
- base, _ := lang.Base()
- region, _ := lang.Region()
- util.Lang = base.String() + "_" + region.String()
- Conf.Lang = util.Lang
- logging.LogInfof("initialized language [%s] based on device locale", Conf.Lang)
- } else {
- logging.LogDebugf("check device locale failed [%s], using default language [en_US]", err)
- util.Lang = "en_US"
- Conf.Lang = util.Lang
- }
- }
- util.Lang = Conf.Lang
- }
- Conf.Langs = loadLangs()
- if nil == Conf.Appearance {
- Conf.Appearance = conf.NewAppearance()
- }
- var langOK bool
- for _, l := range Conf.Langs {
- if Conf.Lang == l.Name {
- langOK = true
- break
- }
- }
- if !langOK {
- Conf.Lang = "en_US"
- util.Lang = Conf.Lang
- }
- Conf.Appearance.Lang = Conf.Lang
- if nil == Conf.UILayout {
- Conf.UILayout = &conf.UILayout{}
- }
- if nil == Conf.Keymap {
- Conf.Keymap = &conf.Keymap{}
- }
- if "" == Conf.Appearance.CodeBlockThemeDark {
- Conf.Appearance.CodeBlockThemeDark = "dracula"
- }
- if "" == Conf.Appearance.CodeBlockThemeLight {
- Conf.Appearance.CodeBlockThemeLight = "github"
- }
- if nil == Conf.FileTree {
- Conf.FileTree = conf.NewFileTree()
- }
- if 1 > Conf.FileTree.MaxListCount {
- Conf.FileTree.MaxListCount = 512
- }
- if 1 > Conf.FileTree.MaxOpenTabCount {
- Conf.FileTree.MaxOpenTabCount = 8
- }
- if 32 < Conf.FileTree.MaxOpenTabCount {
- Conf.FileTree.MaxOpenTabCount = 32
- }
- Conf.FileTree.DocCreateSavePath = strings.TrimSpace(Conf.FileTree.DocCreateSavePath)
- util.UseSingleLineSave = Conf.FileTree.UseSingleLineSave
- util.CurrentCloudRegion = Conf.CloudRegion
- if nil == Conf.Tag {
- Conf.Tag = conf.NewTag()
- }
- if nil == Conf.Editor {
- Conf.Editor = conf.NewEditor()
- }
- if 1 > len(Conf.Editor.Emoji) {
- Conf.Editor.Emoji = []string{}
- }
- if 9 > Conf.Editor.FontSize || 72 < Conf.Editor.FontSize {
- Conf.Editor.FontSize = 16
- }
- if "" == Conf.Editor.PlantUMLServePath {
- Conf.Editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
- }
- if 1 > Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
- Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 64
- }
- if 5120 < Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
- Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 5120
- }
- if 1440 < Conf.Editor.GenerateHistoryInterval {
- Conf.Editor.GenerateHistoryInterval = 1440
- }
- if 1 > Conf.Editor.HistoryRetentionDays {
- Conf.Editor.HistoryRetentionDays = 30
- }
- if conf.MinDynamicLoadBlocks > Conf.Editor.DynamicLoadBlocks {
- Conf.Editor.DynamicLoadBlocks = conf.MinDynamicLoadBlocks
- }
- if conf.MaxDynamicLoadBlocks < Conf.Editor.DynamicLoadBlocks {
- Conf.Editor.DynamicLoadBlocks = conf.MaxDynamicLoadBlocks
- }
- if 0 > Conf.Editor.BacklinkExpandCount {
- Conf.Editor.BacklinkExpandCount = 0
- }
- if -1 > Conf.Editor.BackmentionExpandCount {
- Conf.Editor.BackmentionExpandCount = -1
- }
- if nil == Conf.Editor.Markdown {
- Conf.Editor.Markdown = &util.Markdown{}
- }
- util.MarkdownSettings = Conf.Editor.Markdown
- if nil == Conf.Export {
- Conf.Export = conf.NewExport()
- }
- if 0 == Conf.Export.BlockRefMode || 1 == Conf.Export.BlockRefMode {
- // 废弃导出选项引用块转换为原始块和引述块 https://github.com/siyuan-note/siyuan/issues/3155
- Conf.Export.BlockRefMode = 4 // 改为脚注
- }
- if "" == Conf.Export.PandocBin {
- Conf.Export.PandocBin = util.PandocBinPath
- }
- if nil == Conf.Graph || nil == Conf.Graph.Local || nil == Conf.Graph.Global {
- Conf.Graph = conf.NewGraph()
- }
- if nil == Conf.System {
- Conf.System = conf.NewSystem()
- if util.ContainerIOS != util.Container {
- Conf.OpenHelp = true
- }
- } else {
- if 0 < semver.Compare("v"+util.Ver, "v"+Conf.System.KernelVersion) {
- logging.LogInfof("upgraded from version [%s] to [%s]", Conf.System.KernelVersion, util.Ver)
- Conf.ShowChangelog = true
- } else if 0 > semver.Compare("v"+util.Ver, "v"+Conf.System.KernelVersion) {
- logging.LogInfof("downgraded from version [%s] to [%s]", Conf.System.KernelVersion, util.Ver)
- }
- Conf.System.KernelVersion = util.Ver
- Conf.System.IsInsider = util.IsInsider
- }
- if nil == Conf.System.NetworkProxy {
- Conf.System.NetworkProxy = &conf.NetworkProxy{}
- }
- if "" == Conf.System.ID {
- Conf.System.ID = util.GetDeviceID()
- }
- if "" == Conf.System.Name {
- Conf.System.Name = util.GetDeviceName()
- }
- if util.ContainerStd == util.Container {
- Conf.System.ID = util.GetDeviceID()
- Conf.System.Name = util.GetDeviceName()
- }
- if nil == Conf.Snippet {
- Conf.Snippet = conf.NewSnpt()
- }
- Conf.System.AppDir = util.WorkingDir
- Conf.System.ConfDir = util.ConfDir
- Conf.System.HomeDir = util.HomeDir
- Conf.System.WorkspaceDir = util.WorkspaceDir
- Conf.System.DataDir = util.DataDir
- Conf.System.Container = util.Container
- Conf.System.IsMicrosoftStore = util.ISMicrosoftStore
- if util.ISMicrosoftStore {
- logging.LogInfof("using Microsoft Store edition")
- }
- Conf.System.OS = runtime.GOOS
- Conf.System.OSPlatform = util.GetOSPlatform()
- if "" != Conf.UserData {
- Conf.SetUser(loadUserFromConf())
- }
- if nil == Conf.Account {
- Conf.Account = conf.NewAccount()
- }
- if nil == Conf.Sync {
- Conf.Sync = conf.NewSync()
- }
- if 0 == Conf.Sync.Mode {
- Conf.Sync.Mode = 1
- }
- if nil == Conf.Sync.S3 {
- Conf.Sync.S3 = &conf.S3{}
- }
- Conf.Sync.S3.Endpoint = util.NormalizeEndpoint(Conf.Sync.S3.Endpoint)
- Conf.Sync.S3.Timeout = util.NormalizeTimeout(Conf.Sync.S3.Timeout)
- if nil == Conf.Sync.WebDAV {
- Conf.Sync.WebDAV = &conf.WebDAV{}
- }
- Conf.Sync.WebDAV.Endpoint = util.NormalizeEndpoint(Conf.Sync.WebDAV.Endpoint)
- Conf.Sync.WebDAV.Timeout = util.NormalizeTimeout(Conf.Sync.WebDAV.Timeout)
- if util.ContainerDocker == util.Container {
- Conf.Sync.Perception = false
- }
- if nil == Conf.Api {
- Conf.Api = conf.NewAPI()
- }
- if nil == Conf.Bazaar {
- Conf.Bazaar = conf.NewBazaar()
- }
- if nil == Conf.Publish {
- Conf.Publish = conf.NewPublish()
- }
- if nil == Conf.Repo {
- Conf.Repo = conf.NewRepo()
- }
- if timingEnv := os.Getenv("SIYUAN_SYNC_INDEX_TIMING"); "" != timingEnv {
- val, err := strconv.Atoi(timingEnv)
- if nil == err {
- Conf.Repo.SyncIndexTiming = int64(val)
- }
- }
- if 12000 > Conf.Repo.SyncIndexTiming {
- Conf.Repo.SyncIndexTiming = 12 * 1000
- }
- if nil == Conf.Search {
- Conf.Search = conf.NewSearch()
- }
- if 1 > Conf.Search.Limit {
- Conf.Search.Limit = 64
- }
- if 32 > Conf.Search.Limit {
- Conf.Search.Limit = 32
- }
- if 1 > Conf.Search.BacklinkMentionKeywordsLimit {
- Conf.Search.BacklinkMentionKeywordsLimit = 512
- }
- if nil == Conf.Stat {
- Conf.Stat = conf.NewStat()
- }
- if nil == Conf.Flashcard {
- Conf.Flashcard = conf.NewFlashcard()
- }
- if 0 > Conf.Flashcard.NewCardLimit {
- Conf.Flashcard.NewCardLimit = 20
- }
- if 0 > Conf.Flashcard.ReviewCardLimit {
- Conf.Flashcard.ReviewCardLimit = 200
- }
- if 0 >= Conf.Flashcard.RequestRetention || 1 <= Conf.Flashcard.RequestRetention {
- Conf.Flashcard.RequestRetention = conf.NewFlashcard().RequestRetention
- }
- if 0 >= Conf.Flashcard.MaximumInterval || 36500 <= Conf.Flashcard.MaximumInterval {
- Conf.Flashcard.MaximumInterval = conf.NewFlashcard().MaximumInterval
- }
- if "" == Conf.Flashcard.Weights || 17 != len(strings.Split(Conf.Flashcard.Weights, ",")) {
- Conf.Flashcard.Weights = conf.NewFlashcard().Weights
- }
- if nil == Conf.AI {
- Conf.AI = conf.NewAI()
- }
- if "" == Conf.AI.OpenAI.APIModel {
- Conf.AI.OpenAI.APIModel = openai.GPT3Dot5Turbo
- }
- if "" == Conf.AI.OpenAI.APIUserAgent {
- Conf.AI.OpenAI.APIUserAgent = util.UserAgent
- }
- if strings.HasPrefix(Conf.AI.OpenAI.APIUserAgent, "SiYuan/") {
- Conf.AI.OpenAI.APIUserAgent = util.UserAgent
- }
- if "" == Conf.AI.OpenAI.APIProvider {
- Conf.AI.OpenAI.APIProvider = "OpenAI"
- }
- if 0 > Conf.AI.OpenAI.APIMaxTokens {
- Conf.AI.OpenAI.APIMaxTokens = 0
- }
- if 0 >= Conf.AI.OpenAI.APITemperature || 2 < Conf.AI.OpenAI.APITemperature {
- Conf.AI.OpenAI.APITemperature = 1.0
- }
- if 1 > Conf.AI.OpenAI.APIMaxContexts || 64 < Conf.AI.OpenAI.APIMaxContexts {
- Conf.AI.OpenAI.APIMaxContexts = 7
- }
- if "" != Conf.AI.OpenAI.APIKey {
- logging.LogInfof("OpenAI API enabled\n"+
- " userAgent=%s\n"+
- " baseURL=%s\n"+
- " timeout=%ds\n"+
- " proxy=%s\n"+
- " model=%s\n"+
- " maxTokens=%d\n"+
- " temperature=%.1f\n"+
- " maxContexts=%d",
- Conf.AI.OpenAI.APIUserAgent,
- Conf.AI.OpenAI.APIBaseURL,
- Conf.AI.OpenAI.APITimeout,
- Conf.AI.OpenAI.APIProxy,
- Conf.AI.OpenAI.APIModel,
- Conf.AI.OpenAI.APIMaxTokens,
- Conf.AI.OpenAI.APITemperature,
- Conf.AI.OpenAI.APIMaxContexts)
- }
- Conf.ReadOnly = util.ReadOnly
- if "" != util.AccessAuthCode {
- Conf.AccessAuthCode = util.AccessAuthCode
- }
- Conf.LocalIPs = util.GetLocalIPs()
- if 1 == Conf.State {
- // 上次未正常退出
- go func() {
- util.WaitForUILoaded()
- time.Sleep(2 * time.Second)
- if util.ContainerIOS == util.Container || util.ContainerAndroid == util.Container {
- util.PushMsg(Conf.language(245), 15000)
- } else {
- util.PushMsg(Conf.language(244), 15000)
- }
- }()
- }
- Conf.State = 1 // 运行中
- Conf.Save()
- logging.SetLogLevel(Conf.LogLevel)
- if Conf.System.UploadErrLog {
- logging.LogInfof("user has enabled [Automatically upload error messages and diagnostic data]")
- sentry.Init(sentry.ClientOptions{
- Dsn: "https://bdff135f14654ae58a054adeceb2c308@o1173696.ingest.sentry.io/6269178",
- Release: util.Ver,
- Environment: util.Mode,
- })
- }
- if Conf.System.DisableGoogleAnalytics {
- logging.LogInfof("user has disabled [Google Analytics]")
- }
- util.SetNetworkProxy(Conf.System.NetworkProxy.String())
- go util.InitPandoc()
- go util.InitTesseract()
- }
- func initLang() {
- p := filepath.Join(util.WorkingDir, "appearance", "langs")
- dir, err := os.Open(p)
- if nil != err {
- logging.LogErrorf("open language configuration folder [%s] failed: %s", p, err)
- util.ReportFileSysFatalError(err)
- return
- }
- defer dir.Close()
- langNames, err := dir.Readdirnames(-1)
- if nil != err {
- logging.LogErrorf("list language configuration folder [%s] failed: %s", p, err)
- util.ReportFileSysFatalError(err)
- return
- }
- for _, langName := range langNames {
- jsonPath := filepath.Join(p, langName)
- data, err := os.ReadFile(jsonPath)
- if nil != err {
- logging.LogErrorf("read language configuration [%s] failed: %s", jsonPath, err)
- continue
- }
- langMap := map[string]interface{}{}
- if err := gulu.JSON.UnmarshalJSON(data, &langMap); nil != err {
- logging.LogErrorf("parse language configuration failed [%s] failed: %s", jsonPath, err)
- continue
- }
- kernelMap := map[int]string{}
- label := langMap["_label"].(string)
- kernelLangs := langMap["_kernel"].(map[string]interface{})
- for k, v := range kernelLangs {
- num, convErr := strconv.Atoi(k)
- if nil != convErr {
- logging.LogErrorf("parse language configuration [%s] item [%d] failed: %s", p, num, convErr)
- continue
- }
- kernelMap[num] = v.(string)
- }
- kernelMap[-1] = label
- name := langName[:strings.LastIndex(langName, ".")]
- util.Langs[name] = kernelMap
- util.TimeLangs[name] = langMap["_time"].(map[string]interface{})
- util.TaskActionLangs[name] = langMap["_taskAction"].(map[string]interface{})
- util.TrayMenuLangs[name] = langMap["_trayMenu"].(map[string]interface{})
- util.AttrViewLangs[name] = langMap["_attrView"].(map[string]interface{})
- }
- }
- func loadLangs() (ret []*conf.Lang) {
- for name, langMap := range util.Langs {
- lang := &conf.Lang{Label: langMap[-1], Name: name}
- ret = append(ret, lang)
- }
- sort.Slice(ret, func(i, j int) bool {
- return ret[i].Name < ret[j].Name
- })
- return
- }
- var exitLock = sync.Mutex{}
- // Close 退出内核进程.
- //
- // force:是否不执行同步过程而直接退出
- //
- // setCurrentWorkspace:是否将当前工作空间放到工作空间列表的最后一个
- //
- // execInstallPkg:是否执行新版本安装包
- //
- // 0:默认按照设置项 System.DownloadInstallPkg 检查并推送提示
- // 1:不执行新版本安装
- // 2:执行新版本安装
- //
- // 返回值 exitCode:
- //
- // 0:正常退出
- // 1:同步执行失败
- // 2:提示新安装包
- //
- // 当 force 为 true(强制退出)并且 execInstallPkg 为 0(默认检查更新)并且同步失败并且新版本安装版已经准备就绪时,执行新版本安装 https://github.com/siyuan-note/siyuan/issues/10288
- func Close(force, setCurrentWorkspace bool, execInstallPkg int) (exitCode int) {
- exitLock.Lock()
- defer exitLock.Unlock()
- logging.LogInfof("exiting kernel [force=%v, setCurrentWorkspace=%v, execInstallPkg=%d]", force, setCurrentWorkspace, execInstallPkg)
- util.PushMsg(Conf.Language(95), 10000*60)
- WaitForWritingFiles()
- if !force {
- if Conf.Sync.Enabled && 3 != Conf.Sync.Mode &&
- ((IsSubscriber() && conf.ProviderSiYuan == Conf.Sync.Provider) || conf.ProviderSiYuan != Conf.Sync.Provider) {
- syncData(true, false)
- if 0 != ExitSyncSucc {
- exitCode = 1
- return
- }
- }
- }
- // Close the user guide when exiting https://github.com/siyuan-note/siyuan/issues/10322
- closeUserGuide()
- util.IsExiting.Store(true)
- waitSecondForExecInstallPkg := false
- if !skipNewVerInstallPkg() {
- if newVerInstallPkgPath := getNewVerInstallPkgPath(); "" != newVerInstallPkgPath {
- if 2 == execInstallPkg || (force && 0 == execInstallPkg) { // 执行新版本安装
- waitSecondForExecInstallPkg = true
- if gulu.OS.IsWindows() {
- util.PushMsg(Conf.Language(130), 1000*30)
- }
- go execNewVerInstallPkg(newVerInstallPkgPath)
- } else if 0 == execInstallPkg { // 新版本安装包已经准备就绪
- exitCode = 2
- logging.LogInfof("the new version install pkg is ready [%s], waiting for the user's next instruction", newVerInstallPkgPath)
- return
- }
- }
- }
- Conf.Close()
- sql.CloseDatabase()
- treenode.SaveBlockTree(false)
- util.SaveAssetsTexts()
- clearWorkspaceTemp()
- clearCorruptedNotebooks()
- clearPortJSON()
- if setCurrentWorkspace {
- // 将当前工作空间放到工作空间列表的最后一个
- // Open the last workspace by default https://github.com/siyuan-note/siyuan/issues/10570
- workspacePaths, err := util.ReadWorkspacePaths()
- if nil != err {
- logging.LogErrorf("read workspace paths failed: %s", err)
- } else {
- workspacePaths = gulu.Str.RemoveElem(workspacePaths, util.WorkspaceDir)
- workspacePaths = append(workspacePaths, util.WorkspaceDir)
- util.WriteWorkspacePaths(workspacePaths)
- }
- }
- util.UnlockWorkspace()
- time.Sleep(500 * time.Millisecond)
- if waitSecondForExecInstallPkg {
- // 桌面端退出拉起更新安装时有时需要重启两次 https://github.com/siyuan-note/siyuan/issues/6544
- // 这里多等待一段时间,等待安装程序启动
- if gulu.OS.IsWindows() {
- time.Sleep(30 * time.Second)
- }
- }
- closeSyncWebSocket()
- go func() {
- time.Sleep(500 * time.Millisecond)
- logging.LogInfof("exited kernel")
- util.WebSocketServer.Close()
- os.Exit(logging.ExitCodeOk)
- }()
- return
- }
- var CustomEmojis = sync.Map{}
- func NewLute() (ret *lute.Lute) {
- ret = util.NewLute()
- ret.SetCodeSyntaxHighlightLineNum(Conf.Editor.CodeSyntaxHighlightLineNum)
- ret.SetChineseParagraphBeginningSpace(Conf.Export.ParagraphBeginningSpace)
- ret.SetProtyleMarkNetImg(Conf.Editor.DisplayNetImgMark)
- ret.SetSpellcheck(Conf.Editor.Spellcheck)
- customEmojiMap := map[string]string{}
- CustomEmojis.Range(func(key, value interface{}) bool {
- customEmojiMap[key.(string)] = value.(string)
- return true
- })
- ret.PutEmojis(customEmojiMap)
- return
- }
- func (conf *AppConf) Save() {
- if util.ReadOnly {
- return
- }
- Conf.m.Lock()
- defer Conf.m.Unlock()
- newData, _ := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
- confPath := filepath.Join(util.ConfDir, "conf.json")
- oldData, err := filelock.ReadFile(confPath)
- if nil != err {
- conf.save0(newData)
- return
- }
- if bytes.Equal(newData, oldData) {
- return
- }
- conf.save0(newData)
- }
- func (conf *AppConf) save0(data []byte) {
- confPath := filepath.Join(util.ConfDir, "conf.json")
- if err := filelock.WriteFile(confPath, data); nil != err {
- logging.LogErrorf("write conf [%s] failed: %s", confPath, err)
- util.ReportFileSysFatalError(err)
- return
- }
- }
- func (conf *AppConf) Close() {
- conf.State = 0 // 已经正常退出
- conf.Save()
- }
- func (conf *AppConf) Box(boxID string) *Box {
- for _, box := range conf.GetOpenedBoxes() {
- if box.ID == boxID {
- return box
- }
- }
- return nil
- }
- func (conf *AppConf) GetBox(boxID string) *Box {
- for _, box := range conf.GetBoxes() {
- if box.ID == boxID {
- return box
- }
- }
- return nil
- }
- func (conf *AppConf) BoxNames(boxIDs []string) (ret map[string]string) {
- ret = map[string]string{}
- boxes := conf.GetOpenedBoxes()
- for _, boxID := range boxIDs {
- for _, box := range boxes {
- if box.ID == boxID {
- ret[boxID] = box.Name
- break
- }
- }
- }
- return
- }
- func (conf *AppConf) GetBoxes() (ret []*Box) {
- ret = []*Box{}
- notebooks, err := ListNotebooks()
- if nil != err {
- return
- }
- for _, notebook := range notebooks {
- id := notebook.ID
- name := notebook.Name
- closed := notebook.Closed
- box := &Box{ID: id, Name: name, Closed: closed}
- ret = append(ret, box)
- }
- return
- }
- func (conf *AppConf) GetOpenedBoxes() (ret []*Box) {
- ret = []*Box{}
- notebooks, err := ListNotebooks()
- if nil != err {
- return
- }
- for _, notebook := range notebooks {
- if !notebook.Closed {
- ret = append(ret, notebook)
- }
- }
- return
- }
- func (conf *AppConf) GetClosedBoxes() (ret []*Box) {
- ret = []*Box{}
- notebooks, err := ListNotebooks()
- if nil != err {
- return
- }
- for _, notebook := range notebooks {
- if notebook.Closed {
- ret = append(ret, notebook)
- }
- }
- return
- }
- func (conf *AppConf) Language(num int) (ret string) {
- ret = conf.language(num)
- subscribeURL := util.GetCloudAccountServer() + "/subscribe/siyuan"
- ret = strings.ReplaceAll(ret, "${url}", subscribeURL)
- return
- }
- func (conf *AppConf) language(num int) (ret string) {
- ret = util.Langs[conf.Lang][num]
- if "" != ret {
- return
- }
- ret = util.Langs["en_US"][num]
- return
- }
- func InitBoxes() {
- initialized := false
- if 1 > treenode.CountBlocks() {
- if gulu.File.IsExist(util.BlockTreePath) {
- util.IncBootProgress(20, Conf.Language(91))
- go func() {
- for i := 0; i < 40; i++ {
- util.RandomSleep(50, 100)
- util.IncBootProgress(1, Conf.Language(91))
- }
- }()
- treenode.InitBlockTree(false)
- initialized = true
- }
- } else { // 大于 1 的话说明在同步阶段已经加载过了
- initialized = true
- }
- for _, box := range Conf.GetOpenedBoxes() {
- box.UpdateHistoryGenerated() // 初始化历史生成时间为当前时间
- if !initialized {
- index(box.ID)
- }
- }
- if !initialized {
- treenode.SaveBlockTree(true)
- }
- var dbSize string
- if dbFile, err := os.Stat(util.DBPath); nil == err {
- dbSize = humanize.BytesCustomCeil(uint64(dbFile.Size()), 2)
- }
- logging.LogInfof("database size [%s], tree/block count [%d/%d]", dbSize, treenode.CountTrees(), treenode.CountBlocks())
- }
- func IsSubscriber() bool {
- u := Conf.GetUser()
- return nil != u && (-1 == u.UserSiYuanProExpireTime || 0 < u.UserSiYuanProExpireTime) && 0 == u.UserSiYuanSubscriptionStatus
- }
- func IsPaidUser() bool {
- // S3/WebDAV data sync and backup are available for a fee https://github.com/siyuan-note/siyuan/issues/8780
- if IsSubscriber() {
- return true
- }
- u := Conf.GetUser()
- if nil == u {
- return false
- }
- return 1 == u.UserSiYuanOneTimePayStatus
- }
- const (
- MaskedUserData = ""
- MaskedAccessAuthCode = "*******"
- )
- func GetMaskedConf() (ret *AppConf, err error) {
- // 脱敏处理
- data, err := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
- if nil != err {
- logging.LogErrorf("marshal conf failed: %s", err)
- return
- }
- ret = &AppConf{}
- if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err {
- logging.LogErrorf("unmarshal conf failed: %s", err)
- return
- }
- ret.UserData = MaskedUserData
- if "" != ret.AccessAuthCode {
- ret.AccessAuthCode = MaskedAccessAuthCode
- }
- return
- }
- // REF: https://github.com/siyuan-note/siyuan/issues/11364
- // HideConfSecret 隐藏设置中的秘密信息
- func HideConfSecret(c *AppConf) {
- c.AI = &conf.AI{}
- c.Api = &conf.API{}
- c.Flashcard = &conf.Flashcard{}
- c.LocalIPs = []string{}
- c.Publish = &conf.Publish{}
- c.Repo = &conf.Repo{}
- c.Sync = &conf.Sync{}
- c.System.AppDir = ""
- c.System.ConfDir = ""
- c.System.DataDir = ""
- c.System.HomeDir = ""
- c.System.Name = ""
- c.System.NetworkProxy = &conf.NetworkProxy{}
- }
- func clearPortJSON() {
- pid := fmt.Sprintf("%d", os.Getpid())
- portJSON := filepath.Join(util.HomeDir, ".config", "siyuan", "port.json")
- pidPorts := map[string]string{}
- var data []byte
- var err error
- if gulu.File.IsExist(portJSON) {
- data, err = os.ReadFile(portJSON)
- if nil != err {
- logging.LogWarnf("read port.json failed: %s", err)
- } else {
- if err = gulu.JSON.UnmarshalJSON(data, &pidPorts); nil != err {
- logging.LogWarnf("unmarshal port.json failed: %s", err)
- }
- }
- }
- delete(pidPorts, pid)
- if data, err = gulu.JSON.MarshalIndentJSON(pidPorts, "", " "); nil != err {
- logging.LogWarnf("marshal port.json failed: %s", err)
- } else {
- if err = os.WriteFile(portJSON, data, 0644); nil != err {
- logging.LogWarnf("write port.json failed: %s", err)
- }
- }
- }
- func clearCorruptedNotebooks() {
- // 数据同步时展开文档树操作可能导致数据丢失 https://github.com/siyuan-note/siyuan/issues/7129
- dirs, err := os.ReadDir(util.DataDir)
- if nil != err {
- logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
- return
- }
- for _, dir := range dirs {
- if util.IsReservedFilename(dir.Name()) {
- continue
- }
- if !dir.IsDir() {
- continue
- }
- if !ast.IsNodeIDPattern(dir.Name()) {
- continue
- }
- boxDirPath := filepath.Join(util.DataDir, dir.Name())
- boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json")
- if !filelock.IsExist(boxConfPath) {
- logging.LogWarnf("found a corrupted box [%s]", boxDirPath)
- continue
- }
- }
- }
- func clearWorkspaceTemp() {
- os.RemoveAll(filepath.Join(util.TempDir, "bazaar"))
- os.RemoveAll(filepath.Join(util.TempDir, "export"))
- os.RemoveAll(filepath.Join(util.TempDir, "convert"))
- os.RemoveAll(filepath.Join(util.TempDir, "import"))
- os.RemoveAll(filepath.Join(util.TempDir, "repo"))
- os.RemoveAll(filepath.Join(util.TempDir, "os"))
- os.RemoveAll(filepath.Join(util.TempDir, "blocktree.msgpack")) // v2.7.2 前旧版的块数数据
- // 退出时自动删除超过 7 天的安装包 https://github.com/siyuan-note/siyuan/issues/6128
- install := filepath.Join(util.TempDir, "install")
- if gulu.File.IsDir(install) {
- monthAgo := time.Now().Add(-time.Hour * 24 * 7)
- entries, err := os.ReadDir(install)
- if nil != err {
- logging.LogErrorf("read dir [%s] failed: %s", install, err)
- } else {
- for _, entry := range entries {
- info, _ := entry.Info()
- if nil != info && !info.IsDir() && info.ModTime().Before(monthAgo) {
- if err = os.RemoveAll(filepath.Join(install, entry.Name())); nil != err {
- logging.LogErrorf("remove old install pkg [%s] failed: %s", filepath.Join(install, entry.Name()), err)
- }
- }
- }
- }
- }
- tmps, err := filepath.Glob(filepath.Join(util.TempDir, "*.tmp"))
- if nil != err {
- logging.LogErrorf("glob temp files failed: %s", err)
- }
- for _, tmp := range tmps {
- if err = os.RemoveAll(tmp); nil != err {
- logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
- } else {
- logging.LogInfof("removed temp file [%s]", tmp)
- }
- }
- tmps, err = filepath.Glob(filepath.Join(util.DataDir, ".siyuan", "*.tmp"))
- if nil != err {
- logging.LogErrorf("glob temp files failed: %s", err)
- }
- for _, tmp := range tmps {
- if err = os.RemoveAll(tmp); nil != err {
- logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
- } else {
- logging.LogInfof("removed temp file [%s]", tmp)
- }
- }
- // 老版本遗留文件清理
- os.RemoveAll(filepath.Join(util.DataDir, "assets", ".siyuan", "assets.json"))
- os.RemoveAll(filepath.Join(util.DataDir, ".siyuan", "history"))
- os.RemoveAll(filepath.Join(util.WorkspaceDir, "backup"))
- os.RemoveAll(filepath.Join(util.WorkspaceDir, "sync"))
- os.RemoveAll(filepath.Join(util.DataDir, "%")) // v3.0.6 生成的错误历史文件夹
- logging.LogInfof("cleared workspace temp")
- }
- func closeUserGuide() {
- defer logging.Recover()
- dirs, err := os.ReadDir(util.DataDir)
- if nil != err {
- logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
- return
- }
- for _, dir := range dirs {
- if !IsUserGuide(dir.Name()) {
- continue
- }
- boxID := dir.Name()
- boxDirPath := filepath.Join(util.DataDir, boxID)
- boxConf := conf.NewBoxConf()
- boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json")
- if !filelock.IsExist(boxConfPath) {
- logging.LogWarnf("found a corrupted user guide box [%s]", boxDirPath)
- if removeErr := filelock.Remove(boxDirPath); nil != removeErr {
- logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr)
- } else {
- logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath)
- }
- continue
- }
- data, readErr := filelock.ReadFile(boxConfPath)
- if nil != readErr {
- logging.LogErrorf("read box conf [%s] failed: %s", boxConfPath, readErr)
- if removeErr := filelock.Remove(boxDirPath); nil != removeErr {
- logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr)
- } else {
- logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath)
- }
- continue
- }
- if readErr = gulu.JSON.UnmarshalJSON(data, boxConf); nil != readErr {
- logging.LogErrorf("parse box conf [%s] failed: %s", boxConfPath, readErr)
- if removeErr := filelock.Remove(boxDirPath); nil != removeErr {
- logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr)
- } else {
- logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath)
- }
- continue
- }
- if boxConf.Closed {
- continue
- }
- msgId := util.PushMsg(Conf.language(233), 30000)
- evt := util.NewCmdResult("unmount", 0, util.PushModeBroadcast)
- evt.Data = map[string]interface{}{
- "box": boxID,
- }
- util.PushEvent(evt)
- unindex(boxID)
- if removeErr := filelock.Remove(boxDirPath); nil != removeErr {
- logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr)
- }
- sql.WaitForWritingDatabase()
- util.PushClearMsg(msgId)
- logging.LogInfof("closed user guide box [%s]", boxID)
- }
- }
- func init() {
- subscribeConfEvents()
- }
- func subscribeConfEvents() {
- eventbus.Subscribe(util.EvtConfPandocInitialized, func() {
- logging.LogInfof("pandoc initialized, set pandoc bin to [%s]", util.PandocBinPath)
- Conf.Export.PandocBin = util.PandocBinPath
- Conf.Save()
- })
- }
|