working.go 12 KB

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