working.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. // SiYuan - Build Your Eternal Digital Garden
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package util
  17. import (
  18. "bytes"
  19. "flag"
  20. "log"
  21. "math/rand"
  22. "mime"
  23. "os"
  24. "os/exec"
  25. "path/filepath"
  26. "strconv"
  27. "strings"
  28. "sync"
  29. "time"
  30. "github.com/88250/gulu"
  31. figure "github.com/common-nighthawk/go-figure"
  32. goPS "github.com/mitchellh/go-ps"
  33. "github.com/siyuan-note/httpclient"
  34. "github.com/siyuan-note/logging"
  35. )
  36. // var Mode = "dev"
  37. var Mode = "prod"
  38. const (
  39. Ver = "2.4.5"
  40. IsInsider = false
  41. )
  42. var (
  43. bootProgress float64 // 启动进度,从 0 到 100
  44. bootDetails string // 启动细节描述
  45. HttpServing = false // 是否 HTTP 伺服已经可用
  46. )
  47. func Boot() {
  48. IncBootProgress(3, "Booting...")
  49. rand.Seed(time.Now().UTC().UnixNano())
  50. initMime()
  51. workspacePath := flag.String("workspace", "", "dir path of the workspace, default to ~/Documents/SiYuan/")
  52. wdPath := flag.String("wd", WorkingDir, "working directory of SiYuan")
  53. servePath := flag.String("servePath", "", "obsoleted https://github.com/siyuan-note/siyuan/issues/4647")
  54. _ = servePath
  55. resident := flag.Bool("resident", true, "resident memory even if no active session")
  56. readOnly := flag.Bool("readonly", false, "read-only mode")
  57. accessAuthCode := flag.String("accessAuthCode", "", "access auth code")
  58. ssl := flag.Bool("ssl", false, "for https and wss")
  59. lang := flag.String("lang", "", "zh_CN/zh_CHT/en_US/fr_FR/es_ES")
  60. mode := flag.String("mode", "prod", "dev/prod")
  61. flag.Parse()
  62. if "" != *wdPath {
  63. WorkingDir = *wdPath
  64. }
  65. if "" != *lang {
  66. Lang = *lang
  67. }
  68. Mode = *mode
  69. Resident = *resident
  70. ReadOnly = *readOnly
  71. AccessAuthCode = *accessAuthCode
  72. Container = ContainerStd
  73. if isRunningInDockerContainer() {
  74. Container = ContainerDocker
  75. }
  76. msStoreFilePath := filepath.Join(WorkingDir, "ms-store")
  77. ISMicrosoftStore = gulu.File.IsExist(msStoreFilePath)
  78. UserAgent = UserAgent + " " + Container
  79. httpclient.SetUserAgent(UserAgent)
  80. initWorkspaceDir(*workspacePath)
  81. SSL = *ssl
  82. LogPath = filepath.Join(TempDir, "siyuan.log")
  83. logging.SetLogPath(LogPath)
  84. AppearancePath = filepath.Join(ConfDir, "appearance")
  85. if "dev" == Mode {
  86. ThemesPath = filepath.Join(WorkingDir, "appearance", "themes")
  87. IconsPath = filepath.Join(WorkingDir, "appearance", "icons")
  88. } else {
  89. ThemesPath = filepath.Join(AppearancePath, "themes")
  90. IconsPath = filepath.Join(AppearancePath, "icons")
  91. }
  92. initPathDir()
  93. checkPort()
  94. go initPandoc()
  95. bootBanner := figure.NewColorFigure("SiYuan", "isometric3", "green", true)
  96. logging.LogInfof("\n" + bootBanner.String())
  97. logBootInfo()
  98. }
  99. func setBootDetails(details string) {
  100. bootDetails = "v" + Ver + " " + details
  101. }
  102. func SetBootDetails(details string) {
  103. if 100 <= bootProgress {
  104. return
  105. }
  106. setBootDetails(details)
  107. }
  108. func IncBootProgress(progress float64, details string) {
  109. if 100 <= bootProgress {
  110. return
  111. }
  112. bootProgress += progress
  113. setBootDetails(details)
  114. }
  115. func IsBooted() bool {
  116. return 100 <= bootProgress
  117. }
  118. func GetBootProgressDetails() (float64, string) {
  119. return bootProgress, bootDetails
  120. }
  121. func GetBootProgress() float64 {
  122. return bootProgress
  123. }
  124. func SetBooted() {
  125. setBootDetails("Finishing boot...")
  126. bootProgress = 100
  127. logging.LogInfof("kernel booted")
  128. }
  129. var (
  130. HomeDir, _ = gulu.OS.Home()
  131. WorkingDir, _ = os.Getwd()
  132. WorkspaceDir string // 工作空间目录路径
  133. ConfDir string // 配置目录路径
  134. DataDir string // 数据目录路径
  135. RepoDir string // 仓库目录路径
  136. HistoryDir string // 数据历史目录路径
  137. TempDir string // 临时目录路径
  138. LogPath string // 配置目录下的日志文件 siyuan.log 路径
  139. DBName = "siyuan.db" // SQLite 数据库文件名
  140. DBPath string // SQLite 数据库文件路径
  141. HistoryDBPath string // SQLite 历史数据库文件路径
  142. BlockTreePath string // 区块树文件路径
  143. PandocBinPath string // Pandoc 可执行文件路径
  144. AppearancePath string // 配置目录下的外观目录 appearance/ 路径
  145. ThemesPath string // 配置目录下的外观目录下的 themes/ 路径
  146. IconsPath string // 配置目录下的外观目录下的 icons/ 路径
  147. AndroidNativeLibDir string // Android 库路径
  148. AndroidPrivateDataDir string // Android 私有数据路径
  149. UIProcessIDs = sync.Map{} // UI 进程 ID
  150. IsNewbie bool // 是否是第一次安装
  151. )
  152. func initWorkspaceDir(workspaceArg string) {
  153. userHomeConfDir := filepath.Join(HomeDir, ".config", "siyuan")
  154. workspaceConf := filepath.Join(userHomeConfDir, "workspace.json")
  155. if !gulu.File.IsExist(workspaceConf) {
  156. IsNewbie = ContainerStd == Container // 只有桌面端需要设置新手标识,前端自动挂载帮助文档
  157. if err := os.MkdirAll(userHomeConfDir, 0755); nil != err && !os.IsExist(err) {
  158. log.Printf("create user home conf folder [%s] failed: %s", userHomeConfDir, err)
  159. os.Exit(ExitCodeCreateConfDirErr)
  160. }
  161. }
  162. defaultWorkspaceDir := filepath.Join(HomeDir, "Documents", "SiYuan")
  163. if gulu.OS.IsWindows() {
  164. // 改进 Windows 端默认工作空间路径 https://github.com/siyuan-note/siyuan/issues/5622
  165. if userProfile := os.Getenv("USERPROFILE"); "" != userProfile {
  166. defaultWorkspaceDir = filepath.Join(userProfile, "Documents", "SiYuan")
  167. }
  168. }
  169. var workspacePaths []string
  170. if !gulu.File.IsExist(workspaceConf) {
  171. WorkspaceDir = defaultWorkspaceDir
  172. if "" != workspaceArg {
  173. WorkspaceDir = workspaceArg
  174. }
  175. if !gulu.File.IsDir(WorkspaceDir) {
  176. log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
  177. WorkspaceDir = defaultWorkspaceDir
  178. }
  179. workspacePaths = append(workspacePaths, WorkspaceDir)
  180. } else {
  181. data, err := os.ReadFile(workspaceConf)
  182. if err = gulu.JSON.UnmarshalJSON(data, &workspacePaths); nil != err {
  183. log.Printf("unmarshal workspace conf [%s] failed: %s", workspaceConf, err)
  184. }
  185. tmp := workspacePaths[:0]
  186. for _, d := range workspacePaths {
  187. if gulu.File.IsDir(d) {
  188. tmp = append(tmp, d)
  189. }
  190. }
  191. workspacePaths = tmp
  192. if 0 < len(workspacePaths) {
  193. WorkspaceDir = workspacePaths[len(workspacePaths)-1]
  194. if "" != workspaceArg {
  195. WorkspaceDir = workspaceArg
  196. }
  197. if !gulu.File.IsDir(WorkspaceDir) {
  198. log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
  199. WorkspaceDir = defaultWorkspaceDir
  200. }
  201. workspacePaths[len(workspacePaths)-1] = WorkspaceDir
  202. } else {
  203. WorkspaceDir = defaultWorkspaceDir
  204. if "" != workspaceArg {
  205. WorkspaceDir = workspaceArg
  206. }
  207. if !gulu.File.IsDir(WorkspaceDir) {
  208. log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
  209. WorkspaceDir = defaultWorkspaceDir
  210. }
  211. workspacePaths = append(workspacePaths, WorkspaceDir)
  212. }
  213. }
  214. if data, err := gulu.JSON.MarshalJSON(workspacePaths); nil == err {
  215. if err = os.WriteFile(workspaceConf, data, 0644); nil != err {
  216. log.Fatalf("write workspace conf [%s] failed: %s", workspaceConf, err)
  217. }
  218. } else {
  219. log.Fatalf("marshal workspace conf [%s] failed: %s", workspaceConf, err)
  220. }
  221. ConfDir = filepath.Join(WorkspaceDir, "conf")
  222. DataDir = filepath.Join(WorkspaceDir, "data")
  223. RepoDir = filepath.Join(WorkspaceDir, "repo")
  224. HistoryDir = filepath.Join(WorkspaceDir, "history")
  225. TempDir = filepath.Join(WorkspaceDir, "temp")
  226. osTmpDir := filepath.Join(TempDir, "os")
  227. os.RemoveAll(osTmpDir)
  228. if err := os.MkdirAll(osTmpDir, 0755); nil != err {
  229. log.Fatalf("create os tmp dir [%s] failed: %s", osTmpDir, err)
  230. }
  231. os.RemoveAll(filepath.Join(TempDir, "repo"))
  232. os.Setenv("TMPDIR", osTmpDir)
  233. os.Setenv("TEMP", osTmpDir)
  234. os.Setenv("TMP", osTmpDir)
  235. DBPath = filepath.Join(TempDir, DBName)
  236. HistoryDBPath = filepath.Join(TempDir, "history.db")
  237. BlockTreePath = filepath.Join(TempDir, "blocktree.msgpack")
  238. }
  239. var (
  240. Resident bool
  241. ReadOnly bool
  242. AccessAuthCode string
  243. Lang = ""
  244. Container string // docker, android, ios, std
  245. ISMicrosoftStore bool // 桌面端是否是微软商店版
  246. )
  247. const (
  248. ContainerStd = "std" // 桌面端
  249. ContainerDocker = "docker" // Docker 容器端
  250. ContainerAndroid = "android" // Android 端
  251. ContainerIOS = "ios" // iOS 端
  252. )
  253. func initPathDir() {
  254. if err := os.MkdirAll(ConfDir, 0755); nil != err && !os.IsExist(err) {
  255. log.Fatalf("create conf folder [%s] failed: %s", ConfDir, err)
  256. }
  257. if err := os.MkdirAll(DataDir, 0755); nil != err && !os.IsExist(err) {
  258. log.Fatalf("create data folder [%s] failed: %s", DataDir, err)
  259. }
  260. if err := os.MkdirAll(TempDir, 0755); nil != err && !os.IsExist(err) {
  261. log.Fatalf("create temp folder [%s] failed: %s", TempDir, err)
  262. }
  263. assets := filepath.Join(DataDir, "assets")
  264. if err := os.MkdirAll(assets, 0755); nil != err && !os.IsExist(err) {
  265. log.Fatalf("create data assets folder [%s] failed: %s", assets, err)
  266. }
  267. templates := filepath.Join(DataDir, "templates")
  268. if err := os.MkdirAll(templates, 0755); nil != err && !os.IsExist(err) {
  269. log.Fatalf("create data templates folder [%s] failed: %s", templates, err)
  270. }
  271. widgets := filepath.Join(DataDir, "widgets")
  272. if err := os.MkdirAll(widgets, 0755); nil != err && !os.IsExist(err) {
  273. log.Fatalf("create data widgets folder [%s] failed: %s", widgets, err)
  274. }
  275. emojis := filepath.Join(DataDir, "emojis")
  276. if err := os.MkdirAll(emojis, 0755); nil != err && !os.IsExist(err) {
  277. log.Fatalf("create data emojis folder [%s] failed: %s", widgets, err)
  278. }
  279. }
  280. func checkPort() {
  281. portOpened := isPortOpen(ServerPort)
  282. if !portOpened {
  283. return
  284. }
  285. logging.LogInfof("port [%s] is opened, try to check version of running kernel", ServerPort)
  286. result := NewResult()
  287. _, err := httpclient.NewBrowserRequest().
  288. SetResult(result).
  289. SetHeader("User-Agent", UserAgent).
  290. Get("http://127.0.0.1:" + ServerPort + "/api/system/version")
  291. if nil != err || 0 != result.Code {
  292. logging.LogErrorf("connect to port [%s] for checking running kernel failed", ServerPort)
  293. KillByPort(ServerPort)
  294. return
  295. }
  296. if nil == result.Data {
  297. logging.LogErrorf("connect ot port [%s] for checking running kernel failed", ServerPort)
  298. os.Exit(ExitCodeUnavailablePort)
  299. }
  300. runningVer := result.Data.(string)
  301. if runningVer == Ver {
  302. logging.LogInfof("version of the running kernel is the same as this boot [%s], exit this boot", runningVer)
  303. os.Exit(ExitCodeOk)
  304. }
  305. logging.LogInfof("found kernel [%s] is running, try to exit it", runningVer)
  306. processes, err := goPS.Processes()
  307. if nil != err {
  308. logging.LogErrorf("close kernel [%s] failed: %s", runningVer, err)
  309. os.Exit(ExitCodeUnavailablePort)
  310. }
  311. currentPid := os.Getpid()
  312. for _, p := range processes {
  313. name := p.Executable()
  314. if strings.Contains(strings.ToLower(name), "siyuan-kernel") || strings.Contains(strings.ToLower(name), "siyuan kernel") {
  315. kernelPid := p.Pid()
  316. if currentPid != kernelPid {
  317. pid := strconv.Itoa(kernelPid)
  318. Kill(pid)
  319. logging.LogInfof("killed kernel [name=%s, pid=%s, ver=%s], continue to boot", name, pid, runningVer)
  320. }
  321. }
  322. }
  323. if !tryToListenPort() {
  324. os.Exit(ExitCodeUnavailablePort)
  325. }
  326. }
  327. func initMime() {
  328. // 在某版本的 Windows 10 操作系统上界面样式异常问题
  329. // https://github.com/siyuan-note/siyuan/issues/247
  330. // https://github.com/siyuan-note/siyuan/issues/3813
  331. mime.AddExtensionType(".css", "text/css")
  332. mime.AddExtensionType(".js", "application/x-javascript")
  333. mime.AddExtensionType(".json", "application/json")
  334. mime.AddExtensionType(".html", "text/html")
  335. }
  336. func KillByPort(port string) {
  337. if pid := PidByPort(port); "" != pid {
  338. pidInt, _ := strconv.Atoi(pid)
  339. proc, _ := goPS.FindProcess(pidInt)
  340. var name string
  341. if nil != proc {
  342. name = proc.Executable()
  343. }
  344. Kill(pid)
  345. logging.LogInfof("killed process [name=%s, pid=%s]", name, pid)
  346. }
  347. }
  348. func Kill(pid string) {
  349. var kill *exec.Cmd
  350. if gulu.OS.IsWindows() {
  351. kill = exec.Command("cmd", "/c", "TASKKILL /F /PID "+pid)
  352. } else {
  353. kill = exec.Command("kill", "-9", pid)
  354. }
  355. gulu.CmdAttr(kill)
  356. kill.CombinedOutput()
  357. }
  358. func PidByPort(port string) (ret string) {
  359. if gulu.OS.IsWindows() {
  360. cmd := exec.Command("cmd", "/c", "netstat -ano | findstr "+port)
  361. gulu.CmdAttr(cmd)
  362. data, err := cmd.CombinedOutput()
  363. if nil != err {
  364. logging.LogErrorf("netstat failed: %s", err)
  365. return
  366. }
  367. output := string(data)
  368. lines := strings.Split(output, "\n")
  369. for _, l := range lines {
  370. if strings.Contains(l, "LISTENING") {
  371. l = l[strings.Index(l, "LISTENING")+len("LISTENING"):]
  372. l = strings.TrimSpace(l)
  373. ret = l
  374. return
  375. }
  376. }
  377. return
  378. }
  379. cmd := exec.Command("lsof", "-Fp", "-i", ":"+port)
  380. gulu.CmdAttr(cmd)
  381. data, err := cmd.CombinedOutput()
  382. if nil != err {
  383. logging.LogErrorf("lsof failed: %s", err)
  384. return
  385. }
  386. output := string(data)
  387. lines := strings.Split(output, "\n")
  388. for _, l := range lines {
  389. if strings.HasPrefix(l, "p") {
  390. l = l[1:]
  391. ret = l
  392. return
  393. }
  394. }
  395. return
  396. }
  397. func initPandoc() {
  398. if ContainerStd != Container {
  399. return
  400. }
  401. pandocDir := filepath.Join(TempDir, "pandoc")
  402. if gulu.OS.IsWindows() {
  403. PandocBinPath = filepath.Join(pandocDir, "bin", "pandoc.exe")
  404. } else if gulu.OS.IsDarwin() || gulu.OS.IsLinux() {
  405. PandocBinPath = filepath.Join(pandocDir, "bin", "pandoc")
  406. }
  407. pandocVer := getPandocVer(PandocBinPath)
  408. if "" != pandocVer {
  409. logging.LogInfof("built-in pandoc [ver=%s, bin=%s]", pandocVer, PandocBinPath)
  410. return
  411. }
  412. pandocZip := filepath.Join(WorkingDir, "pandoc.zip")
  413. if "dev" == Mode {
  414. if gulu.OS.IsWindows() {
  415. pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-windows-amd64.zip")
  416. } else if gulu.OS.IsDarwin() {
  417. pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-darwin-amd64.zip")
  418. } else if gulu.OS.IsLinux() {
  419. pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-linux-amd64.zip")
  420. }
  421. }
  422. if err := gulu.Zip.Unzip(pandocZip, pandocDir); nil != err {
  423. logging.LogErrorf("unzip pandoc failed: %s", err)
  424. return
  425. }
  426. if gulu.OS.IsDarwin() || gulu.OS.IsLinux() {
  427. exec.Command("chmod", "+x", PandocBinPath).CombinedOutput()
  428. }
  429. pandocVer = getPandocVer(PandocBinPath)
  430. logging.LogInfof("initialized built-in pandoc [ver=%s, bin=%s]", pandocVer, PandocBinPath)
  431. }
  432. func getPandocVer(binPath string) (ret string) {
  433. if "" == binPath {
  434. return
  435. }
  436. cmd := exec.Command(binPath, "--version")
  437. gulu.CmdAttr(cmd)
  438. data, err := cmd.CombinedOutput()
  439. if nil == err && strings.HasPrefix(string(data), "pandoc") {
  440. parts := bytes.Split(data, []byte("\n"))
  441. if 0 < len(parts) {
  442. ret = strings.TrimPrefix(string(parts[0]), "pandoc")
  443. ret = strings.ReplaceAll(ret, ".exe", "")
  444. ret = strings.TrimSpace(ret)
  445. }
  446. return
  447. }
  448. return
  449. }
  450. func IsValidPandocBin(binPath string) bool {
  451. if "" == binPath {
  452. return false
  453. }
  454. cmd := exec.Command(binPath, "--version")
  455. gulu.CmdAttr(cmd)
  456. data, err := cmd.CombinedOutput()
  457. if nil == err && strings.HasPrefix(string(data), "pandoc") {
  458. return true
  459. }
  460. return false
  461. }
  462. func GetDataAssetsAbsPath() (ret string) {
  463. ret = filepath.Join(DataDir, "assets")
  464. var err error
  465. stat, err := os.Lstat(ret)
  466. if nil != err {
  467. logging.LogErrorf("stat assets failed: %s", err)
  468. return
  469. }
  470. if 0 != stat.Mode()&os.ModeSymlink {
  471. // 跟随符号链接 https://github.com/siyuan-note/siyuan/issues/5480
  472. ret, err = os.Readlink(ret)
  473. if nil != err {
  474. logging.LogErrorf("read assets link failed: %s", err)
  475. }
  476. }
  477. return
  478. }