11import { Client , createClient } from "@previewjs/server/client" ;
22import execa from "execa" ;
3- import { openSync , readFileSync } from "fs" ;
3+ import { closeSync , openSync , readFileSync , utimesSync , watch } from "fs" ;
44import path from "path" ;
55import type { OutputChannel } from "vscode" ;
66import { clientId } from "./client-id" ;
77import { SERVER_PORT } from "./port" ;
88
9- export async function startPreviewJsServer (
9+ const logsPath = path . join ( __dirname , "server.log" ) ;
10+ const serverLockFilePath = path . join ( __dirname , "process.lock" ) ;
11+
12+ export async function ensureServerRunning (
1013 outputChannel : OutputChannel
1114) : Promise < Client | null > {
15+ const client = createClient ( `http://localhost:${ SERVER_PORT } ` ) ;
16+ await startServer ( outputChannel ) ;
17+ const ready = streamServerLogs ( outputChannel ) ;
18+ await ready ;
19+ await client . updateClientStatus ( {
20+ clientId,
21+ alive : true ,
22+ } ) ;
23+ return client ;
24+ }
25+
26+ async function startServer ( outputChannel : OutputChannel ) : Promise < boolean > {
1227 const isWindows = process . platform === "win32" ;
1328 let useWsl = false ;
1429 const nodeVersionCommand = "node --version" ;
@@ -39,7 +54,7 @@ export async function startPreviewJsServer(
3954 invalidNode: if ( checkNodeVersion . kind === "invalid" ) {
4055 outputChannel . appendLine ( checkNodeVersion . message ) ;
4156 if ( ! isWindows ) {
42- return null ;
57+ return false ;
4358 }
4459 // On Windows, try WSL as well.
4560 outputChannel . appendLine ( `Attempting again with WSL...` ) ;
@@ -59,22 +74,21 @@ export async function startPreviewJsServer(
5974 break invalidNode;
6075 }
6176 outputChannel . appendLine ( checkNodeVersionWsl . message ) ;
62- return null ;
77+ return false ;
6378 }
64- const logsPath = path . join ( __dirname , "server.log" ) ;
65- const logs = openSync ( logsPath , "w" ) ;
6679 outputChannel . appendLine (
6780 `🚀 Starting Preview.js server${ useWsl ? " from WSL" : "" } ...`
6881 ) ;
6982 outputChannel . appendLine ( `Streaming server logs to: ${ logsPath } ` ) ;
70- outputChannel . appendLine (
71- `If you experience any issues, please include this log file in bug reports.`
72- ) ;
7383 const nodeServerCommand = "node --trace-warnings server.js" ;
74- const serverOptions = {
84+ const serverOptions : execa . Options = {
7585 cwd : __dirname ,
76- stdio : [ "ignore" , logs , logs ] ,
77- } as const ;
86+ stdio : "inherit" ,
87+ env : {
88+ PREVIEWJS_LOCK_FILE : serverLockFilePath ,
89+ PREVIEWJS_LOG_FILE : logsPath ,
90+ } ,
91+ } ;
7892 let serverProcess : execa . ExecaChildProcess < string > ;
7993 if ( useWsl ) {
8094 serverProcess = execa (
@@ -90,53 +104,51 @@ export async function startPreviewJsServer(
90104 detached : true ,
91105 } ) ;
92106 }
107+ serverProcess . unref ( ) ;
108+ return true ;
109+ }
93110
94- const client = createClient ( `http://localhost:${ SERVER_PORT } ` ) ;
95- try {
96- const startTime = Date . now ( ) ;
97- const timeoutMillis = 30000 ;
98- loop: while ( true ) {
99- try {
100- await client . info ( ) ;
101- break loop;
102- } catch ( e ) {
103- if ( serverProcess . exitCode ) {
104- // Important: an exit code of 0 may be correct, especially if:
105- // 1. Another server is already running.
106- // 2. WSL is used, so the process exits immediately because it spans another one.
107- outputChannel . append ( readFileSync ( logsPath , "utf8" ) ) ;
108- throw new Error (
109- `Preview.js server exited with code ${ serverProcess . exitCode } `
110- ) ;
111- }
112- if ( Date . now ( ) - startTime > timeoutMillis ) {
113- throw new Error (
114- `Connection timed out after ${ timeoutMillis } ms: ${ e } `
115- ) ;
111+ function streamServerLogs ( outputChannel : OutputChannel ) {
112+ const ready = new Promise < void > ( ( resolve ) => {
113+ let lastKnownLogsLength = 0 ;
114+ let resolved = false ;
115+ // Ensure file exists before watching.
116+ // Source: https://remarkablemark.org/blog/2017/12/17/touch-file-nodejs/
117+ try {
118+ const time = Date . now ( ) ;
119+ utimesSync ( logsPath , time , time ) ;
120+ } catch ( e ) {
121+ let fd = openSync ( logsPath , "a" ) ;
122+ closeSync ( fd ) ;
123+ }
124+ watch (
125+ logsPath ,
126+ {
127+ persistent : false ,
128+ } ,
129+ ( ) => {
130+ try {
131+ const logsContent = ignoreBellPrefix ( readFileSync ( logsPath , "utf8" ) ) ;
132+ const newLogsLength = logsContent . length ;
133+ if ( newLogsLength < lastKnownLogsLength ) {
134+ // Log file has been rewritten.
135+ outputChannel . append ( "\n⚠️ Preview.js server was restarted ⚠️\n\n" ) ;
136+ lastKnownLogsLength = 0 ;
137+ }
138+ outputChannel . append ( logsContent . slice ( lastKnownLogsLength ) ) ;
139+ lastKnownLogsLength = newLogsLength ;
140+ // Note: " running on " also appears in "Preview.js daemon server is already running on port 9315".
141+ if ( ! resolved && logsContent . includes ( " running on " ) ) {
142+ resolve ( ) ;
143+ resolved = true ;
144+ }
145+ } catch ( e : any ) {
146+ // Fine, ignore. It just means log streaming is broken.
116147 }
117- // Ignore the error and retry after a short delay.
118- await new Promise < void > ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
119148 }
120- }
121- client . info ( ) ;
122- await client . waitForReady ( ) ;
123- outputChannel . appendLine ( `✅ Preview.js server ready.` ) ;
124- serverProcess . unref ( ) ;
125- } catch ( e : any ) {
126- if ( e . stack ) {
127- outputChannel . appendLine ( e . stack ) ;
128- }
129- outputChannel . appendLine (
130- `❌ Preview.js server failed to start. Please check logs above and report the issue: https://github.com/fwouts/previewjs/issues`
131149 ) ;
132- await serverProcess ;
133- throw e ;
134- }
135- await client . updateClientStatus ( {
136- clientId,
137- alive : true ,
138150 } ) ;
139- return client ;
151+ return ready ;
140152}
141153
142154function checkNodeVersionResult ( result : execa . ExecaReturnValue < string > ) :
0 commit comments