From 3b76c6792c95cc38ef8ed2ffc01b778ed0295ba2 Mon Sep 17 00:00:00 2001 From: Lincoln Nogueira Date: Mon, 8 May 2023 21:16:38 -0300 Subject: [PATCH] feat: add preliminar Windows support (#1636) Add preliminar Windows support for both development and production environments. Default profile.Data will be set to "C:\ProgramData\memos" on Windows. Folder will be created if it does not exist, as this behavior is expected for Windows applications. System service installation can be achieved with third-party tools, explained in docs/windows-service.md. Not sure if it's worth using https://github.com/kardianos/service to make service support built-in. This could be a nice addition alongside #1583 (add Windows artifacts) --- docs/development-windows.md | 90 ++++++++++++++++++++++++++++++++++ docs/windows-service.md | 98 +++++++++++++++++++++++++++++++++++++ scripts/.air-windows.toml | 15 ++++++ scripts/build.ps1 | 36 ++++++++++++++ scripts/start.ps1 | 42 ++++++++++++++++ server/profile/profile.go | 24 +++++++-- 6 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 docs/development-windows.md create mode 100644 docs/windows-service.md create mode 100644 scripts/.air-windows.toml create mode 100644 scripts/build.ps1 create mode 100644 scripts/start.ps1 diff --git a/docs/development-windows.md b/docs/development-windows.md new file mode 100644 index 000000000..5c4096cd9 --- /dev/null +++ b/docs/development-windows.md @@ -0,0 +1,90 @@ +# Development + +Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code: + +1. It has no external dependency. +2. It requires zero config. +3. 1 command to start backend and 1 command to start frontend, both with live reload support. + +## Tech Stack + +| Frontend | Backend | +| ---------------------------------------- | --------------------------------- | +| [React](https://react.dev/) | [Go](https://go.dev/) | +| [Tailwind CSS](https://tailwindcss.com/) | [SQLite](https://www.sqlite.org/) | +| [Vite](https://vitejs.dev/) | | +| [pnpm](https://pnpm.io/) | | + +## Prerequisites + +- [Go](https://golang.org/doc/install) +- [Air](https://github.com/cosmtrek/air#installation) for backend live reload +- [Node.js](https://nodejs.org/) +- [pnpm](https://pnpm.io/installation) + +## Steps + +(Using PowerShell) + +1. pull source code + + ```powershell + git clone https://github.com/usememos/memos + # or + gh repo clone usememos/memos + ``` + +2. cd into the project root directory + + ```powershell + cd memos + ``` + +3. start backend using air (with live reload) + + ```powershell + air -c .\scripts\.air-windows.toml + ``` + +4. start frontend dev server + + ```powershell + cd web; pnpm i; pnpm dev + ``` + +Memos should now be running at [http://localhost:3001](http://localhost:3001) and changing either frontend or backend code would trigger live reload. + +## Building + +Frontend must be built before backend. The built frontend must be placed in the backend ./server/dist directory. Otherwise, you will get a "No frontend embeded" error. + +### Frontend + +```powershell +Move-Item "./server/dist" "./server/dist.bak" +cd web; pnpm i --frozen-lockfile; pnpm build; cd ..; +Move-Item "./web/dist" "./server/" -Force +``` + +### Backend + +```powershell +go build -o ./build/memos.exe ./main.go +``` + +## ❕ Notes + +- Start development servers easier by running the provided `start.ps1` script. +This will start both backend and frontend in detached PowerShell windows: + + ```powershell + .\scripts\start.ps1 + ``` + +- Produce a local build easier using the provided `build.ps1` script to build both frontend and backend: + + ```powershell + .\scripts\build.ps1 + ``` + + This will produce a memos.exe file in the ./build directory. diff --git a/docs/windows-service.md b/docs/windows-service.md new file mode 100644 index 000000000..831cf6143 --- /dev/null +++ b/docs/windows-service.md @@ -0,0 +1,98 @@ +# Installing memos as a service on Windows + +While memos first-class support is for Docker, you may also install memos as a Windows service. It will run under SYSTEM account and start automatically at system boot. + +❗ All service management methods requires admin privileges. Use [gsudo](https://gerardog.github.io/gsudo/docs/install), or open a new PowerShell terminal as admin: + +```powershell +Start-Process powershell -Verb RunAs +``` + +## Choose one of the following methods + +### 1. Using [NSSM](https://nssm.cc/download) + +NSSM is a lightweight service wrapper. + +You may put `nssm.exe` in the same directory as `memos.exe`, or add its directory to your system PATH. Prefer the latest 64-bit version of `nssm.exe`. + +```powershell +# Install memos as a service +nssm install memos "C:\path\to\memos.exe" --mode prod --port 5230 + +# Delay auto start +nssm set memos DisplayName "memos service" + +# Configure extra service parameters +nssm set memos Description "A lightweight, self-hosted memo hub. https://usememos.com/" + +# Delay auto start +nssm set memos Start SERVICE_DELAYED_AUTO_START + +# Edit service using NSSM GUI +nssm edit memos + +# Start the service +nssm start memos + +# Remove the service, if ever needed +nssm remove memos confirm +``` + +### 2. Using [WinSW](https://github.com/winsw/winsw) + +Find the latest release tag and download the asset `WinSW-net46x.exe`. Then, put it in the same directory as `memos.exe` and rename it to `memos-service.exe`. + +Now, in the same directory, create the service configuration file `memos-service.xml`: + +```xml + + memos + memos service + A lightweight, self-hosted memo hub. https://usememos.com/ + + %BASE%\memos.exe + --mode prod --port 5230 + true + + +``` + +Then, install the service: + +```powershell +# Install the service +.\memos-service.exe install + +# Start the service +.\memos-service.exe start + +# Remove the service, if ever needed +.\memos-service.exe uninstall +``` + +### Manage the service + +You may use the `net` command to manage the service: + +```powershell +net start memos +net stop memos +``` + +Also, by using one of the provided methods, the service will appear in the Windows Services Manager `services.msc`. + +## Notes + +- On Windows, memos store its data in the following directory: + + ```powershell + $env:ProgramData\memos + # Typically, this will resolve to C:\ProgramData\memos + ``` + + You may specify a custom directory by appending `--data ` to the service command line. + +- If the service fails to start, you should inspect the Windows Event Viewer `eventvwr.msc`. + +- Memos will be accessible at [http://localhost:5230](http://localhost:5230) by default. diff --git a/scripts/.air-windows.toml b/scripts/.air-windows.toml new file mode 100644 index 000000000..a8346c9c4 --- /dev/null +++ b/scripts/.air-windows.toml @@ -0,0 +1,15 @@ +root = "." +tmp_dir = ".air" + +[build] + bin = "./.air/memos.exe" + cmd = "go build -o ./.air/memos.exe ./main.go" + delay = 1000 + exclude_dir = [".air", "web", "build"] + exclude_file = [] + exclude_regex = [] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + send_interrupt = true + kill_delay = 2000 diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 000000000..e01688e84 --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,36 @@ +# Usage: ./scripts/build.ps1 +# This is only for local builds. + +# For development, setup a proper environment as described here: +# https://github.com/usememos/memos/blob/main/docs/development.md + +$projectRoot = (Resolve-Path "$MyInvocation.MyCommand.Path/..").Path +Write-Host "Project root: $projectRoot" + +Write-Host "Building frontend..." -f Magenta +Set-Location "$projectRoot/web" +npm install -g pnpm +pnpm i --frozen-lockfile +pnpm build + +Write-Host "Backing up frontend placeholder..." -f Magenta +Move-Item "$projectRoot/server/dist" "$projectRoot/server/dist.bak" -Force -ErrorAction Stop + +Write-Host "Moving frontend build to /server/dist ..." -f Magenta +Move-Item "$projectRoot/web/dist" "$projectRoot/server/" -Force -ErrorAction Stop + +Set-Location $projectRoot + +Write-Host "Building backend..." -f Magenta +go build -o ./build/memos.exe ./main.go +Write-Host "Backend built!" -f green + +Write-Host "Removing frontend from /server/dist ..." -f Magenta +Remove-Item "$projectRoot/server/dist" -Recurse -Force -ErrorAction SilentlyContinue + +Write-Host "Restoring frontend placeholder..." -f Magenta +Move-Item "$projectRoot/server/dist.bak" "$projectRoot/server/dist" -Force -ErrorAction Stop + +Write-Host "You can test the build with ./build/memos.exe --mode demo" -f Green + +Set-Location -Path $projectRoot \ No newline at end of file diff --git a/scripts/start.ps1 b/scripts/start.ps1 new file mode 100644 index 000000000..fa2dfabc0 --- /dev/null +++ b/scripts/start.ps1 @@ -0,0 +1,42 @@ +# This script starts the backend and frontend in development mode, with live reload. +# It also installs frontend dependencies. + +# For more details on setting-up a development environment, check the docs: +# https://github.com/usememos/memos/blob/main/docs/development.md + +# Usage: ./scripts/start.ps1 +$LastExitCode = 0 + +$projectRoot = (Resolve-Path "$MyInvocation.MyCommand.Path/..").Path +Write-Host "Project root: $projectRoot" + +Write-Host "Starting backend..." -f Magenta +Start-Process -WorkingDirectory "$projectRoot" -FilePath "air" "-c ./scripts/.air-windows.toml" +if ($LastExitCode -ne 0) { + Write-Host "Failed to start backend!" -f Red + exit $LastExitCode +} +else { + Write-Host "Backend started!" -f Green +} + +Write-Host "Installing frontend dependencies..." -f Magenta +Start-Process -Wait -WorkingDirectory "$projectRoot/web" -FilePath "powershell" -ArgumentList "pnpm i" +if ($LastExitCode -ne 0) { + Write-Host "Failed to install frontend dependencies!" -f Red + exit $LastExitCode +} +else { + Write-Host "Frontend dependencies installed!" -f Green +} + +Write-Host "Starting frontend..." -f Magenta +Start-Process -WorkingDirectory "$projectRoot/web" -FilePath "powershell" -ArgumentList "pnpm dev" +if ($LastExitCode -ne 0) { + Write-Host "Failed to start frontend!" -f Red + exit $LastExitCode +} +else { + Write-Host "Frontend started!" -f Green +} + diff --git a/server/profile/profile.go b/server/profile/profile.go index 1d0541b61..1c0df4398 100644 --- a/server/profile/profile.go +++ b/server/profile/profile.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "github.com/spf13/viper" @@ -31,15 +32,16 @@ func (p *Profile) IsDev() bool { func checkDSN(dataDir string) (string, error) { // Convert to absolute path if relative path is supplied. if !filepath.IsAbs(dataDir) { - absDir, err := filepath.Abs(filepath.Dir(os.Args[0]) + "/" + dataDir) + relativeDir := filepath.Join(filepath.Dir(os.Args[0]), dataDir) + absDir, err := filepath.Abs(relativeDir) if err != nil { return "", err } dataDir = absDir } - // Trim trailing / in case user supplies - dataDir = strings.TrimRight(dataDir, "/") + // Trim trailing \ or / in case user supplies + dataDir = strings.TrimRight(dataDir, "\\/") if _, err := os.Stat(dataDir); err != nil { return "", fmt.Errorf("unable to access data folder %s, err %w", dataDir, err) @@ -61,7 +63,18 @@ func GetProfile() (*Profile, error) { } if profile.Mode == "prod" && profile.Data == "" { - profile.Data = "/var/opt/memos" + if runtime.GOOS == "windows" { + profile.Data = filepath.Join(os.Getenv("ProgramData"), "memos") + + if _, err := os.Stat(profile.Data); os.IsNotExist(err) { + if err := os.MkdirAll(profile.Data, 0770); err != nil { + fmt.Printf("Failed to create data directory: %s, err: %+v\n", profile.Data, err) + return nil, err + } + } + } else { + profile.Data = "/var/opt/memos" + } } dataDir, err := checkDSN(profile.Data) @@ -71,7 +84,8 @@ func GetProfile() (*Profile, error) { } profile.Data = dataDir - profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode) + dbFile := fmt.Sprintf("memos_%s.db", profile.Mode) + profile.DSN = filepath.Join(dataDir, dbFile) profile.Version = version.GetCurrentVersion(profile.Mode) return &profile, nil