From 5340008ad734e77e6e5a950c290558ab4f79723d Mon Sep 17 00:00:00 2001 From: Lincoln Nogueira Date: Tue, 9 May 2023 21:03:55 -0300 Subject: [PATCH] feat: improve Windows support (#1645) * 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) * feat: improve Windows support - Fix local file storage path handling on Windows - Improve Windows dev script --- scripts/start.ps1 | 170 +++++++++++++++++++++++++++++++++++++-------- server/resource.go | 9 ++- 2 files changed, 147 insertions(+), 32 deletions(-) diff --git a/scripts/start.ps1 b/scripts/start.ps1 index fa2dfabc..a1c8f423 100644 --- a/scripts/start.ps1 +++ b/scripts/start.ps1 @@ -2,41 +2,153 @@ # 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 +# * https://usememos.com/docs/contribution/development +# * 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 +foreach ($dir in @(".", "../")) { + if (Test-Path (Join-Path $dir ".gitignore")) { + $repoRoot = (Resolve-Path $dir).Path + break + } } -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 +## +$frontendPort = 3001 +# Tasks to run, in order +$runTasks = @( + @{ + Desc = "start backend with live reload"; + Exe = "air.exe"; + Args = "-c .\scripts\.air-windows.toml"; + Dir = "$repoRoot"; + Wait = $false; + }, + @{ + Desc = "install frontend dependencies"; + Exe = "pnpm.exe"; + Args = "i"; + Dir = "$repoRoot/web" + Wait = $true; + } + @{ + Desc = "start frontend with live reload"; + Exe = "pnpm.exe"; + Args = "dev"; + Dir = "$repoRoot/web"; + Wait = $false; + } +) +## + +if (!$repoRoot) { + Write-Host "Could not find repository root!" -f Red + Write-Host "cd into the repository root and run the script again." + Exit 1 } -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 +Write-Host "Repository root is $repoRoot" +Write-Host "Starting development environment...`n" +Write-Host @" +███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗ +████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝ +██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗ +██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║ +██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║ +╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ +"@ + +function Stop-ProcessTree { + Param([int]$ParentProcessId) + if (!$ParentProcessId) { + Write-Host "Stop-ProcessTree: unspecified ParentProcessId!" -f Red + return + } + Write-Host "Terminating pid $($ParentProcessId) with all its child processes" -f DarkGray + Get-CimInstance Win32_Process | Where-Object { + $_.ParentProcessId -eq $ParentProcessId + } | ForEach-Object { + Stop-ProcessTree $_.ProcessId + } + Stop-Process -Id $ParentProcessId -ErrorAction SilentlyContinue } +$maxDescLength = ( $runTasks | ForEach-Object { $_.Desc.Length } | Measure-Object -Maximum).Maximum +$spawnedPids = @() +foreach ($task in $runTasks) { + Write-Host ("Running task ""$($task.Desc)""...").PadRight($maxDescLength + 20) -f Blue -NoNewline + $task.Dir = (Resolve-Path $task.Dir).Path + try { + $process = Start-Process -PassThru -WorkingDirectory $task.Dir -FilePath $task.Exe -ArgumentList $task.Args -Wait:$task.Wait + + if ($process.ExitCode -and $process.ExitCode -ne 0) { + # ExitCode only works for processes started with -Wait:$true + throw "Process exited with code $($process.ExitCode)" + } + + Write-Host "[OK]" -f Green + $spawnedPids += $process.Id + } + catch { + Write-Host "[FAILED]" -f Red + Write-Host "Error: $_" -f Red + Write-Host "Unable to execute: $($task.Exe) $($task.Args)" -f Red + Write-Host "Process working directory: $($task.Dir)" -f Red + + foreach ($spawnedPid in $spawnedPids) { + Stop-ProcessTree -ParentProcessId $spawnedPid + } + Exit $process.ExitCode + } +} + +Write-Host "Front-end should be accessible at:" -f Green +$ipAddresses = (Get-NetIPAddress -AddressFamily IPv4) | Select-Object -ExpandProperty IPAddress | Sort-Object +$ipAddresses += "localhost" +foreach ($ip in $ipAddresses) { + Write-Host "· http://$($ip):$($frontendPort)" -f Cyan +} + +Write-Host "`nPress" -NoNewline +Write-Host " Ctrl + C" -f DarkYellow -NoNewline +Write-Host " or" -NoNewline +Write-Host " Esc" -f DarkYellow -NoNewline +Write-Host " to terminate running servers." -f DarkYellow +[Console]::TreatControlCAsInput = $true + +$lastPoll = 0 +$noWaitTasks = $runTasks | Where-Object { $_.Wait -eq $false } +while ($true) { + if ([Console]::KeyAvailable) { + $readkey = [Console]::ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho") + if ($readkey.Modifiers -eq "Control" -and $readkey.Key -eq "C") { + break + } + if ($readkey.Key -eq "Escape") { + Break + } + } + + # Poll for processes that exited unexpectedly + # Do this every 5 seconds to avoid excessive CPU usage + if (([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - $lastPoll) -ge 5000) { + $noWaitTasks | ForEach-Object { + $name = $_.Exe.TrimEnd(".exe") + if (!(Get-Process -Name $name -ErrorAction SilentlyContinue)) { + Write-Host "Process " -f Red -NoNewline + Write-Host $name -NoNewline -f DarkYellow + Write-Host " is not running anymore!" -f Red + break + } + } + $lastPoll = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + } + Start-Sleep -Milliseconds 500 +} + +foreach ($spawnedPid in $spawnedPids) { + Stop-ProcessTree -ParentProcessId $spawnedPid +} + +Write-Host "Exiting..." diff --git a/server/resource.go b/server/resource.go index 0ef55139..37a5088a 100644 --- a/server/resource.go +++ b/server/resource.go @@ -112,6 +112,9 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { Blob: fileBytes, } } else if storageServiceID == api.LocalStorage { + // filepath.Join() should be used for local file paths, + // as it handles the os-specific path separator automatically. + // path.Join() always uses '/' as path separator. systemSettingLocalStoragePath, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{Name: api.SystemSettingLocalStoragePathName}) if err != nil && common.ErrorCode(err) != common.NotFound { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find local storage path setting").SetInternal(err) @@ -123,11 +126,11 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal local storage path setting").SetInternal(err) } } - filePath := localStoragePath + filePath := filepath.FromSlash(localStoragePath) if !strings.Contains(filePath, "{filename}") { - filePath = path.Join(filePath, "{filename}") + filePath = filepath.Join(filePath, "{filename}") } - filePath = path.Join(s.Profile.Data, replacePathTemplate(filePath, file.Filename)) + filePath = filepath.Join(s.Profile.Data, replacePathTemplate(filePath, file.Filename)) dir, filename := filepath.Split(filePath) if err = os.MkdirAll(dir, os.ModePerm); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create directory").SetInternal(err)