@@ -3,34 +3,46 @@ package codespace
33import (
44 "context"
55 "fmt"
6+ "io/ioutil"
7+ "log"
68 "net"
9+ "os"
710
811 "github.com/cli/cli/v2/internal/codespaces"
912 "github.com/cli/cli/v2/pkg/liveshare"
1013 "github.com/spf13/cobra"
1114)
1215
16+ type sshOptions struct {
17+ codespace string
18+ profile string
19+ serverPort int
20+ debug bool
21+ debugFile string
22+ }
23+
1324func newSSHCmd (app * App ) * cobra.Command {
14- var sshProfile , codespaceName string
15- var sshServerPort int
25+ var opts sshOptions
1626
1727 sshCmd := & cobra.Command {
1828 Use : "ssh [flags] [--] [ssh-flags] [command]" ,
1929 Short : "SSH into a codespace" ,
2030 RunE : func (cmd * cobra.Command , args []string ) error {
21- return app .SSH (cmd .Context (), args , sshProfile , codespaceName , sshServerPort )
31+ return app .SSH (cmd .Context (), args , opts )
2232 },
2333 }
2434
25- sshCmd .Flags ().StringVarP (& sshProfile , "profile" , "" , "" , "Name of the SSH profile to use" )
26- sshCmd .Flags ().IntVarP (& sshServerPort , "server-port" , "" , 0 , "SSH server port number (0 => pick unused)" )
27- sshCmd .Flags ().StringVarP (& codespaceName , "codespace" , "c" , "" , "Name of the codespace" )
35+ sshCmd .Flags ().StringVarP (& opts .profile , "profile" , "" , "" , "Name of the SSH profile to use" )
36+ sshCmd .Flags ().IntVarP (& opts .serverPort , "server-port" , "" , 0 , "SSH server port number (0 => pick unused)" )
37+ sshCmd .Flags ().StringVarP (& opts .codespace , "codespace" , "c" , "" , "Name of the codespace" )
38+ sshCmd .Flags ().BoolVarP (& opts .debug , "debug" , "d" , false , "Log debug data to a file" )
39+ sshCmd .Flags ().StringVarP (& opts .debugFile , "debug-file" , "" , "" , "Path of the file log to" )
2840
2941 return sshCmd
3042}
3143
3244// SSH opens an ssh session or runs an ssh command in a codespace.
33- func (a * App ) SSH (ctx context.Context , sshArgs []string , sshProfile , codespaceName string , localSSHServerPort int ) (err error ) {
45+ func (a * App ) SSH (ctx context.Context , sshArgs []string , opts sshOptions ) (err error ) {
3446 // Ensure all child tasks (e.g. port forwarding) terminate before return.
3547 ctx , cancel := context .WithCancel (ctx )
3648 defer cancel ()
@@ -45,12 +57,22 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, sshProfile, codespaceNa
4557 authkeys <- checkAuthorizedKeys (ctx , a .apiClient , user .Login )
4658 }()
4759
48- codespace , err := getOrChooseCodespace (ctx , a .apiClient , codespaceName )
60+ codespace , err := getOrChooseCodespace (ctx , a .apiClient , opts . codespace )
4961 if err != nil {
5062 return fmt .Errorf ("get or choose codespace: %w" , err )
5163 }
5264
53- session , err := codespaces .ConnectToLiveshare (ctx , a .logger , a .apiClient , codespace )
65+ var debugLogger * fileLogger
66+ if opts .debug {
67+ debugLogger , err = newFileLogger (opts .debugFile )
68+ if err != nil {
69+ return fmt .Errorf ("error creating debug logger: %w" , err )
70+ }
71+ defer safeClose (debugLogger , & err )
72+ a .logger .Println ("Debug file located at: " + debugLogger .Name ())
73+ }
74+
75+ session , err := codespaces .ConnectToLiveshare (ctx , a .logger , debugLogger , a .apiClient , codespace )
5476 if err != nil {
5577 return fmt .Errorf ("error connecting to Live Share: %w" , err )
5678 }
@@ -66,6 +88,7 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, sshProfile, codespaceNa
6688 return fmt .Errorf ("error getting ssh server details: %w" , err )
6789 }
6890
91+ localSSHServerPort := opts .serverPort
6992 usingCustomPort := localSSHServerPort != 0 // suppress log of command line in Shell
7093
7194 // Ensure local port is listening before client (Shell) connects.
@@ -76,15 +99,15 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, sshProfile, codespaceNa
7699 defer listen .Close ()
77100 localSSHServerPort = listen .Addr ().(* net.TCPAddr ).Port
78101
79- connectDestination := sshProfile
102+ connectDestination := opts . profile
80103 if connectDestination == "" {
81104 connectDestination = fmt .Sprintf ("%s@localhost" , sshUser )
82105 }
83106
84107 a .logger .Println ("Ready..." )
85108 tunnelClosed := make (chan error , 1 )
86109 go func () {
87- fwd := liveshare .NewPortForwarder (session , "sshd" , remoteSSHServerPort )
110+ fwd := liveshare .NewPortForwarder (session , "sshd" , remoteSSHServerPort , true )
88111 tunnelClosed <- fwd .ForwardToListener (ctx , listen ) // always non-nil
89112 }()
90113
@@ -103,3 +126,43 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, sshProfile, codespaceNa
103126 return nil // success
104127 }
105128}
129+
130+ // fileLogger is a wrapper around an log.Logger configured to write
131+ // to a file. It exports two additional methods to get the log file name
132+ // and close the file handle when the operation is finished.
133+ type fileLogger struct {
134+ * log.Logger
135+
136+ f * os.File
137+ }
138+
139+ // newFileLogger creates a new fileLogger. It returns an error if the file
140+ // cannot be created. The file is created on the specified path, if the path
141+ // is empty it is created in the temporary directory.
142+ func newFileLogger (file string ) (fl * fileLogger , err error ) {
143+ var f * os.File
144+ if file == "" {
145+ f , err = ioutil .TempFile ("" , "" )
146+ if err != nil {
147+ return nil , fmt .Errorf ("failed to create tmp file: %w" , err )
148+ }
149+ } else {
150+ f , err = os .Create (file )
151+ if err != nil {
152+ return nil , err
153+ }
154+ }
155+
156+ return & fileLogger {
157+ Logger : log .New (f , "" , log .LstdFlags ),
158+ f : f ,
159+ }, nil
160+ }
161+
162+ func (fl * fileLogger ) Name () string {
163+ return fl .f .Name ()
164+ }
165+
166+ func (fl * fileLogger ) Close () error {
167+ return fl .f .Close ()
168+ }
0 commit comments