Skip to content

Commit 3b76c67

Browse files
feat: add preliminar Windows support (usememos#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 usememos#1583 (add Windows artifacts)
1 parent 4605349 commit 3b76c67

File tree

6 files changed

+300
-5
lines changed

6 files changed

+300
-5
lines changed

docs/development-windows.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Development
2+
3+
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
4+
5+
1. It has no external dependency.
6+
2. It requires zero config.
7+
3. 1 command to start backend and 1 command to start frontend, both with live reload support.
8+
9+
## Tech Stack
10+
11+
| Frontend | Backend |
12+
| ---------------------------------------- | --------------------------------- |
13+
| [React](https://react.dev/) | [Go](https://go.dev/) |
14+
| [Tailwind CSS](https://tailwindcss.com/) | [SQLite](https://www.sqlite.org/) |
15+
| [Vite](https://vitejs.dev/) | |
16+
| [pnpm](https://pnpm.io/) | |
17+
18+
## Prerequisites
19+
20+
- [Go](https://golang.org/doc/install)
21+
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
22+
- [Node.js](https://nodejs.org/)
23+
- [pnpm](https://pnpm.io/installation)
24+
25+
## Steps
26+
27+
(Using PowerShell)
28+
29+
1. pull source code
30+
31+
```powershell
32+
git clone https://github.com/usememos/memos
33+
# or
34+
gh repo clone usememos/memos
35+
```
36+
37+
2. cd into the project root directory
38+
39+
```powershell
40+
cd memos
41+
```
42+
43+
3. start backend using air (with live reload)
44+
45+
```powershell
46+
air -c .\scripts\.air-windows.toml
47+
```
48+
49+
4. start frontend dev server
50+
51+
```powershell
52+
cd web; pnpm i; pnpm dev
53+
```
54+
55+
Memos should now be running at [http://localhost:3001](http://localhost:3001) and changing either frontend or backend code would trigger live reload.
56+
57+
## Building
58+
59+
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.
60+
61+
### Frontend
62+
63+
```powershell
64+
Move-Item "./server/dist" "./server/dist.bak"
65+
cd web; pnpm i --frozen-lockfile; pnpm build; cd ..;
66+
Move-Item "./web/dist" "./server/" -Force
67+
```
68+
69+
### Backend
70+
71+
```powershell
72+
go build -o ./build/memos.exe ./main.go
73+
```
74+
75+
## ❕ Notes
76+
77+
- Start development servers easier by running the provided `start.ps1` script.
78+
This will start both backend and frontend in detached PowerShell windows:
79+
80+
```powershell
81+
.\scripts\start.ps1
82+
```
83+
84+
- Produce a local build easier using the provided `build.ps1` script to build both frontend and backend:
85+
86+
```powershell
87+
.\scripts\build.ps1
88+
```
89+
90+
This will produce a memos.exe file in the ./build directory.

docs/windows-service.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Installing memos as a service on Windows
2+
3+
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.
4+
5+
❗ All service management methods requires admin privileges. Use [gsudo](https://gerardog.github.io/gsudo/docs/install), or open a new PowerShell terminal as admin:
6+
7+
```powershell
8+
Start-Process powershell -Verb RunAs
9+
```
10+
11+
## Choose one of the following methods
12+
13+
### 1. Using [NSSM](https://nssm.cc/download)
14+
15+
NSSM is a lightweight service wrapper.
16+
17+
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`.
18+
19+
```powershell
20+
# Install memos as a service
21+
nssm install memos "C:\path\to\memos.exe" --mode prod --port 5230
22+
23+
# Delay auto start
24+
nssm set memos DisplayName "memos service"
25+
26+
# Configure extra service parameters
27+
nssm set memos Description "A lightweight, self-hosted memo hub. https://usememos.com/"
28+
29+
# Delay auto start
30+
nssm set memos Start SERVICE_DELAYED_AUTO_START
31+
32+
# Edit service using NSSM GUI
33+
nssm edit memos
34+
35+
# Start the service
36+
nssm start memos
37+
38+
# Remove the service, if ever needed
39+
nssm remove memos confirm
40+
```
41+
42+
### 2. Using [WinSW](https://github.com/winsw/winsw)
43+
44+
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`.
45+
46+
Now, in the same directory, create the service configuration file `memos-service.xml`:
47+
48+
```xml
49+
<service>
50+
<id>memos</id>
51+
<name>memos service</name>
52+
<description>A lightweight, self-hosted memo hub. https://usememos.com/</description>
53+
<onfailure action="restart" delay="10 sec"/>
54+
<executable>%BASE%\memos.exe</executable>
55+
<arguments>--mode prod --port 5230</arguments>
56+
<delayedAutoStart>true</delayedAutoStart>
57+
<log mode="none" />
58+
</service>
59+
```
60+
61+
Then, install the service:
62+
63+
```powershell
64+
# Install the service
65+
.\memos-service.exe install
66+
67+
# Start the service
68+
.\memos-service.exe start
69+
70+
# Remove the service, if ever needed
71+
.\memos-service.exe uninstall
72+
```
73+
74+
### Manage the service
75+
76+
You may use the `net` command to manage the service:
77+
78+
```powershell
79+
net start memos
80+
net stop memos
81+
```
82+
83+
Also, by using one of the provided methods, the service will appear in the Windows Services Manager `services.msc`.
84+
85+
## Notes
86+
87+
- On Windows, memos store its data in the following directory:
88+
89+
```powershell
90+
$env:ProgramData\memos
91+
# Typically, this will resolve to C:\ProgramData\memos
92+
```
93+
94+
You may specify a custom directory by appending `--data <path>` to the service command line.
95+
96+
- If the service fails to start, you should inspect the Windows Event Viewer `eventvwr.msc`.
97+
98+
- Memos will be accessible at [http://localhost:5230](http://localhost:5230) by default.

scripts/.air-windows.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = "."
2+
tmp_dir = ".air"
3+
4+
[build]
5+
bin = "./.air/memos.exe"
6+
cmd = "go build -o ./.air/memos.exe ./main.go"
7+
delay = 1000
8+
exclude_dir = [".air", "web", "build"]
9+
exclude_file = []
10+
exclude_regex = []
11+
exclude_unchanged = false
12+
follow_symlink = false
13+
full_bin = ""
14+
send_interrupt = true
15+
kill_delay = 2000

scripts/build.ps1

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Usage: ./scripts/build.ps1
2+
# This is only for local builds.
3+
4+
# For development, setup a proper environment as described here:
5+
# https://github.com/usememos/memos/blob/main/docs/development.md
6+
7+
$projectRoot = (Resolve-Path "$MyInvocation.MyCommand.Path/..").Path
8+
Write-Host "Project root: $projectRoot"
9+
10+
Write-Host "Building frontend..." -f Magenta
11+
Set-Location "$projectRoot/web"
12+
npm install -g pnpm
13+
pnpm i --frozen-lockfile
14+
pnpm build
15+
16+
Write-Host "Backing up frontend placeholder..." -f Magenta
17+
Move-Item "$projectRoot/server/dist" "$projectRoot/server/dist.bak" -Force -ErrorAction Stop
18+
19+
Write-Host "Moving frontend build to /server/dist ..." -f Magenta
20+
Move-Item "$projectRoot/web/dist" "$projectRoot/server/" -Force -ErrorAction Stop
21+
22+
Set-Location $projectRoot
23+
24+
Write-Host "Building backend..." -f Magenta
25+
go build -o ./build/memos.exe ./main.go
26+
Write-Host "Backend built!" -f green
27+
28+
Write-Host "Removing frontend from /server/dist ..." -f Magenta
29+
Remove-Item "$projectRoot/server/dist" -Recurse -Force -ErrorAction SilentlyContinue
30+
31+
Write-Host "Restoring frontend placeholder..." -f Magenta
32+
Move-Item "$projectRoot/server/dist.bak" "$projectRoot/server/dist" -Force -ErrorAction Stop
33+
34+
Write-Host "You can test the build with ./build/memos.exe --mode demo" -f Green
35+
36+
Set-Location -Path $projectRoot

scripts/start.ps1

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# This script starts the backend and frontend in development mode, with live reload.
2+
# It also installs frontend dependencies.
3+
4+
# For more details on setting-up a development environment, check the docs:
5+
# https://github.com/usememos/memos/blob/main/docs/development.md
6+
7+
# Usage: ./scripts/start.ps1
8+
$LastExitCode = 0
9+
10+
$projectRoot = (Resolve-Path "$MyInvocation.MyCommand.Path/..").Path
11+
Write-Host "Project root: $projectRoot"
12+
13+
Write-Host "Starting backend..." -f Magenta
14+
Start-Process -WorkingDirectory "$projectRoot" -FilePath "air" "-c ./scripts/.air-windows.toml"
15+
if ($LastExitCode -ne 0) {
16+
Write-Host "Failed to start backend!" -f Red
17+
exit $LastExitCode
18+
}
19+
else {
20+
Write-Host "Backend started!" -f Green
21+
}
22+
23+
Write-Host "Installing frontend dependencies..." -f Magenta
24+
Start-Process -Wait -WorkingDirectory "$projectRoot/web" -FilePath "powershell" -ArgumentList "pnpm i"
25+
if ($LastExitCode -ne 0) {
26+
Write-Host "Failed to install frontend dependencies!" -f Red
27+
exit $LastExitCode
28+
}
29+
else {
30+
Write-Host "Frontend dependencies installed!" -f Green
31+
}
32+
33+
Write-Host "Starting frontend..." -f Magenta
34+
Start-Process -WorkingDirectory "$projectRoot/web" -FilePath "powershell" -ArgumentList "pnpm dev"
35+
if ($LastExitCode -ne 0) {
36+
Write-Host "Failed to start frontend!" -f Red
37+
exit $LastExitCode
38+
}
39+
else {
40+
Write-Host "Frontend started!" -f Green
41+
}
42+

server/profile/profile.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"runtime"
78
"strings"
89

910
"github.com/spf13/viper"
@@ -31,15 +32,16 @@ func (p *Profile) IsDev() bool {
3132
func checkDSN(dataDir string) (string, error) {
3233
// Convert to absolute path if relative path is supplied.
3334
if !filepath.IsAbs(dataDir) {
34-
absDir, err := filepath.Abs(filepath.Dir(os.Args[0]) + "/" + dataDir)
35+
relativeDir := filepath.Join(filepath.Dir(os.Args[0]), dataDir)
36+
absDir, err := filepath.Abs(relativeDir)
3537
if err != nil {
3638
return "", err
3739
}
3840
dataDir = absDir
3941
}
4042

41-
// Trim trailing / in case user supplies
42-
dataDir = strings.TrimRight(dataDir, "/")
43+
// Trim trailing \ or / in case user supplies
44+
dataDir = strings.TrimRight(dataDir, "\\/")
4345

4446
if _, err := os.Stat(dataDir); err != nil {
4547
return "", fmt.Errorf("unable to access data folder %s, err %w", dataDir, err)
@@ -61,7 +63,18 @@ func GetProfile() (*Profile, error) {
6163
}
6264

6365
if profile.Mode == "prod" && profile.Data == "" {
64-
profile.Data = "/var/opt/memos"
66+
if runtime.GOOS == "windows" {
67+
profile.Data = filepath.Join(os.Getenv("ProgramData"), "memos")
68+
69+
if _, err := os.Stat(profile.Data); os.IsNotExist(err) {
70+
if err := os.MkdirAll(profile.Data, 0770); err != nil {
71+
fmt.Printf("Failed to create data directory: %s, err: %+v\n", profile.Data, err)
72+
return nil, err
73+
}
74+
}
75+
} else {
76+
profile.Data = "/var/opt/memos"
77+
}
6578
}
6679

6780
dataDir, err := checkDSN(profile.Data)
@@ -71,7 +84,8 @@ func GetProfile() (*Profile, error) {
7184
}
7285

7386
profile.Data = dataDir
74-
profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
87+
dbFile := fmt.Sprintf("memos_%s.db", profile.Mode)
88+
profile.DSN = filepath.Join(dataDir, dbFile)
7589
profile.Version = version.GetCurrentVersion(profile.Mode)
7690

7791
return &profile, nil

0 commit comments

Comments
 (0)