123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541 |
- // SiYuan - Build Your Eternal Digital Garden
- // 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 util
- import (
- "bytes"
- "flag"
- "log"
- "math/rand"
- "mime"
- "os"
- "os/exec"
- "path/filepath"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/88250/gulu"
- figure "github.com/common-nighthawk/go-figure"
- goPS "github.com/mitchellh/go-ps"
- "github.com/siyuan-note/httpclient"
- "github.com/siyuan-note/logging"
- )
- // var Mode = "dev"
- var Mode = "prod"
- const (
- Ver = "2.4.5"
- IsInsider = false
- )
- var (
- bootProgress float64 // 启动进度,从 0 到 100
- bootDetails string // 启动细节描述
- HttpServing = false // 是否 HTTP 伺服已经可用
- )
- func Boot() {
- IncBootProgress(3, "Booting...")
- rand.Seed(time.Now().UTC().UnixNano())
- initMime()
- workspacePath := flag.String("workspace", "", "dir path of the workspace, default to ~/Documents/SiYuan/")
- wdPath := flag.String("wd", WorkingDir, "working directory of SiYuan")
- servePath := flag.String("servePath", "", "obsoleted https://github.com/siyuan-note/siyuan/issues/4647")
- _ = servePath
- resident := flag.Bool("resident", true, "resident memory even if no active session")
- readOnly := flag.Bool("readonly", false, "read-only mode")
- accessAuthCode := flag.String("accessAuthCode", "", "access auth code")
- ssl := flag.Bool("ssl", false, "for https and wss")
- lang := flag.String("lang", "", "zh_CN/zh_CHT/en_US/fr_FR/es_ES")
- mode := flag.String("mode", "prod", "dev/prod")
- flag.Parse()
- if "" != *wdPath {
- WorkingDir = *wdPath
- }
- if "" != *lang {
- Lang = *lang
- }
- Mode = *mode
- Resident = *resident
- ReadOnly = *readOnly
- AccessAuthCode = *accessAuthCode
- Container = ContainerStd
- if isRunningInDockerContainer() {
- Container = ContainerDocker
- }
- msStoreFilePath := filepath.Join(WorkingDir, "ms-store")
- ISMicrosoftStore = gulu.File.IsExist(msStoreFilePath)
- UserAgent = UserAgent + " " + Container
- httpclient.SetUserAgent(UserAgent)
- initWorkspaceDir(*workspacePath)
- SSL = *ssl
- LogPath = filepath.Join(TempDir, "siyuan.log")
- logging.SetLogPath(LogPath)
- AppearancePath = filepath.Join(ConfDir, "appearance")
- if "dev" == Mode {
- ThemesPath = filepath.Join(WorkingDir, "appearance", "themes")
- IconsPath = filepath.Join(WorkingDir, "appearance", "icons")
- } else {
- ThemesPath = filepath.Join(AppearancePath, "themes")
- IconsPath = filepath.Join(AppearancePath, "icons")
- }
- initPathDir()
- checkPort()
- go initPandoc()
- bootBanner := figure.NewColorFigure("SiYuan", "isometric3", "green", true)
- logging.LogInfof("\n" + bootBanner.String())
- logBootInfo()
- }
- func setBootDetails(details string) {
- bootDetails = "v" + Ver + " " + details
- }
- func SetBootDetails(details string) {
- if 100 <= bootProgress {
- return
- }
- setBootDetails(details)
- }
- func IncBootProgress(progress float64, details string) {
- if 100 <= bootProgress {
- return
- }
- bootProgress += progress
- setBootDetails(details)
- }
- func IsBooted() bool {
- return 100 <= bootProgress
- }
- func GetBootProgressDetails() (float64, string) {
- return bootProgress, bootDetails
- }
- func GetBootProgress() float64 {
- return bootProgress
- }
- func SetBooted() {
- setBootDetails("Finishing boot...")
- bootProgress = 100
- logging.LogInfof("kernel booted")
- }
- var (
- HomeDir, _ = gulu.OS.Home()
- WorkingDir, _ = os.Getwd()
- WorkspaceDir string // 工作空间目录路径
- ConfDir string // 配置目录路径
- DataDir string // 数据目录路径
- RepoDir string // 仓库目录路径
- HistoryDir string // 数据历史目录路径
- TempDir string // 临时目录路径
- LogPath string // 配置目录下的日志文件 siyuan.log 路径
- DBName = "siyuan.db" // SQLite 数据库文件名
- DBPath string // SQLite 数据库文件路径
- HistoryDBPath string // SQLite 历史数据库文件路径
- BlockTreePath string // 区块树文件路径
- PandocBinPath string // Pandoc 可执行文件路径
- AppearancePath string // 配置目录下的外观目录 appearance/ 路径
- ThemesPath string // 配置目录下的外观目录下的 themes/ 路径
- IconsPath string // 配置目录下的外观目录下的 icons/ 路径
- AndroidNativeLibDir string // Android 库路径
- AndroidPrivateDataDir string // Android 私有数据路径
- UIProcessIDs = sync.Map{} // UI 进程 ID
- IsNewbie bool // 是否是第一次安装
- )
- func initWorkspaceDir(workspaceArg string) {
- userHomeConfDir := filepath.Join(HomeDir, ".config", "siyuan")
- workspaceConf := filepath.Join(userHomeConfDir, "workspace.json")
- if !gulu.File.IsExist(workspaceConf) {
- IsNewbie = ContainerStd == Container // 只有桌面端需要设置新手标识,前端自动挂载帮助文档
- if err := os.MkdirAll(userHomeConfDir, 0755); nil != err && !os.IsExist(err) {
- log.Printf("create user home conf folder [%s] failed: %s", userHomeConfDir, err)
- os.Exit(ExitCodeCreateConfDirErr)
- }
- }
- defaultWorkspaceDir := filepath.Join(HomeDir, "Documents", "SiYuan")
- if gulu.OS.IsWindows() {
- // 改进 Windows 端默认工作空间路径 https://github.com/siyuan-note/siyuan/issues/5622
- if userProfile := os.Getenv("USERPROFILE"); "" != userProfile {
- defaultWorkspaceDir = filepath.Join(userProfile, "Documents", "SiYuan")
- }
- }
- var workspacePaths []string
- if !gulu.File.IsExist(workspaceConf) {
- WorkspaceDir = defaultWorkspaceDir
- if "" != workspaceArg {
- WorkspaceDir = workspaceArg
- }
- if !gulu.File.IsDir(WorkspaceDir) {
- log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
- WorkspaceDir = defaultWorkspaceDir
- }
- workspacePaths = append(workspacePaths, WorkspaceDir)
- } else {
- data, err := os.ReadFile(workspaceConf)
- if err = gulu.JSON.UnmarshalJSON(data, &workspacePaths); nil != err {
- log.Printf("unmarshal workspace conf [%s] failed: %s", workspaceConf, err)
- }
- tmp := workspacePaths[:0]
- for _, d := range workspacePaths {
- if gulu.File.IsDir(d) {
- tmp = append(tmp, d)
- }
- }
- workspacePaths = tmp
- if 0 < len(workspacePaths) {
- WorkspaceDir = workspacePaths[len(workspacePaths)-1]
- if "" != workspaceArg {
- WorkspaceDir = workspaceArg
- }
- if !gulu.File.IsDir(WorkspaceDir) {
- log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
- WorkspaceDir = defaultWorkspaceDir
- }
- workspacePaths[len(workspacePaths)-1] = WorkspaceDir
- } else {
- WorkspaceDir = defaultWorkspaceDir
- if "" != workspaceArg {
- WorkspaceDir = workspaceArg
- }
- if !gulu.File.IsDir(WorkspaceDir) {
- log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
- WorkspaceDir = defaultWorkspaceDir
- }
- workspacePaths = append(workspacePaths, WorkspaceDir)
- }
- }
- if data, err := gulu.JSON.MarshalJSON(workspacePaths); nil == err {
- if err = os.WriteFile(workspaceConf, data, 0644); nil != err {
- log.Fatalf("write workspace conf [%s] failed: %s", workspaceConf, err)
- }
- } else {
- log.Fatalf("marshal workspace conf [%s] failed: %s", workspaceConf, err)
- }
- ConfDir = filepath.Join(WorkspaceDir, "conf")
- DataDir = filepath.Join(WorkspaceDir, "data")
- RepoDir = filepath.Join(WorkspaceDir, "repo")
- HistoryDir = filepath.Join(WorkspaceDir, "history")
- TempDir = filepath.Join(WorkspaceDir, "temp")
- osTmpDir := filepath.Join(TempDir, "os")
- os.RemoveAll(osTmpDir)
- if err := os.MkdirAll(osTmpDir, 0755); nil != err {
- log.Fatalf("create os tmp dir [%s] failed: %s", osTmpDir, err)
- }
- os.RemoveAll(filepath.Join(TempDir, "repo"))
- os.Setenv("TMPDIR", osTmpDir)
- os.Setenv("TEMP", osTmpDir)
- os.Setenv("TMP", osTmpDir)
- DBPath = filepath.Join(TempDir, DBName)
- HistoryDBPath = filepath.Join(TempDir, "history.db")
- BlockTreePath = filepath.Join(TempDir, "blocktree.msgpack")
- }
- var (
- Resident bool
- ReadOnly bool
- AccessAuthCode string
- Lang = ""
- Container string // docker, android, ios, std
- ISMicrosoftStore bool // 桌面端是否是微软商店版
- )
- const (
- ContainerStd = "std" // 桌面端
- ContainerDocker = "docker" // Docker 容器端
- ContainerAndroid = "android" // Android 端
- ContainerIOS = "ios" // iOS 端
- )
- func initPathDir() {
- if err := os.MkdirAll(ConfDir, 0755); nil != err && !os.IsExist(err) {
- log.Fatalf("create conf folder [%s] failed: %s", ConfDir, err)
- }
- if err := os.MkdirAll(DataDir, 0755); nil != err && !os.IsExist(err) {
- log.Fatalf("create data folder [%s] failed: %s", DataDir, err)
- }
- if err := os.MkdirAll(TempDir, 0755); nil != err && !os.IsExist(err) {
- log.Fatalf("create temp folder [%s] failed: %s", TempDir, err)
- }
- assets := filepath.Join(DataDir, "assets")
- if err := os.MkdirAll(assets, 0755); nil != err && !os.IsExist(err) {
- log.Fatalf("create data assets folder [%s] failed: %s", assets, err)
- }
- templates := filepath.Join(DataDir, "templates")
- if err := os.MkdirAll(templates, 0755); nil != err && !os.IsExist(err) {
- log.Fatalf("create data templates folder [%s] failed: %s", templates, err)
- }
- widgets := filepath.Join(DataDir, "widgets")
- if err := os.MkdirAll(widgets, 0755); nil != err && !os.IsExist(err) {
- log.Fatalf("create data widgets folder [%s] failed: %s", widgets, err)
- }
- emojis := filepath.Join(DataDir, "emojis")
- if err := os.MkdirAll(emojis, 0755); nil != err && !os.IsExist(err) {
- log.Fatalf("create data emojis folder [%s] failed: %s", widgets, err)
- }
- }
- func checkPort() {
- portOpened := isPortOpen(ServerPort)
- if !portOpened {
- return
- }
- logging.LogInfof("port [%s] is opened, try to check version of running kernel", ServerPort)
- result := NewResult()
- _, err := httpclient.NewBrowserRequest().
- SetResult(result).
- SetHeader("User-Agent", UserAgent).
- Get("http://127.0.0.1:" + ServerPort + "/api/system/version")
- if nil != err || 0 != result.Code {
- logging.LogErrorf("connect to port [%s] for checking running kernel failed", ServerPort)
- KillByPort(ServerPort)
- return
- }
- if nil == result.Data {
- logging.LogErrorf("connect ot port [%s] for checking running kernel failed", ServerPort)
- os.Exit(ExitCodeUnavailablePort)
- }
- runningVer := result.Data.(string)
- if runningVer == Ver {
- logging.LogInfof("version of the running kernel is the same as this boot [%s], exit this boot", runningVer)
- os.Exit(ExitCodeOk)
- }
- logging.LogInfof("found kernel [%s] is running, try to exit it", runningVer)
- processes, err := goPS.Processes()
- if nil != err {
- logging.LogErrorf("close kernel [%s] failed: %s", runningVer, err)
- os.Exit(ExitCodeUnavailablePort)
- }
- currentPid := os.Getpid()
- for _, p := range processes {
- name := p.Executable()
- if strings.Contains(strings.ToLower(name), "siyuan-kernel") || strings.Contains(strings.ToLower(name), "siyuan kernel") {
- kernelPid := p.Pid()
- if currentPid != kernelPid {
- pid := strconv.Itoa(kernelPid)
- Kill(pid)
- logging.LogInfof("killed kernel [name=%s, pid=%s, ver=%s], continue to boot", name, pid, runningVer)
- }
- }
- }
- if !tryToListenPort() {
- os.Exit(ExitCodeUnavailablePort)
- }
- }
- func initMime() {
- // 在某版本的 Windows 10 操作系统上界面样式异常问题
- // https://github.com/siyuan-note/siyuan/issues/247
- // https://github.com/siyuan-note/siyuan/issues/3813
- mime.AddExtensionType(".css", "text/css")
- mime.AddExtensionType(".js", "application/x-javascript")
- mime.AddExtensionType(".json", "application/json")
- mime.AddExtensionType(".html", "text/html")
- }
- func KillByPort(port string) {
- if pid := PidByPort(port); "" != pid {
- pidInt, _ := strconv.Atoi(pid)
- proc, _ := goPS.FindProcess(pidInt)
- var name string
- if nil != proc {
- name = proc.Executable()
- }
- Kill(pid)
- logging.LogInfof("killed process [name=%s, pid=%s]", name, pid)
- }
- }
- func Kill(pid string) {
- var kill *exec.Cmd
- if gulu.OS.IsWindows() {
- kill = exec.Command("cmd", "/c", "TASKKILL /F /PID "+pid)
- } else {
- kill = exec.Command("kill", "-9", pid)
- }
- gulu.CmdAttr(kill)
- kill.CombinedOutput()
- }
- func PidByPort(port string) (ret string) {
- if gulu.OS.IsWindows() {
- cmd := exec.Command("cmd", "/c", "netstat -ano | findstr "+port)
- gulu.CmdAttr(cmd)
- data, err := cmd.CombinedOutput()
- if nil != err {
- logging.LogErrorf("netstat failed: %s", err)
- return
- }
- output := string(data)
- lines := strings.Split(output, "\n")
- for _, l := range lines {
- if strings.Contains(l, "LISTENING") {
- l = l[strings.Index(l, "LISTENING")+len("LISTENING"):]
- l = strings.TrimSpace(l)
- ret = l
- return
- }
- }
- return
- }
- cmd := exec.Command("lsof", "-Fp", "-i", ":"+port)
- gulu.CmdAttr(cmd)
- data, err := cmd.CombinedOutput()
- if nil != err {
- logging.LogErrorf("lsof failed: %s", err)
- return
- }
- output := string(data)
- lines := strings.Split(output, "\n")
- for _, l := range lines {
- if strings.HasPrefix(l, "p") {
- l = l[1:]
- ret = l
- return
- }
- }
- return
- }
- func initPandoc() {
- if ContainerStd != Container {
- return
- }
- pandocDir := filepath.Join(TempDir, "pandoc")
- if gulu.OS.IsWindows() {
- PandocBinPath = filepath.Join(pandocDir, "bin", "pandoc.exe")
- } else if gulu.OS.IsDarwin() || gulu.OS.IsLinux() {
- PandocBinPath = filepath.Join(pandocDir, "bin", "pandoc")
- }
- pandocVer := getPandocVer(PandocBinPath)
- if "" != pandocVer {
- logging.LogInfof("built-in pandoc [ver=%s, bin=%s]", pandocVer, PandocBinPath)
- return
- }
- pandocZip := filepath.Join(WorkingDir, "pandoc.zip")
- if "dev" == Mode {
- if gulu.OS.IsWindows() {
- pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-windows-amd64.zip")
- } else if gulu.OS.IsDarwin() {
- pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-darwin-amd64.zip")
- } else if gulu.OS.IsLinux() {
- pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-linux-amd64.zip")
- }
- }
- if err := gulu.Zip.Unzip(pandocZip, pandocDir); nil != err {
- logging.LogErrorf("unzip pandoc failed: %s", err)
- return
- }
- if gulu.OS.IsDarwin() || gulu.OS.IsLinux() {
- exec.Command("chmod", "+x", PandocBinPath).CombinedOutput()
- }
- pandocVer = getPandocVer(PandocBinPath)
- logging.LogInfof("initialized built-in pandoc [ver=%s, bin=%s]", pandocVer, PandocBinPath)
- }
- func getPandocVer(binPath string) (ret string) {
- if "" == binPath {
- return
- }
- cmd := exec.Command(binPath, "--version")
- gulu.CmdAttr(cmd)
- data, err := cmd.CombinedOutput()
- if nil == err && strings.HasPrefix(string(data), "pandoc") {
- parts := bytes.Split(data, []byte("\n"))
- if 0 < len(parts) {
- ret = strings.TrimPrefix(string(parts[0]), "pandoc")
- ret = strings.ReplaceAll(ret, ".exe", "")
- ret = strings.TrimSpace(ret)
- }
- return
- }
- return
- }
- func IsValidPandocBin(binPath string) bool {
- if "" == binPath {
- return false
- }
- cmd := exec.Command(binPath, "--version")
- gulu.CmdAttr(cmd)
- data, err := cmd.CombinedOutput()
- if nil == err && strings.HasPrefix(string(data), "pandoc") {
- return true
- }
- return false
- }
- func GetDataAssetsAbsPath() (ret string) {
- ret = filepath.Join(DataDir, "assets")
- var err error
- stat, err := os.Lstat(ret)
- if nil != err {
- logging.LogErrorf("stat assets failed: %s", err)
- return
- }
- if 0 != stat.Mode()&os.ModeSymlink {
- // 跟随符号链接 https://github.com/siyuan-note/siyuan/issues/5480
- ret, err = os.Readlink(ret)
- if nil != err {
- logging.LogErrorf("read assets link failed: %s", err)
- }
- }
- return
- }
|