Skip to content

Commit 6086aff

Browse files
committed
Added synchronization of message processing, removed "arduino/selectedBoard"
1 parent f62da6e commit 6086aff

File tree

2 files changed

+106
-90
lines changed

2 files changed

+106
-90
lines changed

Diff for: handler/handler.go

+54-90
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9-
"io/ioutil"
109
"log"
1110
"os"
1211
"regexp"
@@ -48,7 +47,10 @@ func NewInoHandler(stdin io.ReadCloser, stdout io.WriteCloser, logStreams *Strea
4847
stdStream := jsonrpc2.NewBufferedStream(logStreams.AttachStdInOut(stdin, stdout), jsonrpc2.VSCodeObjectCodec{})
4948
var stdHandler jsonrpc2.Handler = jsonrpc2.HandlerWithError(handler.FromStdio)
5049
if asyncProcessing {
51-
stdHandler = jsonrpc2.AsyncHandler(stdHandler)
50+
stdHandler = AsyncHandler{
51+
handler: stdHandler,
52+
synchronizer: &handler.synchronizer,
53+
}
5254
}
5355
handler.StdioConn = jsonrpc2.NewConn(context.Background(), stdStream, stdHandler)
5456
if enableLogging {
@@ -59,11 +61,12 @@ func NewInoHandler(stdin io.ReadCloser, stdout io.WriteCloser, logStreams *Strea
5961

6062
// InoHandler is a JSON-RPC handler that delegates messages to clangd.
6163
type InoHandler struct {
62-
StdioConn *jsonrpc2.Conn
63-
ClangdConn *jsonrpc2.Conn
64-
clangdProc ClangdProc
65-
data map[lsp.DocumentURI]*FileData
66-
config BoardConfig
64+
StdioConn *jsonrpc2.Conn
65+
ClangdConn *jsonrpc2.Conn
66+
clangdProc ClangdProc
67+
data map[lsp.DocumentURI]*FileData
68+
config BoardConfig
69+
synchronizer Synchronizer
6770
}
6871

6972
// ClangdProc contains the process input / output streams for clangd.
@@ -108,15 +111,7 @@ func (handler *InoHandler) FromStdio(ctx context.Context, conn *jsonrpc2.Conn, r
108111
return
109112
}
110113

111-
// Handle special methods (non-LSP)
112-
switch req.Method {
113-
case "arduino/selectedBoard":
114-
p := params.(*BoardConfig)
115-
err = handler.changeBoardConfig(ctx, p)
116-
return
117-
}
118-
119-
// Handle LSP methods: transform and send to clangd
114+
// Handle LSP methods: transform parameters and send to clangd
120115
var uri lsp.DocumentURI
121116
if params == nil {
122117
params = req.Params
@@ -128,89 +123,53 @@ func (handler *InoHandler) FromStdio(ctx context.Context, conn *jsonrpc2.Conn, r
128123
}
129124
if req.Notif {
130125
err = handler.ClangdConn.Notify(ctx, req.Method, params)
126+
if enableLogging {
127+
log.Println("From stdio:", req.Method)
128+
}
131129
} else {
132130
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
133131
defer cancel()
134132
result, err = sendRequest(ctx, handler.ClangdConn, req.Method, params)
133+
if enableLogging {
134+
log.Println("From stdio:", req.Method, "id", req.ID)
135+
}
135136
}
136137
if err != nil {
138+
// Exit the process and trigger a restart by the client in case of a severe error
137139
if err.Error() == "context deadline exceeded" {
138-
// Exit the process and trigger a restart by the client
139-
log.Println("Timeout exceeded while waiting for a reply from clangd:", req.Method)
140-
log.Println("Please restart the language server.")
141-
handler.StopClangd()
142-
os.Exit(1)
140+
log.Println("Timeout exceeded while waiting for a reply from clangd.")
141+
handler.exit()
142+
}
143+
if strings.Contains(err.Error(), "non-added document") || strings.Contains(err.Error(), "non-added file") {
144+
log.Println("The clangd process has lost track of the open document.")
145+
handler.exit()
143146
}
144147
return
145148
}
146-
if enableLogging {
147-
log.Println("From stdio:", req.Method)
148-
}
149+
150+
// Transform and return the result
149151
if result != nil {
150152
result = handler.transformClangdResult(req.Method, uri, result)
151153
}
152154
return
153155
}
154156

155-
func (handler *InoHandler) changeBoardConfig(ctx context.Context, config *BoardConfig) (resultErr error) {
156-
previousFqbn := handler.config.SelectedBoard.Fqbn
157-
handler.config = *config
158-
if config.SelectedBoard.Fqbn == previousFqbn || len(handler.data) == 0 {
159-
return
160-
}
161-
if enableLogging {
162-
log.Println("New board configuration:", *config)
163-
}
164-
165-
// Stop the clangd process and update all file data with the new board
157+
func (handler *InoHandler) exit() {
158+
log.Println("Please restart the language server.")
166159
handler.StopClangd()
167-
openFileData := make(map[*FileData][]byte)
168-
for uri, data := range handler.data {
169-
if uri != data.sourceURI {
170-
continue
171-
}
172-
targetBytes, err := updateCpp([]byte(data.sourceText), uriToPath(data.sourceURI), config.SelectedBoard.Fqbn, true, uriToPath(data.targetURI))
173-
if err != nil {
174-
if resultErr == nil {
175-
resultErr = handler.handleError(ctx, err)
176-
}
177-
targetBytes, _ = ioutil.ReadFile(uriToPath(data.targetURI))
178-
}
179-
sourceLineMap, targetLineMap := createSourceMaps(bytes.NewReader(targetBytes))
180-
data.sourceLineMap = sourceLineMap
181-
data.targetLineMap = targetLineMap
182-
openFileData[data] = targetBytes
183-
}
184-
185-
// Restart the clangd process, initialize it and reopen the files
186-
handler.startClangd()
187-
initResult := new(lsp.InitializeResult)
188-
err := handler.ClangdConn.Call(ctx, "initialize", &handler.clangdProc.initParams, initResult)
189-
if err != nil {
190-
resultErr = err
191-
return
192-
}
193-
for data, targetBytes := range openFileData {
194-
if enableLogging {
195-
log.Println("Reopening file: ", data.sourceURI)
196-
}
197-
openParams := lsp.DidOpenTextDocumentParams{
198-
TextDocument: lsp.TextDocumentItem{
199-
LanguageID: "cpp",
200-
URI: data.targetURI,
201-
Version: data.version,
202-
Text: string(targetBytes),
203-
},
204-
}
205-
err := handler.ClangdConn.Notify(ctx, "textDocument/didOpen", openParams)
206-
if err != nil && resultErr == nil {
207-
resultErr = err
208-
}
209-
}
210-
return
160+
os.Exit(1)
211161
}
212162

213163
func (handler *InoHandler) transformParamsToClangd(ctx context.Context, method string, params interface{}) (uri lsp.DocumentURI, err error) {
164+
needsWriteLock := method == "textDocument/didOpen" || method == "textDocument/didChange" || method == "textDocument/didClose"
165+
if needsWriteLock {
166+
handler.synchronizer.DataMux.Lock()
167+
defer handler.synchronizer.DataMux.Unlock()
168+
} else {
169+
handler.synchronizer.DataMux.RLock()
170+
defer handler.synchronizer.DataMux.RUnlock()
171+
}
172+
214173
switch method {
215174
case "initialize":
216175
handler.clangdProc.initParams = *params.(*lsp.InitializeParams)
@@ -221,7 +180,7 @@ func (handler *InoHandler) transformParamsToClangd(ctx context.Context, method s
221180
case "textDocument/didChange":
222181
p := params.(*lsp.DidChangeTextDocumentParams)
223182
uri = p.TextDocument.URI
224-
err = handler.ino2cppDidChangeTextDocumentParams(p)
183+
err = handler.ino2cppDidChangeTextDocumentParams(ctx, p)
225184
case "textDocument/didSave":
226185
p := params.(*lsp.DidSaveTextDocumentParams)
227186
uri = p.TextDocument.URI
@@ -317,7 +276,7 @@ func (handler *InoHandler) createFileData(ctx context.Context, sourceURI lsp.Doc
317276
return data, targetBytes, nil
318277
}
319278

320-
func (handler *InoHandler) updateFileData(data *FileData, change *lsp.TextDocumentContentChangeEvent) (err error) {
279+
func (handler *InoHandler) updateFileData(ctx context.Context, data *FileData, change *lsp.TextDocumentContentChangeEvent) (err error) {
321280
rang := change.Range
322281
if rang == nil || rang.Start.Line != rang.End.Line {
323282
// Update the source text and regenerate the cpp code
@@ -439,11 +398,11 @@ func (handler *InoHandler) ino2cppTextDocumentItem(ctx context.Context, doc *lsp
439398
return nil
440399
}
441400

442-
func (handler *InoHandler) ino2cppDidChangeTextDocumentParams(params *lsp.DidChangeTextDocumentParams) error {
401+
func (handler *InoHandler) ino2cppDidChangeTextDocumentParams(ctx context.Context, params *lsp.DidChangeTextDocumentParams) error {
443402
handler.ino2cppTextDocumentIdentifier(&params.TextDocument.TextDocumentIdentifier)
444403
if data, ok := handler.data[params.TextDocument.URI]; ok {
445404
for index := range params.ContentChanges {
446-
err := handler.updateFileData(data, &params.ContentChanges[index])
405+
err := handler.updateFileData(ctx, data, &params.ContentChanges[index])
447406
if err != nil {
448407
return err
449408
}
@@ -551,6 +510,9 @@ func (handler *InoHandler) ino2cppWorkspaceEdit(origEdit *lsp.WorkspaceEdit) *ls
551510
}
552511

553512
func (handler *InoHandler) transformClangdResult(method string, uri lsp.DocumentURI, result interface{}) interface{} {
513+
handler.synchronizer.DataMux.RLock()
514+
defer handler.synchronizer.DataMux.RUnlock()
515+
554516
switch method {
555517
case "textDocument/completion":
556518
r := result.(*lsp.CompletionList)
@@ -798,20 +760,22 @@ func (handler *InoHandler) FromClangd(ctx context.Context, connection *jsonrpc2.
798760
var result interface{}
799761
if req.Notif {
800762
err = handler.StdioConn.Notify(ctx, req.Method, params)
763+
if enableLogging {
764+
log.Println("From clangd:", req.Method)
765+
}
801766
} else {
802767
result, err = sendRequest(ctx, handler.StdioConn, req.Method, params)
803-
}
804-
if err != nil {
805-
log.Println("From clangd: Method:", req.Method, "Error:", err)
806-
return nil, err
807-
}
808-
if enableLogging {
809-
log.Println("From clangd:", req.Method)
768+
if enableLogging {
769+
log.Println("From clangd:", req.Method, "id", req.ID)
770+
}
810771
}
811772
return result, err
812773
}
813774

814775
func (handler *InoHandler) transformParamsToStdio(method string, raw *json.RawMessage) (params interface{}, uri lsp.DocumentURI, err error) {
776+
handler.synchronizer.DataMux.RLock()
777+
defer handler.synchronizer.DataMux.RUnlock()
778+
815779
params, err = readParams(method, raw)
816780
if err != nil {
817781
return

Diff for: handler/syncer.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"log"
6+
"sync"
7+
8+
"github.com/sourcegraph/jsonrpc2"
9+
)
10+
11+
// Synchronizer is used to block message processing while an edit or config change is applied.
12+
type Synchronizer struct {
13+
// FileMux is a read/write mutex for file access. It is locked during the processing of
14+
// messages that modify target files for clangd.
15+
FileMux sync.RWMutex
16+
// DataMux is a mutex for document metadata access, i.e. source-target URI mappings and line mappings.
17+
DataMux sync.RWMutex
18+
}
19+
20+
// AsyncHandler wraps a Handler such that each request is handled in its own goroutine.
21+
type AsyncHandler struct {
22+
handler jsonrpc2.Handler
23+
synchronizer *Synchronizer
24+
}
25+
26+
// Handle handles a request or notification
27+
func (ah AsyncHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
28+
needsWriteLock := req.Method == "textDocument/didOpen" || req.Method == "textDocument/didChange"
29+
if needsWriteLock {
30+
ah.synchronizer.FileMux.Lock()
31+
if enableLogging {
32+
log.Println("Message processing locked for", req.Method)
33+
}
34+
go ah.runWrite(ctx, conn, req)
35+
} else {
36+
ah.synchronizer.FileMux.RLock()
37+
go ah.runRead(ctx, conn, req)
38+
}
39+
}
40+
41+
func (ah AsyncHandler) runRead(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
42+
defer ah.synchronizer.FileMux.RUnlock()
43+
ah.handler.Handle(ctx, conn, req)
44+
}
45+
46+
func (ah AsyncHandler) runWrite(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
47+
defer ah.synchronizer.FileMux.Unlock()
48+
ah.handler.Handle(ctx, conn, req)
49+
if enableLogging {
50+
log.Println("Message processing unlocked for", req.Method)
51+
}
52+
}

0 commit comments

Comments
 (0)