@@ -3,11 +3,13 @@ package devcontainers
33import (
44 "bufio"
55 "bytes"
6+ "encoding/json"
67 "fmt"
78 "os"
89 "os/exec"
910 "path"
1011 "path/filepath"
12+ "sort"
1113 "strings"
1214
1315 "github.com/stuartleeks/devcontainer-cli/internal/pkg/terminal"
@@ -67,10 +69,17 @@ func ListDevcontainers() ([]DevcontainerInfo, error) {
6769 name = name [index + 1 :]
6870 }
6971 }
72+ localPath := parts [listPartLocalFolder ]
73+ if strings .HasPrefix (localPath , "\\ \\ wsl$" ) && wsl .IsWsl () {
74+ localPath , err = wsl .ConvertWindowsPathToWslPath (localPath )
75+ if err != nil {
76+ return []DevcontainerInfo {}, fmt .Errorf ("error converting path: %s" , err )
77+ }
78+ }
7079 devcontainer := DevcontainerInfo {
7180 ContainerID : parts [listPartID ],
7281 ContainerName : parts [listPartContainerName ],
73- LocalFolderPath : parts [ listPartLocalFolder ] ,
82+ LocalFolderPath : localPath ,
7483 DevcontainerName : name ,
7584 }
7685 devcontainers = append (devcontainers , devcontainer )
@@ -92,87 +101,91 @@ func GetLocalFolderFromDevContainer(containerIDOrName string) (string, error) {
92101 return strings .TrimSpace (string (output )), nil
93102}
94103
95- // GetContainerIDForPath returns the ID of the running container that matches the path
96- func GetContainerIDForPath (devcontainerPath string ) (string , error ) {
97- if devcontainerPath == "" {
98- devcontainerPath = "."
99- }
100- absPath , err := filepath .Abs (devcontainerPath )
104+ // DockerMount represents mount info from Docker output
105+ type DockerMount struct {
106+ Source string `json:"Source"`
107+ Destination string `json:"Destination"`
108+ }
109+
110+ // GetSourceMountFolderFromDevContainer inspects the specified container and returns the DockerMount for the source mount
111+ func GetSourceMountFolderFromDevContainer (containerIDOrName string ) (DockerMount , error ) {
112+ localPath , err := GetLocalFolderFromDevContainer (containerIDOrName )
101113 if err != nil {
102- return "" , fmt . Errorf ( "Error handling path %q: %s" , devcontainerPath , err )
114+ return DockerMount {}, err
103115 }
104116
105- windowsPath := absPath
106- if wsl .IsWsl () {
107- var err error
108- windowsPath , err = wsl .ConvertWslPathToWindowsPath (windowsPath )
117+ if strings .HasPrefix (localPath , "\\ \\ wsl$" ) && wsl .IsWsl () {
118+ localPath , err = wsl .ConvertWindowsPathToWslPath (localPath )
109119 if err != nil {
110- return " " , err
120+ return DockerMount {}, fmt . Errorf ( "error converting path: %s " , err )
111121 }
112122 }
113123
114- devcontainerList , err := ListDevcontainers ()
124+ cmd := exec .Command ("docker" , "inspect" , containerIDOrName , "--format" , fmt .Sprintf ("{{ range .Mounts }}{{if eq .Source \" %s\" }}{{json .}}{{end}}{{end}}" , localPath ))
125+
126+ output , err := cmd .Output ()
115127 if err != nil {
116- return "" , fmt .Errorf ("Error getting container list : %s " , err )
128+ return DockerMount {} , fmt .Errorf ("Failed to read docker stdout : %v " , err )
117129 }
118130
119- for _ , devcontainer := range devcontainerList {
120- if devcontainer .LocalFolderPath == windowsPath {
121- containerID := devcontainer .ContainerID
122- return containerID , nil
123- }
131+ var mount DockerMount
132+ err = json .Unmarshal (output , & mount )
133+ if err != nil {
134+ return DockerMount {}, err
124135 }
125- return "" , fmt .Errorf ("Could not find running container for path %q" , devcontainerPath )
136+
137+ return mount , nil
126138}
127139
128- func ExecInDevContainer ( containerIDOrName string , workDir string , args [] string ) error {
140+ type byLocalPathLength [] DevcontainerInfo
129141
130- statusWriter := & terminal.UpdatingStatusWriter {}
142+ func (s byLocalPathLength ) Len () int {
143+ return len (s )
144+ }
145+ func (s byLocalPathLength ) Swap (i , j int ) {
146+ s [i ], s [j ] = s [j ], s [i ]
147+ }
148+ func (s byLocalPathLength ) Less (i , j int ) bool {
149+ return len (s [i ].LocalFolderPath ) < len (s [j ].LocalFolderPath )
150+ }
131151
132- containerID := ""
133- devcontainerList , err := ListDevcontainers ()
152+ // GetClosestPathMatchForPath returns the dev container with the closes match to the specified path
153+ func GetClosestPathMatchForPath (devContainers []DevcontainerInfo , devcontainerPath string ) (DevcontainerInfo , error ) {
154+ if devcontainerPath == "" {
155+ devcontainerPath = "."
156+ }
157+ absPath , err := filepath .Abs (devcontainerPath )
134158 if err != nil {
135- return err
159+ return DevcontainerInfo {}, fmt . Errorf ( "Error handling path %q: %s" , devcontainerPath , err )
136160 }
137161
138- // Get container ID
139- for _ , devcontainer := range devcontainerList {
140- if devcontainer .ContainerName == containerIDOrName ||
141- devcontainer .DevcontainerName == containerIDOrName ||
142- devcontainer .ContainerID == containerIDOrName {
143- containerID = devcontainer .ContainerID
144- break
162+ matchingPaths := byLocalPathLength {}
163+ for _ , devcontainer := range devContainers {
164+ // Treat as match if the specified path is within the devcontainer path
165+ if strings .HasPrefix (absPath , devcontainer .LocalFolderPath ) {
166+ matchingPaths = append (matchingPaths , devcontainer )
145167 }
146168 }
147-
148- if containerID == "" {
149- return fmt .Errorf ("Failed to find a matching (running) dev container for %q" , containerIDOrName )
169+ if len (matchingPaths ) == 0 {
170+ return DevcontainerInfo {}, fmt .Errorf ("Could not find running container for path %q" , devcontainerPath )
150171 }
172+ // return longest prefix match
173+ sort .Sort (matchingPaths )
174+ return matchingPaths [len (matchingPaths )- 1 ], nil
175+ }
151176
152- localPath , err := GetLocalFolderFromDevContainer (containerID )
153- if err != nil {
154- return err
155- }
177+ func ExecInDevContainer (containerID string , workDir string , args []string ) error {
156178
157- statusWriter .Printf ("Getting mount path" )
158- if workDir == "" {
159- workDir , err = GetWorkspaceMountPath (localPath )
160- if err != nil {
161- return err
162- }
163- }
179+ statusWriter := & terminal.UpdatingStatusWriter {}
164180
165- wslPath := localPath
166- if strings .HasPrefix (wslPath , "\\ \\ wsl$" ) && wsl .IsWsl () {
167- statusWriter .Printf ("Converting to WSL path" )
168- wslPath , err = wsl .ConvertWindowsPathToWslPath (wslPath )
169- if err != nil {
170- return fmt .Errorf ("error converting path: %s" , err )
171- }
181+ sourceMount , err := GetSourceMountFolderFromDevContainer (containerID )
182+ if err != nil {
183+ return err
172184 }
185+ localPath := sourceMount .Source
173186
174187 statusWriter .Printf ("Getting user name" )
175- devcontainerJSONPath := path .Join (wslPath , ".devcontainer/devcontainer.json" )
188+ devcontainerJSONPath := path .Join (localPath , ".devcontainer/devcontainer.json" )
176189 userName , err := GetDevContainerUserName (devcontainerJSONPath )
177190 if err != nil {
178191 return err
@@ -217,6 +230,35 @@ func ExecInDevContainer(containerIDOrName string, workDir string, args []string)
217230 fmt .Println ("Continuing without setting VSCODE_IPC_HOOK_CLI..." )
218231 }
219232
233+ mountPath := sourceMount .Destination
234+ if workDir == "" {
235+ workDir = mountPath
236+ } else if ! filepath .IsAbs (workDir ) {
237+
238+ // Convert to absolute (local) path
239+ // This takes into account current directory (potentially within the dev container path)
240+ // We'll convert local to container path below
241+ workDir , err = filepath .Abs (workDir )
242+ if err != nil {
243+ return err
244+ }
245+ }
246+
247+ statusWriter .Printf ("Test container path" )
248+ containerPathExists , err := testContainerPathExists (containerID , workDir )
249+ if err != nil {
250+ return fmt .Errorf ("error checking container path: %s" , err )
251+ }
252+ if ! containerPathExists {
253+ // path not found - try converting from local path
254+ // ? Should we check here that the workDir has localPath as a prefix?
255+ devContainerRelativePath , err := filepath .Rel (localPath , workDir )
256+ if err != nil {
257+ return fmt .Errorf ("error getting path relative to mount dir: %s" , err )
258+ }
259+ workDir = filepath .Join (mountPath , devContainerRelativePath )
260+ }
261+
220262 statusWriter .Printf ("Starting exec session\n " ) // newline to put container shell at start of line
221263 dockerArgs := []string {"exec" , "-it" , "--workdir" , workDir }
222264 if userName != "" {
@@ -306,3 +348,16 @@ func getContainerEnvVar(containerID string, varName string) (string, error) {
306348
307349 return string (buf ), nil
308350}
351+
352+ func testContainerPathExists (containerID string , path string ) (bool , error ) {
353+ dockerArgs := []string {"exec" , containerID , "bash" , "-c" , fmt .Sprintf ("[[ -d %s ]]; echo $?" , path )}
354+ dockerCmd := exec .Command ("docker" , dockerArgs ... )
355+ buf , err := dockerCmd .CombinedOutput ()
356+ if err != nil {
357+ errMessage := string (buf )
358+ return false , fmt .Errorf ("Docker exec error: %s (%s)" , err , strings .TrimSpace (errMessage ))
359+ }
360+
361+ response := strings .TrimSpace (string (buf ))
362+ return response == "0" , nil
363+ }
0 commit comments