working.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  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. "flag"
  19. "log"
  20. "math/rand"
  21. "mime"
  22. "os"
  23. "os/exec"
  24. "path/filepath"
  25. "strconv"
  26. "strings"
  27. "sync"
  28. "time"
  29. "github.com/88250/gulu"
  30. figure "github.com/common-nighthawk/go-figure"
  31. goPS "github.com/mitchellh/go-ps"
  32. "github.com/siyuan-note/httpclient"
  33. "github.com/siyuan-note/logging"
  34. )
  35. // var Mode = "dev"
  36. var Mode = "prod"
  37. const (
  38. Ver = "2.1.8"
  39. IsInsider = false
  40. )
  41. var (
  42. bootProgress float64 // 启动进度,从 0 到 100
  43. bootDetails string // 启动细节描述
  44. HttpServing = false // 是否 HTTP 伺服已经可用
  45. )
  46. func Boot() {
  47. IncBootProgress(3, "Booting...")
  48. rand.Seed(time.Now().UTC().UnixNano())
  49. initMime()
  50. workspacePath := flag.String("workspace", "", "dir path of the workspace, default to ~/Documents/SiYuan/")
  51. wdPath := flag.String("wd", WorkingDir, "working directory of SiYuan")
  52. servePath := flag.String("servePath", "", "obsoleted https://github.com/siyuan-note/siyuan/issues/4647")
  53. _ = servePath
  54. resident := flag.Bool("resident", true, "resident memory even if no active session")
  55. readOnly := flag.Bool("readonly", false, "read-only mode")
  56. accessAuthCode := flag.String("accessAuthCode", "", "access auth code")
  57. ssl := flag.Bool("ssl", false, "for https and wss")
  58. lang := flag.String("lang", "", "zh_CN/zh_CHT/en_US/fr_FR/es_ES")
  59. mode := flag.String("mode", "prod", "dev/prod")
  60. flag.Parse()
  61. if "" != *wdPath {
  62. WorkingDir = *wdPath
  63. }
  64. if "" != *lang {
  65. Lang = *lang
  66. }
  67. Mode = *mode
  68. Resident = *resident
  69. ReadOnly = *readOnly
  70. AccessAuthCode = *accessAuthCode
  71. Container = "std"
  72. if isRunningInDockerContainer() {
  73. Container = "docker"
  74. }
  75. UserAgent = UserAgent + " " + Container
  76. httpclient.SetUserAgent(UserAgent)
  77. initWorkspaceDir(*workspacePath)
  78. SSL = *ssl
  79. LogPath = filepath.Join(TempDir, "siyuan.log")
  80. logging.SetLogPath(LogPath)
  81. AppearancePath = filepath.Join(ConfDir, "appearance")
  82. if "dev" == Mode {
  83. ThemesPath = filepath.Join(WorkingDir, "appearance", "themes")
  84. IconsPath = filepath.Join(WorkingDir, "appearance", "icons")
  85. } else {
  86. ThemesPath = filepath.Join(AppearancePath, "themes")
  87. IconsPath = filepath.Join(AppearancePath, "icons")
  88. }
  89. initPathDir()
  90. checkPort()
  91. bootBanner := figure.NewColorFigure("SiYuan", "isometric3", "green", true)
  92. logging.LogInfof("\n" + bootBanner.String())
  93. logBootInfo()
  94. go cleanOld()
  95. }
  96. func setBootDetails(details string) {
  97. bootDetails = "v" + Ver + " " + details
  98. }
  99. func SetBootDetails(details string) {
  100. if 100 <= bootProgress {
  101. return
  102. }
  103. setBootDetails(details)
  104. }
  105. func IncBootProgress(progress float64, details string) {
  106. if 100 <= bootProgress {
  107. return
  108. }
  109. bootProgress += progress
  110. setBootDetails(details)
  111. }
  112. func IsBooted() bool {
  113. return 100 <= bootProgress
  114. }
  115. func GetBootProgressDetails() (float64, string) {
  116. return bootProgress, bootDetails
  117. }
  118. func GetBootProgress() float64 {
  119. return bootProgress
  120. }
  121. func SetBooted() {
  122. setBootDetails("Finishing boot...")
  123. bootProgress = 100
  124. logging.LogInfof("kernel booted")
  125. }
  126. var (
  127. HomeDir, _ = gulu.OS.Home()
  128. WorkingDir, _ = os.Getwd()
  129. WorkspaceDir string // 工作空间目录路径
  130. ConfDir string // 配置目录路径
  131. DataDir string // 数据目录路径
  132. RepoDir string // 仓库目录路径
  133. HistoryDir string // 数据历史目录路径
  134. TempDir string // 临时目录路径
  135. LogPath string // 配置目录下的日志文件 siyuan.log 路径
  136. DBName = "siyuan.db" // SQLite 数据库文件名
  137. DBPath string // SQLite 数据库文件路径
  138. HistoryDBPath string // SQLite 历史数据库文件路径
  139. BlockTreePath string // 区块树文件路径
  140. AppearancePath string // 配置目录下的外观目录 appearance/ 路径
  141. ThemesPath string // 配置目录下的外观目录下的 themes/ 路径
  142. IconsPath string // 配置目录下的外观目录下的 icons/ 路径
  143. AndroidNativeLibDir string // Android 库路径
  144. AndroidPrivateDataDir string // Android 私有数据路径
  145. UIProcessIDs = sync.Map{} // UI 进程 ID
  146. IsNewbie bool // 是否是第一次安装
  147. )
  148. func initWorkspaceDir(workspaceArg string) {
  149. userHomeConfDir := filepath.Join(HomeDir, ".config", "siyuan")
  150. workspaceConf := filepath.Join(userHomeConfDir, "workspace.json")
  151. if !gulu.File.IsExist(workspaceConf) {
  152. IsNewbie = "std" == Container // 只有桌面端需要设置新手标识,前端自动挂载帮助文档
  153. if err := os.MkdirAll(userHomeConfDir, 0755); nil != err && !os.IsExist(err) {
  154. log.Printf("create user home conf folder [%s] failed: %s", userHomeConfDir, err)
  155. os.Exit(ExitCodeCreateConfDirErr)
  156. }
  157. }
  158. defaultWorkspaceDir := filepath.Join(HomeDir, "Documents", "SiYuan")
  159. if gulu.OS.IsWindows() {
  160. // 改进 Windows 端默认工作空间路径 https://github.com/siyuan-note/siyuan/issues/5622
  161. if userProfile := os.Getenv("USERPROFILE"); "" != userProfile {
  162. defaultWorkspaceDir = filepath.Join(userProfile, "Documents", "SiYuan")
  163. }
  164. }
  165. var workspacePaths []string
  166. if !gulu.File.IsExist(workspaceConf) {
  167. WorkspaceDir = defaultWorkspaceDir
  168. if "" != workspaceArg {
  169. WorkspaceDir = workspaceArg
  170. }
  171. if !gulu.File.IsDir(WorkspaceDir) {
  172. log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
  173. WorkspaceDir = defaultWorkspaceDir
  174. }
  175. workspacePaths = append(workspacePaths, WorkspaceDir)
  176. } else {
  177. data, err := os.ReadFile(workspaceConf)
  178. if err = gulu.JSON.UnmarshalJSON(data, &workspacePaths); nil != err {
  179. log.Printf("unmarshal workspace conf [%s] failed: %s", workspaceConf, err)
  180. }
  181. tmp := workspacePaths[:0]
  182. for _, d := range workspacePaths {
  183. if gulu.File.IsDir(d) {
  184. tmp = append(tmp, d)
  185. }
  186. }
  187. workspacePaths = tmp
  188. if 0 < len(workspacePaths) {
  189. WorkspaceDir = workspacePaths[len(workspacePaths)-1]
  190. if "" != workspaceArg {
  191. WorkspaceDir = workspaceArg
  192. }
  193. if !gulu.File.IsDir(WorkspaceDir) {
  194. log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
  195. WorkspaceDir = defaultWorkspaceDir
  196. }
  197. workspacePaths[len(workspacePaths)-1] = WorkspaceDir
  198. } else {
  199. WorkspaceDir = defaultWorkspaceDir
  200. if "" != workspaceArg {
  201. WorkspaceDir = workspaceArg
  202. }
  203. if !gulu.File.IsDir(WorkspaceDir) {
  204. log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
  205. WorkspaceDir = defaultWorkspaceDir
  206. }
  207. workspacePaths = append(workspacePaths, WorkspaceDir)
  208. }
  209. }
  210. if data, err := gulu.JSON.MarshalJSON(workspacePaths); nil == err {
  211. if err = os.WriteFile(workspaceConf, data, 0644); nil != err {
  212. log.Fatalf("write workspace conf [%s] failed: %s", workspaceConf, err)
  213. }
  214. } else {
  215. log.Fatalf("marshal workspace conf [%s] failed: %s", workspaceConf, err)
  216. }
  217. ConfDir = filepath.Join(WorkspaceDir, "conf")
  218. DataDir = filepath.Join(WorkspaceDir, "data")
  219. RepoDir = filepath.Join(WorkspaceDir, "repo")
  220. HistoryDir = filepath.Join(WorkspaceDir, "history")
  221. TempDir = filepath.Join(WorkspaceDir, "temp")
  222. osTmpDir := filepath.Join(TempDir, "os")
  223. os.RemoveAll(osTmpDir)
  224. if err := os.MkdirAll(osTmpDir, 0755); nil != err {
  225. log.Fatalf("create os tmp dir [%s] failed: %s", osTmpDir, err)
  226. }
  227. os.RemoveAll(filepath.Join(TempDir, "repo"))
  228. os.Setenv("TMPDIR", osTmpDir)
  229. os.Setenv("TEMP", osTmpDir)
  230. os.Setenv("TMP", osTmpDir)
  231. DBPath = filepath.Join(TempDir, DBName)
  232. HistoryDBPath = filepath.Join(TempDir, "history.db")
  233. BlockTreePath = filepath.Join(TempDir, "blocktree.msgpack")
  234. }
  235. var (
  236. Resident bool
  237. ReadOnly bool
  238. AccessAuthCode string
  239. Lang = ""
  240. Container string // docker, android, ios, std
  241. )
  242. func initPathDir() {
  243. if err := os.MkdirAll(ConfDir, 0755); nil != err && !os.IsExist(err) {
  244. log.Fatalf("create conf folder [%s] failed: %s", ConfDir, err)
  245. }
  246. if err := os.MkdirAll(DataDir, 0755); nil != err && !os.IsExist(err) {
  247. log.Fatalf("create data folder [%s] failed: %s", DataDir, err)
  248. }
  249. if err := os.MkdirAll(TempDir, 0755); nil != err && !os.IsExist(err) {
  250. log.Fatalf("create temp folder [%s] failed: %s", TempDir, err)
  251. }
  252. assets := filepath.Join(DataDir, "assets")
  253. if err := os.MkdirAll(assets, 0755); nil != err && !os.IsExist(err) {
  254. log.Fatalf("create data assets folder [%s] failed: %s", assets, err)
  255. }
  256. templates := filepath.Join(DataDir, "templates")
  257. if err := os.MkdirAll(templates, 0755); nil != err && !os.IsExist(err) {
  258. log.Fatalf("create data templates folder [%s] failed: %s", templates, err)
  259. }
  260. widgets := filepath.Join(DataDir, "widgets")
  261. if err := os.MkdirAll(widgets, 0755); nil != err && !os.IsExist(err) {
  262. log.Fatalf("create data widgets folder [%s] failed: %s", widgets, err)
  263. }
  264. emojis := filepath.Join(DataDir, "emojis")
  265. if err := os.MkdirAll(emojis, 0755); nil != err && !os.IsExist(err) {
  266. log.Fatalf("create data emojis folder [%s] failed: %s", widgets, err)
  267. }
  268. }
  269. // TODO: v2.2.0 移除
  270. func cleanOld() {
  271. dirs, _ := os.ReadDir(WorkingDir)
  272. for _, dir := range dirs {
  273. if strings.HasSuffix(dir.Name(), ".old") {
  274. old := filepath.Join(WorkingDir, dir.Name())
  275. os.RemoveAll(old)
  276. }
  277. }
  278. }
  279. func checkPort() {
  280. portOpened := isPortOpen(ServerPort)
  281. if !portOpened {
  282. return
  283. }
  284. logging.LogInfof("port [%s] is opened, try to check version of running kernel", ServerPort)
  285. result := NewResult()
  286. _, err := httpclient.NewBrowserRequest().
  287. SetResult(result).
  288. SetHeader("User-Agent", UserAgent).
  289. Get("http://127.0.0.1:" + ServerPort + "/api/system/version")
  290. if nil != err || 0 != result.Code {
  291. logging.LogErrorf("connect to port [%s] for checking running kernel failed", ServerPort)
  292. KillByPort(ServerPort)
  293. return
  294. }
  295. if nil == result.Data {
  296. logging.LogErrorf("connect ot port [%s] for checking running kernel failed", ServerPort)
  297. os.Exit(ExitCodeUnavailablePort)
  298. }
  299. runningVer := result.Data.(string)
  300. if runningVer == Ver {
  301. logging.LogInfof("version of the running kernel is the same as this boot [%s], exit this boot", runningVer)
  302. os.Exit(ExitCodeOk)
  303. }
  304. logging.LogInfof("found kernel [%s] is running, try to exit it", runningVer)
  305. processes, err := goPS.Processes()
  306. if nil != err {
  307. logging.LogErrorf("close kernel [%s] failed: %s", runningVer, err)
  308. os.Exit(ExitCodeUnavailablePort)
  309. }
  310. currentPid := os.Getpid()
  311. for _, p := range processes {
  312. name := p.Executable()
  313. if strings.Contains(strings.ToLower(name), "siyuan-kernel") || strings.Contains(strings.ToLower(name), "siyuan kernel") {
  314. kernelPid := p.Pid()
  315. if currentPid != kernelPid {
  316. pid := strconv.Itoa(kernelPid)
  317. Kill(pid)
  318. logging.LogInfof("killed kernel [name=%s, pid=%s, ver=%s], continue to boot", name, pid, runningVer)
  319. }
  320. }
  321. }
  322. if !tryToListenPort() {
  323. os.Exit(ExitCodeUnavailablePort)
  324. }
  325. }
  326. func initMime() {
  327. // 在某版本的 Windows 10 操作系统上界面样式异常问题
  328. // https://github.com/siyuan-note/siyuan/issues/247
  329. // https://github.com/siyuan-note/siyuan/issues/3813
  330. mime.AddExtensionType(".css", "text/css")
  331. mime.AddExtensionType(".js", "application/x-javascript")
  332. mime.AddExtensionType(".json", "application/json")
  333. mime.AddExtensionType(".html", "text/html")
  334. }
  335. func KillByPort(port string) {
  336. if pid := PidByPort(port); "" != pid {
  337. pidInt, _ := strconv.Atoi(pid)
  338. proc, _ := goPS.FindProcess(pidInt)
  339. var name string
  340. if nil != proc {
  341. name = proc.Executable()
  342. }
  343. Kill(pid)
  344. logging.LogInfof("killed process [name=%s, pid=%s]", name, pid)
  345. }
  346. }
  347. func Kill(pid string) {
  348. var kill *exec.Cmd
  349. if gulu.OS.IsWindows() {
  350. kill = exec.Command("cmd", "/c", "TASKKILL /F /PID "+pid)
  351. } else {
  352. kill = exec.Command("kill", "-9", pid)
  353. }
  354. CmdAttr(kill)
  355. kill.CombinedOutput()
  356. }
  357. func PidByPort(port string) (ret string) {
  358. if gulu.OS.IsWindows() {
  359. cmd := exec.Command("cmd", "/c", "netstat -ano | findstr "+port)
  360. CmdAttr(cmd)
  361. data, err := cmd.CombinedOutput()
  362. if nil != err {
  363. logging.LogErrorf("netstat failed: %s", err)
  364. return
  365. }
  366. output := string(data)
  367. lines := strings.Split(output, "\n")
  368. for _, l := range lines {
  369. if strings.Contains(l, "LISTENING") {
  370. l = l[strings.Index(l, "LISTENING")+len("LISTENING"):]
  371. l = strings.TrimSpace(l)
  372. ret = l
  373. return
  374. }
  375. }
  376. return
  377. }
  378. cmd := exec.Command("lsof", "-Fp", "-i", ":"+port)
  379. CmdAttr(cmd)
  380. data, err := cmd.CombinedOutput()
  381. if nil != err {
  382. logging.LogErrorf("lsof failed: %s", err)
  383. return
  384. }
  385. output := string(data)
  386. lines := strings.Split(output, "\n")
  387. for _, l := range lines {
  388. if strings.HasPrefix(l, "p") {
  389. l = l[1:]
  390. ret = l
  391. return
  392. }
  393. }
  394. return
  395. }