mirror of
https://github.com/yudai/gotty.git
synced 2025-09-22 05:50:48 +02:00
Compare commits
151 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a080c85cbc | ||
![]() |
8df0bf44a8 | ||
![]() |
b4728f6aa4 | ||
![]() |
9ac120a557 | ||
![]() |
0bb62e0381 | ||
![]() |
513b3a5c4d | ||
![]() |
79e132824a | ||
![]() |
2c50c43290 | ||
![]() |
6ab3093956 | ||
![]() |
b2c2db0764 | ||
![]() |
807bcc25a4 | ||
![]() |
d1ec7125cf | ||
![]() |
a6ae1210da | ||
![]() |
973cf362fb | ||
![]() |
ba1aa690ed | ||
![]() |
248f51b290 | ||
![]() |
4d682aa01d | ||
![]() |
d0f6481cab | ||
![]() |
7355d67a64 | ||
![]() |
46a8b006f0 | ||
![]() |
48c91151ad | ||
![]() |
024ab8f28e | ||
![]() |
2b4eb55d28 | ||
![]() |
a8bb23f570 | ||
![]() |
27b6436196 | ||
![]() |
70aaf33082 | ||
![]() |
8f95182392 | ||
![]() |
c1ccfdd859 | ||
![]() |
7aafac9448 | ||
![]() |
8803721f3d | ||
![]() |
d6c98866b9 | ||
![]() |
45f8f61103 | ||
![]() |
56e9b89199 | ||
![]() |
91ee778665 | ||
![]() |
4fd3ac376c | ||
![]() |
6765efbd61 | ||
![]() |
84ec13ca19 | ||
![]() |
af41111458 | ||
![]() |
2a2a034788 | ||
![]() |
9b8d2d5ed5 | ||
![]() |
21899e638b | ||
![]() |
e81f4e9b7e | ||
![]() |
a6133f34b7 | ||
![]() |
b5c56d57f2 | ||
![]() |
54403dd678 | ||
![]() |
496ef86339 | ||
![]() |
d71e2fcfa8 | ||
![]() |
c4ed7bc415 | ||
![]() |
0f6909a165 | ||
![]() |
91be466d29 | ||
![]() |
b181c6baf5 | ||
![]() |
412ffeb06d | ||
![]() |
1b7f41ca6f | ||
![]() |
dbcdc904b9 | ||
![]() |
f23f38a492 | ||
![]() |
b1e5d95841 | ||
![]() |
b61b784187 | ||
![]() |
1aa392f29b | ||
![]() |
5046d874bc | ||
![]() |
e721482e57 | ||
![]() |
63c9a78b06 | ||
![]() |
798d2ac1d9 | ||
![]() |
7496404438 | ||
![]() |
ef91648d8f | ||
![]() |
456aa65d17 | ||
![]() |
fd25f80257 | ||
![]() |
92d8d62ae1 | ||
![]() |
8c9433ff21 | ||
![]() |
c91fef051b | ||
![]() |
8fd09cd9ec | ||
![]() |
ddbaa983c0 | ||
![]() |
be07d420dd | ||
![]() |
54597c0ba6 | ||
![]() |
49f2051c93 | ||
![]() |
ac49153071 | ||
![]() |
510542b159 | ||
![]() |
a350994aa2 | ||
![]() |
b1c5226875 | ||
![]() |
db45d0febc | ||
![]() |
84f098fe4c | ||
![]() |
ca66b46fa2 | ||
![]() |
6a6d0e1350 | ||
![]() |
888fe870dc | ||
![]() |
95ad63e5ae | ||
![]() |
d56157f1ee | ||
![]() |
6013e43933 | ||
![]() |
788c9942ad | ||
![]() |
04deb4902f | ||
![]() |
f7b54e9e5a | ||
![]() |
a9d26bec2c | ||
![]() |
c8fb306521 | ||
![]() |
1bcc6bd9ce | ||
![]() |
589ec6b50a | ||
![]() |
86151f1ac9 | ||
![]() |
23804fa2ff | ||
![]() |
ec70160666 | ||
![]() |
eb200ce579 | ||
![]() |
f72b18052a | ||
![]() |
46e33887c4 | ||
![]() |
e1de07bce2 | ||
![]() |
9e392334a7 | ||
![]() |
6af36bc6d9 | ||
![]() |
6edf5b450f | ||
![]() |
a4e77b2b76 | ||
![]() |
7715f93517 | ||
![]() |
57923e98c4 | ||
![]() |
36dfe5deac | ||
![]() |
f75cc9771a | ||
![]() |
096f77c780 | ||
![]() |
9534205004 | ||
![]() |
7321b43f67 | ||
![]() |
5eb5959c93 | ||
![]() |
5de1ece388 | ||
![]() |
82c17f8271 | ||
![]() |
6500449916 | ||
![]() |
7e11f664d9 | ||
![]() |
84abb39358 | ||
![]() |
783254c3be | ||
![]() |
9918bdce81 | ||
![]() |
c88cf7a52d | ||
![]() |
af1a80c962 | ||
![]() |
3857e41dbc | ||
![]() |
8f9d5ba582 | ||
![]() |
4f75000256 | ||
![]() |
83923b6f39 | ||
![]() |
62f5d4aaf3 | ||
![]() |
05d9267f20 | ||
![]() |
ff92777011 | ||
![]() |
0bd2f3f2c8 | ||
![]() |
00ddf781de | ||
![]() |
b711de495b | ||
![]() |
fc590d5bc3 | ||
![]() |
0e81c484a9 | ||
![]() |
e7e607b3d7 | ||
![]() |
5622bf7538 | ||
![]() |
a4a3f1a01d | ||
![]() |
eb2f2d08bc | ||
![]() |
a40dde8d3a | ||
![]() |
f654030be3 | ||
![]() |
ee77ec1b3a | ||
![]() |
e79e78a323 | ||
![]() |
4814a38708 | ||
![]() |
f133c28cf8 | ||
![]() |
678258ecf1 | ||
![]() |
b821754e8a | ||
![]() |
d3e48aa3ae | ||
![]() |
25a5bc0b89 | ||
![]() |
8e90497c31 | ||
![]() |
02e0b6fafd | ||
![]() |
c71cc21721 | ||
![]() |
4b67e3059d |
223 changed files with 17771 additions and 8075 deletions
27
.github/ISSUE_TEMPLATE.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
# When file a bug report (see below for feature requests)
|
||||
|
||||
Please answer these quesions for a bug report. Thanks!
|
||||
|
||||
### What version of GoTTY are you using (`gotty --version`)?
|
||||
|
||||
|
||||
### What operating system and browser are you using?
|
||||
|
||||
|
||||
### What did you do?
|
||||
|
||||
If possible, please provide the command you ran.
|
||||
|
||||
|
||||
### What did you expect to see?
|
||||
|
||||
|
||||
### What did you see instead?
|
||||
|
||||
If possible, please provide the output of the command and your browser's console output.
|
||||
|
||||
|
||||
|
||||
# When file a new feature proposal
|
||||
|
||||
Please provide an actual usecase that requires your new feature.
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
gotty
|
||||
bindata
|
||||
builds
|
||||
js/node_modules/*
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
|||
[submodule "libapps"]
|
||||
path = libapps
|
||||
url = https://chromium.googlesource.com/apps/libapps
|
302
.gotty
Normal file
302
.gotty
Normal file
|
@ -0,0 +1,302 @@
|
|||
// [string] Address to listen, all addresses will be used when empty
|
||||
// address = ""
|
||||
|
||||
// [string] Port to listen
|
||||
// port = "8080"
|
||||
|
||||
// [bool] Permit clients to write to the TTY
|
||||
// permit_write = false
|
||||
|
||||
// [bool] Enable basic authentication
|
||||
// enable_basic_auth = false
|
||||
|
||||
// [string] Default username and password of basic authentication (user:pass)
|
||||
// To enable basic authentication, set `true` to `enable_basic_auth`
|
||||
// credential = "user:pass"
|
||||
|
||||
// [bool] Enable random URL generation
|
||||
// enable_random_url = false
|
||||
|
||||
// [int] Default length of random strings appended to URL
|
||||
// To enable random URL generation, set `true` to `enable_random_url`
|
||||
// random_url_length = 8
|
||||
|
||||
// [bool] Enable TLS/SSL
|
||||
// enable_tls = false
|
||||
|
||||
// [string] Default TLS certificate file path
|
||||
// tls_crt_file = "~/.gotty.crt"
|
||||
|
||||
// [string] Default TLS key file path
|
||||
// tls_key_file = "~/.gotty.key"
|
||||
|
||||
// [bool] Enable client certificate authentication
|
||||
// enable_tls_client_auth = false
|
||||
|
||||
// [string] Certificate file of CA for client certificates
|
||||
// tls_ca_crt_file = "~/.gotty.ca.crt"
|
||||
|
||||
// [string] Custom index.html file
|
||||
// index_file = ""
|
||||
|
||||
// [string] Title format of browser window
|
||||
// Available variables are:
|
||||
// Command Command string
|
||||
// Pid PID of the process for the client
|
||||
// Hostname Server hostname
|
||||
// RemoteAddr Client IP address
|
||||
// title_format = "GoTTY - {{ .Command }} ({{ .Hostname }})"
|
||||
|
||||
// [bool] Enable client side reconnection when connection closed
|
||||
// enable_reconnect = false
|
||||
|
||||
// [int] Interval time to try reconnection (seconds)
|
||||
// To enable reconnection, set `true` to `enable_reconnect`
|
||||
// reconnect_time = 10
|
||||
|
||||
// [int] Timeout seconds for waiting a client (0 to disable)
|
||||
// timeout = 60
|
||||
|
||||
// [int] Maximum connection to gotty, 0(default) means no limit.
|
||||
// max_connection = 0
|
||||
|
||||
// [bool] Accept only one client and exit gotty once the client exits
|
||||
// once = false
|
||||
|
||||
// [bool] Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB)
|
||||
// permit_arguments = false
|
||||
|
||||
// [object] Client terminal (hterm) preferences
|
||||
// preferences {
|
||||
|
||||
// [enum(null, "none", "ctrl-alt", "left-alt", "right-alt")]
|
||||
// Select an AltGr detection hack^Wheuristic.
|
||||
// null: Autodetect based on navigator.language: "en-us" => "none", else => "right-alt"
|
||||
// "none": Disable any AltGr related munging.
|
||||
// "ctrl-alt": Assume Ctrl+Alt means AltGr.
|
||||
// "left-alt": Assume left Alt means AltGr.
|
||||
// "right-alt": Assume right Alt means AltGr.
|
||||
// alt_gr_mode = null
|
||||
|
||||
// [bool] If set, alt-backspace indeed is alt-backspace.
|
||||
// alt_backspace_is_meta_backspace = false
|
||||
|
||||
// [bool] Set whether the alt key acts as a meta key or as a distinct alt key.
|
||||
// alt_is_meta = false
|
||||
|
||||
// [enum("escape", "8-bit", "browser-key")]
|
||||
// Controls how the alt key is handled.
|
||||
// "escape"....... Send an ESC prefix.
|
||||
// "8-bit"........ Add 128 to the unshifted character as in xterm.
|
||||
// "browser-key".. Wait for the keypress event and see what the browser says.
|
||||
// (This won't work well on platforms where the browser performs a default action for some alt sequences.)
|
||||
// alt_sends_what = "escape"
|
||||
|
||||
// [string] URL of the terminal bell sound. Empty string for no audible bell.
|
||||
// audible_bell_sound = "lib-resource:hterm/audio/bell"
|
||||
|
||||
// [bool] If true, terminal bells in the background will create a Web Notification. http://www.w3.org/TR/notifications/
|
||||
// Displaying notifications requires permission from the user.
|
||||
// When this option is set to true, hterm will attempt to ask the user for permission if necessary.
|
||||
// Note browsers may not show this permission request
|
||||
// if it did not originate from a user action.
|
||||
// desktop_notification_bell = false
|
||||
|
||||
// [string] The background color for text with no other color attributes.
|
||||
// background_color = "rgb(16, 16, 16)"
|
||||
|
||||
// [string] CSS value of the background image. Empty string for no image.
|
||||
// For example:
|
||||
// "url(https://goo.gl/anedTK) linear-gradient(top bottom, blue, red)"
|
||||
// background_image = ""
|
||||
|
||||
// [string] CSS value of the background image size. Defaults to none.
|
||||
// background_size = ""
|
||||
|
||||
// [string] CSS value of the background image position.
|
||||
// For example:
|
||||
// "10% 10% center"
|
||||
// background_position = ""
|
||||
|
||||
// [bool] If true, the backspace should send BS ('\x08', aka ^H). Otherwise the backspace key should send '\x7f'.
|
||||
// backspace_sends_backspace = false
|
||||
|
||||
// [map[string]map[string]string]
|
||||
// A nested map where each property is the character set code and the value is a map that is a sparse array itself.
|
||||
// In that sparse array, each property is the received character and the value is the displayed character.
|
||||
// For example:
|
||||
// {"0" = {"+" = "\u2192"
|
||||
// "," = "\u2190"
|
||||
// "-" = "\u2191"
|
||||
// "." = "\u2193"
|
||||
// "0" = "\u2588"}}
|
||||
// character_map_overrides = null
|
||||
|
||||
// [bool] Whether or not to close the window when the command exits.
|
||||
// close_on_exit = true
|
||||
|
||||
// [bool] Whether or not to blink the cursor by default.
|
||||
// cursor_blink = false
|
||||
|
||||
// [2[int]] The cursor blink rate in milliseconds.
|
||||
// A two element array, the first of which is how long the cursor should be on, second is how long it should be off.
|
||||
// cursor_blink_cycle = [1000, 500]
|
||||
|
||||
// [string] The color of the visible cursor.
|
||||
// cursor_color = "rgba(255, 0, 0, 0.5)"
|
||||
|
||||
// [[]string]
|
||||
// Override colors in the default palette.
|
||||
// This can be specified as an array or an object.
|
||||
// Values can be specified as almost any css color value.
|
||||
// This includes #RGB, #RRGGBB, rgb(...), rgba(...), and any color names that are also part of the stock X11 rgb.txt file.
|
||||
// You can use 'null' to specify that the default value should be not be changed.
|
||||
// This is useful for skipping a small number of indicies when the value is specified as an array.
|
||||
// color_palette_overrides = null
|
||||
|
||||
// [bool] Automatically copy mouse selection to the clipboard.
|
||||
// copy_on_select = true
|
||||
|
||||
// [bool] Whether to use the default window copy behaviour
|
||||
// use_default_window_copy = false
|
||||
|
||||
// [bool] Whether to clear the selection after copying.
|
||||
// clear_selection_after_copy = true
|
||||
|
||||
// [bool] If true, Ctrl-Plus/Minus/Zero controls zoom.
|
||||
// If false, Ctrl-Shift-Plus/Minus/Zero controls zoom, Ctrl-Minus sends ^_, Ctrl-Plus/Zero do nothing.
|
||||
// ctrl_plus_minus_zero_zoom = true
|
||||
|
||||
// [bool] Ctrl+C copies if true, send ^C to host if false.
|
||||
// Ctrl+Shift+C sends ^C to host if true, copies if false.
|
||||
// ctrl_c_copy = false
|
||||
|
||||
// [bool] Ctrl+V pastes if true, send ^V to host if false.
|
||||
// Ctrl+Shift+V sends ^V to host if true, pastes if false.
|
||||
// ctrl_v_paste = false
|
||||
|
||||
// [bool] Set whether East Asian Ambiguous characters have two column width.
|
||||
// east_asian_ambiguous_as_two_column = false
|
||||
|
||||
// [bool] True to enable 8-bit control characters, false to ignore them.
|
||||
// We'll respect the two-byte versions of these control characters regardless of this setting.
|
||||
// enable_8_bit_control = false
|
||||
|
||||
// [enum(null, true, false)]
|
||||
// True if we should use bold weight font for text with the bold/bright attribute.
|
||||
// False to use the normal weight font.
|
||||
// Null to autodetect.
|
||||
// enable_bold = null
|
||||
|
||||
// [bool] True if we should use bright colors (8-15 on a 16 color palette) for any text with the bold attribute.
|
||||
// False otherwise.
|
||||
// enable_bold_as_bright = true
|
||||
|
||||
// [bool] Show a message in the terminal when the host writes to the clipboard.
|
||||
// enable_clipboard_notice = true
|
||||
|
||||
// [bool] Allow the host to write directly to the system clipboard.
|
||||
// enable_clipboard_write = true
|
||||
|
||||
// [bool] Respect the host's attempt to change the cursor blink status using DEC Private Mode 12.
|
||||
// enable_dec12 = false
|
||||
|
||||
// [map[string]string] The default environment variables, as an object.
|
||||
// environment = {"TERM" = "xterm-256color"}
|
||||
|
||||
// [string] Default font family for the terminal text.
|
||||
// font_family = "'DejaVu Sans Mono', 'Everson Mono', FreeMono, 'Menlo', 'Terminal', monospace"
|
||||
|
||||
// [int] The default font size in pixels.
|
||||
// font_size = 15
|
||||
|
||||
// [string] CSS font-smoothing property.
|
||||
// font_smoothing = "antialiased"
|
||||
|
||||
// [string] The foreground color for text with no other color attributes.
|
||||
// foreground_color = "rgb(240, 240, 240)"
|
||||
|
||||
// [bool] If true, home/end will control the terminal scrollbar and shift home/end will send the VT keycodes.
|
||||
// If false then home/end sends VT codes and shift home/end scrolls.
|
||||
// home_keys_scroll = false
|
||||
|
||||
// [map[string]string]
|
||||
// A map of key sequence to key actions.
|
||||
// Key sequences include zero or more modifier keys followed by a key code.
|
||||
// Key codes can be decimal or hexadecimal numbers, or a key identifier.
|
||||
// Key actions can be specified a string to send to the host, or an action identifier.
|
||||
// For a full list of key code and action identifiers, see https://goo.gl/8AoD09.
|
||||
// Sample keybindings:
|
||||
// {"Ctrl-Alt-K" = "clearScrollback"
|
||||
// "Ctrl-Shift-L"= "PASS"
|
||||
// "Ctrl-H" = "'HELLO\n'"}
|
||||
// keybindings = null
|
||||
|
||||
// [int] Max length of a DCS, OSC, PM, or APS sequence before we give up and ignore the code.
|
||||
// max_string_sequence = 100000
|
||||
|
||||
// [bool] If true, convert media keys to their Fkey equivalent.
|
||||
// If false, let the browser handle the keys.
|
||||
// media_keys_are_fkeys = false
|
||||
|
||||
// [bool] Set whether the meta key sends a leading escape or not.
|
||||
// meta_sends_escape = true
|
||||
|
||||
// [enum(null, 0, 1, 2, 3, 4, 5, 6]
|
||||
// Mouse paste button, or null to autodetect.
|
||||
// For autodetect, we'll try to enable middle button paste for non-X11 platforms.
|
||||
// On X11 we move it to button 3.
|
||||
// mouse_paste_button = null
|
||||
|
||||
// [bool] If true, page up/down will control the terminal scrollbar and shift page up/down will send the VT keycodes.
|
||||
// If false then page up/down sends VT codes and shift page up/down scrolls.
|
||||
// page_keys_scroll = false
|
||||
|
||||
// [enum(null, true, false)]
|
||||
// Set whether we should pass Alt-1..9 to the browser.
|
||||
// This is handy when running hterm in a browser tab, so that you don't lose Chrome's "switch to tab" keyboard accelerators.
|
||||
// When not running in a tab it's better to send these keys to the host so they can be used in vim or emacs.
|
||||
// If true, Alt-1..9 will be handled by the browser.
|
||||
// If false, Alt-1..9 will be sent to the host.
|
||||
// If null, autodetect based on browser platform and window type.
|
||||
// pass_alt_number = null
|
||||
|
||||
// [enum(null, true, false)]
|
||||
// Set whether we should pass Ctrl-1..9 to the browser.
|
||||
// This is handy when running hterm in a browser tab, so that you don't lose Chrome's "switch to tab" keyboard accelerators.
|
||||
// When not running in a tab it's better to send these keys to the host so they can be used in vim or emacs.
|
||||
// If true, Ctrl-1..9 will be handled by the browser.
|
||||
// If false, Ctrl-1..9 will be sent to the host.
|
||||
// If null, autodetect based on browser platform and window type.
|
||||
// pass_ctrl_number = null
|
||||
|
||||
// [enum(null, true, false)]
|
||||
// Set whether we should pass Meta-1..9 to the browser.
|
||||
// This is handy when running hterm in a browser tab, so that you don't lose Chrome's "switch to tab" keyboard accelerators.
|
||||
// When not running in a tab it's better to send these keys to the host so they can be used in vim or emacs.
|
||||
// If true, Meta-1..9 will be handled by the browser.
|
||||
// If false, Meta-1..9 will be sent to the host. If null, autodetect based on browser platform and window type.
|
||||
// pass_meta_number = null
|
||||
|
||||
// [bool] Set whether meta-V gets passed to host.
|
||||
// pass_meta_v = true
|
||||
|
||||
// [bool] If true, scroll to the bottom on any keystroke.
|
||||
// scroll_on_keystroke = true
|
||||
|
||||
// [bool] If true, scroll to the bottom on terminal output.
|
||||
// scroll_on_output = false
|
||||
|
||||
// [bool] The vertical scrollbar mode.
|
||||
// scrollbar_visible = true
|
||||
|
||||
// [int] The multiplier for the pixel delta in mousewheel event caused by the scroll wheel. Alters how fast the page scrolls.
|
||||
// scroll_wheel_move_multiplier = 1
|
||||
|
||||
// [bool] Shift + Insert pastes if true, sent to host if false.
|
||||
// shift_insert_paste = true
|
||||
|
||||
// [string] URL of user stylesheet to include in the terminal document.
|
||||
// user_css = ""
|
||||
|
||||
// }
|
44
CONTRIBUTING.md
Normal file
44
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# How to contribute
|
||||
|
||||
GoTTY is MIT licensed and accepts contributions via GitHub pull requests. We also accepts feature requests on GitHub issues.
|
||||
|
||||
## Reporting a bug
|
||||
|
||||
Reporting a bug is always welcome and one of the best ways to contribute. A good bug report helps the developers to improve the product much easier. We therefore would like to ask you to fill out the quesions on the issue template as much as possible. That helps us to figure out what's happening and discover the root cause.
|
||||
|
||||
|
||||
## Requesting a new feature
|
||||
|
||||
When you find that GoTTY cannot fullfill your requirements because of lack of ability, you may want to open a new feature request. In that case, please file a new issue with your usecase and requirements.
|
||||
|
||||
|
||||
## Opening a pull request
|
||||
|
||||
### Code Style
|
||||
|
||||
Please run `go fmt` on your Go code and make sure that your commits are organized for each logical change and your commit messages are in proper format (see below).
|
||||
|
||||
[Go's official code style guide](https://github.com/golang/go/wiki/CodeReviewComments) is also helpful.
|
||||
|
||||
### Format of the commit message
|
||||
|
||||
When you write a commit message, we recommend include following information to make review easier and keep the history cleaerer.
|
||||
|
||||
* What is the change
|
||||
* The reason for the change
|
||||
|
||||
The following is an example:
|
||||
|
||||
```
|
||||
Add something new to existing package
|
||||
|
||||
Since the existing function lacks that mechanism for some purpose,
|
||||
this commit adds a new structure to provide it.
|
||||
```
|
||||
|
||||
When your pull request is to add a new feature, we recommend add an actual usecase so that we can discuss the best way to achive your requirement. Opening a proposal issue in advance is another good way to start discussion of new features.
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
If you have a trivial question about GoTTY for a bug or new feature, you can contact @i_yudai on Twitter (unfortunately, I cannot provide support on GoTTY though).
|
37
Godeps/Godeps.json
generated
37
Godeps/Godeps.json
generated
|
@ -1,33 +1,54 @@
|
|||
{
|
||||
"ImportPath": "github.com/yudai/gotty",
|
||||
"GoVersion": "go1.5",
|
||||
"GoVersion": "go1.9",
|
||||
"GodepVersion": "v79",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/braintree/manners",
|
||||
"Comment": "0.4.0-6-g9e2a271",
|
||||
"Rev": "9e2a2714de21eb092ead2ef56d8c7a60d7928819"
|
||||
"ImportPath": "github.com/NYTimes/gziphandler",
|
||||
"Rev": "967539e5e271a2bc9b3dcb1285078a1b1df105ae"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/codegangsta/cli",
|
||||
"Comment": "1.2.0-139-g142e6cd",
|
||||
"Rev": "142e6cd241a4dfbf7f07a018f1f8225180018da4"
|
||||
"Comment": "v1.19.1",
|
||||
"Rev": "0bdeddeeb0f650497d603c4ad7b20cfe685682f6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/elazarl/go-bindata-assetfs",
|
||||
"Rev": "d5cac425555ca5cf00694df246e04f05e6a55150"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/fatih/structs",
|
||||
"Rev": "a9f7daa9c2729e97450c2da2feda19130a367d8f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/websocket",
|
||||
"Rev": "b6ab76f1fe9803ee1d59e7e5b2a797c1fe897ce5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/hashicorp/go-multierror",
|
||||
"Rev": "56912fb08d85084aa318edcf2bba735b97cf35c5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/kr/pty",
|
||||
"Comment": "release.r56-28-g5cf931e",
|
||||
"Rev": "5cf931ef8f76dccd0910001d74a58a7fca84a83d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/yudai/utf8reader",
|
||||
"Rev": "0ccad3e5e2d8dc2493179319c4c8d1172f583ea4"
|
||||
"ImportPath": "github.com/pkg/errors",
|
||||
"Comment": "v0.8.0-2-g248dadf",
|
||||
"Rev": "248dadf4e9068a0b3e79f02ed0a610d935de5302"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/yudai/hcl",
|
||||
"Rev": "5fa2393b3552119bf33a69adb1402a1160cba23d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/yudai/hcl/hcl",
|
||||
"Rev": "5fa2393b3552119bf33a69adb1402a1160cba23d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/yudai/hcl/json",
|
||||
"Rev": "5fa2393b3552119bf33a69adb1402a1160cba23d"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
2
Godeps/_workspace/.gitignore
generated
vendored
2
Godeps/_workspace/.gitignore
generated
vendored
|
@ -1,2 +0,0 @@
|
|||
/pkg
|
||||
/bin
|
36
Godeps/_workspace/src/github.com/braintree/manners/README.md
generated
vendored
36
Godeps/_workspace/src/github.com/braintree/manners/README.md
generated
vendored
|
@ -1,36 +0,0 @@
|
|||
# Manners
|
||||
|
||||
A *polite* webserver for Go.
|
||||
|
||||
Manners allows you to shut your Go webserver down gracefully, without dropping any requests. It can act as a drop-in replacement for the standard library's http.ListenAndServe function:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
handler := MyHTTPHandler()
|
||||
manners.ListenAndServe(":7000", handler)
|
||||
}
|
||||
```
|
||||
|
||||
Then, when you want to shut the server down:
|
||||
|
||||
```go
|
||||
manners.Close()
|
||||
```
|
||||
|
||||
(Note that this does not block until all the requests are finished. Rather, the call to manners.ListenAndServe will stop blocking when all the requests are finished.)
|
||||
|
||||
Manners ensures that all requests are served by incrementing a WaitGroup when a request comes in and decrementing it when the request finishes.
|
||||
|
||||
If your request handler spawns Goroutines that are not guaranteed to finish with the request, you can ensure they are also completed with the `StartRoutine` and `FinishRoutine` functions on the server.
|
||||
|
||||
### Known Issues
|
||||
|
||||
Manners does not correctly shut down long-lived keepalive connections when issued a shutdown command. Clients on an idle keepalive connection may see a connection reset error rather than a close. See https://github.com/braintree/manners/issues/13 for details.
|
||||
|
||||
### Compatability
|
||||
|
||||
Manners 0.3.0 and above uses standard library functionality introduced in Go 1.3.
|
||||
|
||||
### Installation
|
||||
|
||||
`go get github.com/braintree/manners`
|
118
Godeps/_workspace/src/github.com/braintree/manners/helpers_test.go
generated
vendored
118
Godeps/_workspace/src/github.com/braintree/manners/helpers_test.go
generated
vendored
|
@ -1,118 +0,0 @@
|
|||
package manners
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newServer() *GracefulServer {
|
||||
return NewWithServer(new(http.Server))
|
||||
}
|
||||
|
||||
// a simple step-controllable http client
|
||||
type client struct {
|
||||
tls bool
|
||||
addr net.Addr
|
||||
connected chan error
|
||||
sendrequest chan bool
|
||||
idle chan error
|
||||
idlerelease chan bool
|
||||
closed chan bool
|
||||
}
|
||||
|
||||
func (c *client) Run() {
|
||||
go func() {
|
||||
var err error
|
||||
conn, err := net.Dial(c.addr.Network(), c.addr.String())
|
||||
if err != nil {
|
||||
c.connected <- err
|
||||
return
|
||||
}
|
||||
if c.tls {
|
||||
conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
||||
}
|
||||
c.connected <- nil
|
||||
for <-c.sendrequest {
|
||||
_, err = conn.Write([]byte("GET / HTTP/1.1\nHost: localhost:8000\n\n"))
|
||||
if err != nil {
|
||||
c.idle <- err
|
||||
}
|
||||
// Read response; no content
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
// our null handler doesn't send a body, so we know the request is
|
||||
// done when we reach the blank line after the headers
|
||||
if scanner.Text() == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.idle <- scanner.Err()
|
||||
<-c.idlerelease
|
||||
}
|
||||
conn.Close()
|
||||
ioutil.ReadAll(conn)
|
||||
c.closed <- true
|
||||
}()
|
||||
}
|
||||
|
||||
func newClient(addr net.Addr, tls bool) *client {
|
||||
return &client{
|
||||
addr: addr,
|
||||
tls: tls,
|
||||
connected: make(chan error),
|
||||
sendrequest: make(chan bool),
|
||||
idle: make(chan error),
|
||||
idlerelease: make(chan bool),
|
||||
closed: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// a handler that returns 200 ok with no body
|
||||
var nullHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
func startGenericServer(t *testing.T, server *GracefulServer, statechanged chan http.ConnState, runner func() error) (l net.Listener, errc chan error) {
|
||||
server.Addr = "localhost:0"
|
||||
server.Handler = nullHandler
|
||||
if statechanged != nil {
|
||||
// Wrap the ConnState handler with something that will notify
|
||||
// the statechanged channel when a state change happens
|
||||
server.ConnState = func(conn net.Conn, newState http.ConnState) {
|
||||
statechanged <- newState
|
||||
}
|
||||
}
|
||||
|
||||
server.up = make(chan net.Listener)
|
||||
exitchan := make(chan error)
|
||||
|
||||
go func() {
|
||||
exitchan <- runner()
|
||||
}()
|
||||
|
||||
// wait for server socket to be bound
|
||||
select {
|
||||
case l = <-server.up:
|
||||
// all good
|
||||
|
||||
case err := <-exitchan:
|
||||
// all bad
|
||||
t.Fatal("Server failed to start", err)
|
||||
}
|
||||
return l, exitchan
|
||||
}
|
||||
|
||||
func startServer(t *testing.T, server *GracefulServer, statechanged chan http.ConnState) (
|
||||
l net.Listener, errc chan error) {
|
||||
return startGenericServer(t, server, statechanged, server.ListenAndServe)
|
||||
}
|
||||
|
||||
func startTLSServer(t *testing.T, server *GracefulServer, certFile, keyFile string, statechanged chan http.ConnState) (l net.Listener, errc chan error) {
|
||||
runner := func() error {
|
||||
return server.ListenAndServeTLS(certFile, keyFile)
|
||||
}
|
||||
|
||||
return startGenericServer(t, server, statechanged, runner)
|
||||
}
|
7
Godeps/_workspace/src/github.com/braintree/manners/interfaces.go
generated
vendored
7
Godeps/_workspace/src/github.com/braintree/manners/interfaces.go
generated
vendored
|
@ -1,7 +0,0 @@
|
|||
package manners
|
||||
|
||||
type waitGroup interface {
|
||||
Add(int)
|
||||
Done()
|
||||
Wait()
|
||||
}
|
228
Godeps/_workspace/src/github.com/braintree/manners/server.go
generated
vendored
228
Godeps/_workspace/src/github.com/braintree/manners/server.go
generated
vendored
|
@ -1,228 +0,0 @@
|
|||
/*
|
||||
Package manners provides a wrapper for a standard net/http server that
|
||||
ensures all active HTTP client have completed their current request
|
||||
before the server shuts down.
|
||||
|
||||
It can be used a drop-in replacement for the standard http package,
|
||||
or can wrap a pre-configured Server.
|
||||
|
||||
eg.
|
||||
|
||||
http.Handle("/hello", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Hello\n"))
|
||||
})
|
||||
|
||||
log.Fatal(manners.ListenAndServe(":8080", nil))
|
||||
|
||||
or for a customized server:
|
||||
|
||||
s := manners.NewWithServer(&http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: myHandler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
})
|
||||
log.Fatal(s.ListenAndServe())
|
||||
|
||||
The server will shut down cleanly when the Close() method is called:
|
||||
|
||||
go func() {
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, os.Interrupt, os.Kill)
|
||||
<-sigchan
|
||||
log.Info("Shutting down...")
|
||||
manners.Close()
|
||||
}()
|
||||
|
||||
http.Handle("/hello", myHandler)
|
||||
log.Fatal(manners.ListenAndServe(":8080", nil))
|
||||
*/
|
||||
package manners
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// A GracefulServer maintains a WaitGroup that counts how many in-flight
|
||||
// requests the server is handling. When it receives a shutdown signal,
|
||||
// it stops accepting new requests but does not actually shut down until
|
||||
// all in-flight requests terminate.
|
||||
//
|
||||
// GracefulServer embeds the underlying net/http.Server making its non-override
|
||||
// methods and properties avaiable.
|
||||
//
|
||||
// It must be initialized by calling NewWithServer.
|
||||
type GracefulServer struct {
|
||||
*http.Server
|
||||
|
||||
shutdown chan bool
|
||||
wg waitGroup
|
||||
|
||||
lcsmu sync.RWMutex
|
||||
lastConnState map[net.Conn]http.ConnState
|
||||
|
||||
up chan net.Listener // Only used by test code.
|
||||
}
|
||||
|
||||
// NewWithServer wraps an existing http.Server object and returns a
|
||||
// GracefulServer that supports all of the original Server operations.
|
||||
func NewWithServer(s *http.Server) *GracefulServer {
|
||||
return &GracefulServer{
|
||||
Server: s,
|
||||
shutdown: make(chan bool),
|
||||
wg: new(sync.WaitGroup),
|
||||
lastConnState: make(map[net.Conn]http.ConnState),
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the server from accepting new requets and begins shutting down.
|
||||
// It returns true if it's the first time Close is called.
|
||||
func (s *GracefulServer) Close() bool {
|
||||
return <-s.shutdown
|
||||
}
|
||||
|
||||
// ListenAndServe provides a graceful equivalent of net/http.Serve.ListenAndServe.
|
||||
func (s *GracefulServer) ListenAndServe() error {
|
||||
addr := s.Addr
|
||||
if addr == "" {
|
||||
addr = ":http"
|
||||
}
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Serve(listener)
|
||||
}
|
||||
|
||||
// ListenAndServeTLS provides a graceful equivalent of net/http.Serve.ListenAndServeTLS.
|
||||
func (s *GracefulServer) ListenAndServeTLS(certFile, keyFile string) error {
|
||||
// direct lift from net/http/server.go
|
||||
addr := s.Addr
|
||||
if addr == "" {
|
||||
addr = ":https"
|
||||
}
|
||||
config := &tls.Config{}
|
||||
if s.TLSConfig != nil {
|
||||
*config = *s.TLSConfig
|
||||
}
|
||||
if config.NextProtos == nil {
|
||||
config.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
var err error
|
||||
config.Certificates = make([]tls.Certificate, 1)
|
||||
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Serve(tls.NewListener(ln, config))
|
||||
}
|
||||
|
||||
// Serve provides a graceful equivalent net/http.Server.Serve.
|
||||
func (s *GracefulServer) Serve(listener net.Listener) error {
|
||||
var closing int32
|
||||
|
||||
go func() {
|
||||
s.shutdown <- true
|
||||
close(s.shutdown)
|
||||
atomic.StoreInt32(&closing, 1)
|
||||
s.Server.SetKeepAlivesEnabled(false)
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
originalConnState := s.Server.ConnState
|
||||
|
||||
// s.ConnState is invoked by the net/http.Server every time a connectiion
|
||||
// changes state. It keeps track of each connection's state over time,
|
||||
// enabling manners to handle persisted connections correctly.
|
||||
s.ConnState = func(conn net.Conn, newState http.ConnState) {
|
||||
s.lcsmu.RLock()
|
||||
lastConnState := s.lastConnState[conn]
|
||||
s.lcsmu.RUnlock()
|
||||
|
||||
switch newState {
|
||||
|
||||
// New connection -> StateNew
|
||||
case http.StateNew:
|
||||
s.StartRoutine()
|
||||
|
||||
// (StateNew, StateIdle) -> StateActive
|
||||
case http.StateActive:
|
||||
// The connection transitioned from idle back to active
|
||||
if lastConnState == http.StateIdle {
|
||||
s.StartRoutine()
|
||||
}
|
||||
|
||||
// StateActive -> StateIdle
|
||||
// Immediately close newly idle connections; if not they may make
|
||||
// one more request before SetKeepAliveEnabled(false) takes effect.
|
||||
case http.StateIdle:
|
||||
if atomic.LoadInt32(&closing) == 1 {
|
||||
conn.Close()
|
||||
}
|
||||
s.FinishRoutine()
|
||||
|
||||
// (StateNew, StateActive, StateIdle) -> (StateClosed, StateHiJacked)
|
||||
// If the connection was idle we do not need to decrement the counter.
|
||||
case http.StateClosed, http.StateHijacked:
|
||||
if lastConnState != http.StateIdle {
|
||||
s.FinishRoutine()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
s.lcsmu.Lock()
|
||||
if newState == http.StateClosed || newState == http.StateHijacked {
|
||||
delete(s.lastConnState, conn)
|
||||
} else {
|
||||
s.lastConnState[conn] = newState
|
||||
}
|
||||
s.lcsmu.Unlock()
|
||||
|
||||
if originalConnState != nil {
|
||||
originalConnState(conn, newState)
|
||||
}
|
||||
}
|
||||
|
||||
// A hook to allow the server to notify others when it is ready to receive
|
||||
// requests; only used by tests.
|
||||
if s.up != nil {
|
||||
s.up <- listener
|
||||
}
|
||||
|
||||
err := s.Server.Serve(listener)
|
||||
|
||||
// This block is reached when the server has received a shut down command
|
||||
// or a real error happened.
|
||||
if err == nil || atomic.LoadInt32(&closing) == 1 {
|
||||
s.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// StartRoutine increments the server's WaitGroup. Use this if a web request
|
||||
// starts more goroutines and these goroutines are not guaranteed to finish
|
||||
// before the request.
|
||||
func (s *GracefulServer) StartRoutine() {
|
||||
s.wg.Add(1)
|
||||
}
|
||||
|
||||
// FinishRoutine decrements the server's WaitGroup. Use this to complement
|
||||
// StartRoutine().
|
||||
func (s *GracefulServer) FinishRoutine() {
|
||||
s.wg.Done()
|
||||
}
|
243
Godeps/_workspace/src/github.com/braintree/manners/server_test.go
generated
vendored
243
Godeps/_workspace/src/github.com/braintree/manners/server_test.go
generated
vendored
|
@ -1,243 +0,0 @@
|
|||
package manners
|
||||
|
||||
import (
|
||||
helpers "github.com/braintree/manners/test_helpers"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Tests that the server allows in-flight requests to complete
|
||||
// before shutting down.
|
||||
func TestGracefulness(t *testing.T) {
|
||||
server := newServer()
|
||||
wg := helpers.NewWaitGroup()
|
||||
server.wg = wg
|
||||
statechanged := make(chan http.ConnState)
|
||||
listener, exitchan := startServer(t, server, statechanged)
|
||||
|
||||
client := newClient(listener.Addr(), false)
|
||||
client.Run()
|
||||
|
||||
// wait for client to connect, but don't let it send the request yet
|
||||
if err := <-client.connected; err != nil {
|
||||
t.Fatal("Client failed to connect to server", err)
|
||||
}
|
||||
// avoid a race between the client connection and the server accept
|
||||
if state := <-statechanged; state != http.StateNew {
|
||||
t.Fatal("Unexpected state", state)
|
||||
}
|
||||
|
||||
server.Close()
|
||||
|
||||
waiting := <-wg.WaitCalled
|
||||
if waiting < 1 {
|
||||
t.Errorf("Expected the waitgroup to equal 1 at shutdown; actually %d", waiting)
|
||||
}
|
||||
|
||||
// allow the client to finish sending the request and make sure the server exits after
|
||||
// (client will be in connected but idle state at that point)
|
||||
client.sendrequest <- true
|
||||
close(client.sendrequest)
|
||||
if err := <-exitchan; err != nil {
|
||||
t.Error("Unexpected error during shutdown", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that the server begins to shut down when told to and does not accept
|
||||
// new requests once shutdown has begun
|
||||
func TestShutdown(t *testing.T) {
|
||||
server := newServer()
|
||||
wg := helpers.NewWaitGroup()
|
||||
server.wg = wg
|
||||
statechanged := make(chan http.ConnState)
|
||||
listener, exitchan := startServer(t, server, statechanged)
|
||||
|
||||
client1 := newClient(listener.Addr(), false)
|
||||
client1.Run()
|
||||
|
||||
// wait for client1 to connect
|
||||
if err := <-client1.connected; err != nil {
|
||||
t.Fatal("Client failed to connect to server", err)
|
||||
}
|
||||
// avoid a race between the client connection and the server accept
|
||||
if state := <-statechanged; state != http.StateNew {
|
||||
t.Fatal("Unexpected state", state)
|
||||
}
|
||||
|
||||
// start the shutdown; once it hits waitgroup.Wait()
|
||||
// the listener should of been closed, though client1 is still connected
|
||||
if server.Close() != true {
|
||||
t.Fatal("first call to Close returned false")
|
||||
}
|
||||
if server.Close() != false {
|
||||
t.Fatal("second call to Close returned true")
|
||||
}
|
||||
|
||||
waiting := <-wg.WaitCalled
|
||||
if waiting != 1 {
|
||||
t.Errorf("Waitcount should be one, got %d", waiting)
|
||||
}
|
||||
|
||||
// should get connection refused at this point
|
||||
client2 := newClient(listener.Addr(), false)
|
||||
client2.Run()
|
||||
|
||||
if err := <-client2.connected; err == nil {
|
||||
t.Fatal("client2 connected when it should of received connection refused")
|
||||
}
|
||||
|
||||
// let client1 finish so the server can exit
|
||||
close(client1.sendrequest) // don't bother sending an actual request
|
||||
|
||||
<-exitchan
|
||||
}
|
||||
|
||||
// Test that a connection is closed upon reaching an idle state if and only if the server
|
||||
// is shutting down.
|
||||
func TestCloseOnIdle(t *testing.T) {
|
||||
server := newServer()
|
||||
wg := helpers.NewWaitGroup()
|
||||
server.wg = wg
|
||||
fl := helpers.NewListener()
|
||||
runner := func() error {
|
||||
return server.Serve(fl)
|
||||
}
|
||||
|
||||
startGenericServer(t, server, nil, runner)
|
||||
|
||||
// Change to idle state while server is not closing; Close should not be called
|
||||
conn := &helpers.Conn{}
|
||||
server.ConnState(conn, http.StateIdle)
|
||||
if conn.CloseCalled {
|
||||
t.Error("Close was called unexpected")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
|
||||
// wait until the server calls Close() on the listener
|
||||
// by that point the atomic closing variable will have been updated, avoiding a race.
|
||||
<-fl.CloseCalled
|
||||
|
||||
conn = &helpers.Conn{}
|
||||
server.ConnState(conn, http.StateIdle)
|
||||
if !conn.CloseCalled {
|
||||
t.Error("Close was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func waitForState(t *testing.T, waiter chan http.ConnState, state http.ConnState, errmsg string) {
|
||||
for {
|
||||
select {
|
||||
case ns := <-waiter:
|
||||
if ns == state {
|
||||
return
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal(errmsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a request moving from active->idle->active using an actual
|
||||
// network connection still results in a corect shutdown
|
||||
func TestStateTransitionActiveIdleActive(t *testing.T) {
|
||||
server := newServer()
|
||||
wg := helpers.NewWaitGroup()
|
||||
statechanged := make(chan http.ConnState)
|
||||
server.wg = wg
|
||||
listener, exitchan := startServer(t, server, statechanged)
|
||||
|
||||
client := newClient(listener.Addr(), false)
|
||||
client.Run()
|
||||
|
||||
// wait for client to connect, but don't let it send the request
|
||||
if err := <-client.connected; err != nil {
|
||||
t.Fatal("Client failed to connect to server", err)
|
||||
}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
client.sendrequest <- true
|
||||
waitForState(t, statechanged, http.StateActive, "Client failed to reach active state")
|
||||
<-client.idle
|
||||
client.idlerelease <- true
|
||||
waitForState(t, statechanged, http.StateIdle, "Client failed to reach idle state")
|
||||
}
|
||||
|
||||
// client is now in an idle state
|
||||
|
||||
server.Close()
|
||||
waiting := <-wg.WaitCalled
|
||||
if waiting != 0 {
|
||||
t.Errorf("Waitcount should be zero, got %d", waiting)
|
||||
}
|
||||
|
||||
if err := <-exitchan; err != nil {
|
||||
t.Error("Unexpected error during shutdown", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test state transitions from new->active->-idle->closed using an actual
|
||||
// network connection and make sure the waitgroup count is correct at the end.
|
||||
func TestStateTransitionActiveIdleClosed(t *testing.T) {
|
||||
var (
|
||||
listener net.Listener
|
||||
exitchan chan error
|
||||
)
|
||||
|
||||
keyFile, err1 := helpers.NewTempFile(helpers.Key)
|
||||
certFile, err2 := helpers.NewTempFile(helpers.Cert)
|
||||
defer keyFile.Unlink()
|
||||
defer certFile.Unlink()
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
t.Fatal("Failed to create temporary files", err1, err2)
|
||||
}
|
||||
|
||||
for _, withTLS := range []bool{false, true} {
|
||||
server := newServer()
|
||||
wg := helpers.NewWaitGroup()
|
||||
statechanged := make(chan http.ConnState)
|
||||
server.wg = wg
|
||||
if withTLS {
|
||||
listener, exitchan = startTLSServer(t, server, certFile.Name(), keyFile.Name(), statechanged)
|
||||
} else {
|
||||
listener, exitchan = startServer(t, server, statechanged)
|
||||
}
|
||||
|
||||
client := newClient(listener.Addr(), withTLS)
|
||||
client.Run()
|
||||
|
||||
// wait for client to connect, but don't let it send the request
|
||||
if err := <-client.connected; err != nil {
|
||||
t.Fatal("Client failed to connect to server", err)
|
||||
}
|
||||
|
||||
client.sendrequest <- true
|
||||
waitForState(t, statechanged, http.StateActive, "Client failed to reach active state")
|
||||
|
||||
err := <-client.idle
|
||||
if err != nil {
|
||||
t.Fatalf("tls=%t unexpected error from client %s", withTLS, err)
|
||||
}
|
||||
|
||||
client.idlerelease <- true
|
||||
waitForState(t, statechanged, http.StateIdle, "Client failed to reach idle state")
|
||||
|
||||
// client is now in an idle state
|
||||
close(client.sendrequest)
|
||||
<-client.closed
|
||||
waitForState(t, statechanged, http.StateClosed, "Client failed to reach closed state")
|
||||
|
||||
server.Close()
|
||||
waiting := <-wg.WaitCalled
|
||||
if waiting != 0 {
|
||||
t.Errorf("Waitcount should be zero, got %d", waiting)
|
||||
}
|
||||
|
||||
if err := <-exitchan; err != nil {
|
||||
t.Error("Unexpected error during shutdown", err)
|
||||
}
|
||||
}
|
||||
}
|
35
Godeps/_workspace/src/github.com/braintree/manners/static.go
generated
vendored
35
Godeps/_workspace/src/github.com/braintree/manners/static.go
generated
vendored
|
@ -1,35 +0,0 @@
|
|||
package manners
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var defaultServer *GracefulServer
|
||||
|
||||
// ListenAndServe provides a graceful version of the function provided by the
|
||||
// net/http package. Call Close() to stop the server.
|
||||
func ListenAndServe(addr string, handler http.Handler) error {
|
||||
defaultServer = NewWithServer(&http.Server{Addr: addr, Handler: handler})
|
||||
return defaultServer.ListenAndServe()
|
||||
}
|
||||
|
||||
// ListenAndServeTLS provides a graceful version of the function provided by the
|
||||
// net/http package. Call Close() to stop the server.
|
||||
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler http.Handler) error {
|
||||
defaultServer = NewWithServer(&http.Server{Addr: addr, Handler: handler})
|
||||
return defaultServer.ListenAndServeTLS(certFile, keyFile)
|
||||
}
|
||||
|
||||
// Serve provides a graceful version of the function provided by the net/http
|
||||
// package. Call Close() to stop the server.
|
||||
func Serve(l net.Listener, handler http.Handler) error {
|
||||
defaultServer = NewWithServer(&http.Server{Handler: handler})
|
||||
return defaultServer.Serve(l)
|
||||
}
|
||||
|
||||
// Shuts down the default server used by ListenAndServe, ListenAndServeTLS and
|
||||
// Serve. It returns true if it's the first time Close is called.
|
||||
func Close() bool {
|
||||
return defaultServer.Close()
|
||||
}
|
29
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/certs.go
generated
vendored
29
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/certs.go
generated
vendored
|
@ -1,29 +0,0 @@
|
|||
package test_helpers
|
||||
|
||||
// A PEM-encoded TLS cert with SAN IPs "127.0.0.1" and "[::1]", expiring at the
|
||||
// last second of 2049 (the end of ASN.1 time).
|
||||
|
||||
// generated from src/pkg/crypto/tls:
|
||||
// go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
||||
var (
|
||||
Cert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIBdzCCASOgAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD
|
||||
bzAeFw03MDAxMDEwMDAwMDBaFw00OTEyMzEyMzU5NTlaMBIxEDAOBgNVBAoTB0Fj
|
||||
bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAN55NcYKZeInyTuhcCwFMhDHCmwa
|
||||
IUSdtXdcbItRB/yfXGBhiex00IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEA
|
||||
AaNoMGYwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
|
||||
EwEB/wQFMAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAA
|
||||
AAAAAAAAAAAAAAEwCwYJKoZIhvcNAQEFA0EAAoQn/ytgqpiLcZu9XKbCJsJcvkgk
|
||||
Se6AbGXgSlq+ZCEVo0qIwSgeBqmsJxUu7NCSOwVJLYNEBO2DtIxoYVk+MA==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
Key = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBPAIBAAJBAN55NcYKZeInyTuhcCwFMhDHCmwaIUSdtXdcbItRB/yfXGBhiex0
|
||||
0IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEAAQJBAQdUx66rfh8sYsgfdcvV
|
||||
NoafYpnEcB5s4m/vSVe6SU7dCK6eYec9f9wpT353ljhDUHq3EbmE4foNzJngh35d
|
||||
AekCIQDhRQG5Li0Wj8TM4obOnnXUXf1jRv0UkzE9AHWLG5q3AwIhAPzSjpYUDjVW
|
||||
MCUXgckTpKCuGwbJk7424Nb8bLzf3kllAiA5mUBgjfr/WtFSJdWcPQ4Zt9KTMNKD
|
||||
EUO0ukpTwEIl6wIhAMbGqZK3zAAFdq8DD2jPx+UJXnh0rnOkZBzDtJ6/iN69AiEA
|
||||
1Aq8MJgTaYsDQWyU/hDq5YkDJc9e9DSCvUIzqxQWMQE=
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
)
|
13
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/conn.go
generated
vendored
13
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/conn.go
generated
vendored
|
@ -1,13 +0,0 @@
|
|||
package test_helpers
|
||||
|
||||
import "net"
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
CloseCalled bool
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.CloseCalled = true
|
||||
return nil
|
||||
}
|
34
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/listener.go
generated
vendored
34
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/listener.go
generated
vendored
|
@ -1,34 +0,0 @@
|
|||
package test_helpers
|
||||
|
||||
import (
|
||||
"net"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
AcceptRelease chan bool
|
||||
CloseCalled chan bool
|
||||
}
|
||||
|
||||
func NewListener() *Listener {
|
||||
return &Listener{
|
||||
make(chan bool, 1),
|
||||
make(chan bool, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Addr() net.Addr {
|
||||
addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
|
||||
return addr
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
l.CloseCalled <- true
|
||||
l.AcceptRelease <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) Accept() (net.Conn, error) {
|
||||
<-l.AcceptRelease
|
||||
return nil, errors.New("connection closed")
|
||||
}
|
27
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/temp_file.go
generated
vendored
27
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/temp_file.go
generated
vendored
|
@ -1,27 +0,0 @@
|
|||
package test_helpers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
type TempFile struct {
|
||||
*os.File
|
||||
}
|
||||
|
||||
func NewTempFile(content []byte) (*TempFile, error) {
|
||||
f, err := ioutil.TempFile("", "graceful-test")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.Write(content)
|
||||
return &TempFile{f}, nil
|
||||
}
|
||||
|
||||
func (tf *TempFile) Unlink() {
|
||||
if tf.File != nil {
|
||||
os.Remove(tf.Name())
|
||||
tf.File = nil
|
||||
}
|
||||
}
|
33
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/wait_group.go
generated
vendored
33
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/wait_group.go
generated
vendored
|
@ -1,33 +0,0 @@
|
|||
package test_helpers
|
||||
|
||||
import "sync"
|
||||
|
||||
type WaitGroup struct {
|
||||
sync.Mutex
|
||||
Count int
|
||||
WaitCalled chan int
|
||||
}
|
||||
|
||||
func NewWaitGroup() *WaitGroup {
|
||||
return &WaitGroup{
|
||||
WaitCalled: make(chan int, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (wg *WaitGroup) Add(delta int) {
|
||||
wg.Lock()
|
||||
wg.Count++
|
||||
wg.Unlock()
|
||||
}
|
||||
|
||||
func (wg *WaitGroup) Done() {
|
||||
wg.Lock()
|
||||
wg.Count--
|
||||
wg.Unlock()
|
||||
}
|
||||
|
||||
func (wg *WaitGroup) Wait() {
|
||||
wg.Lock()
|
||||
wg.WaitCalled <- wg.Count
|
||||
wg.Unlock()
|
||||
}
|
54
Godeps/_workspace/src/github.com/braintree/manners/transition_test.go
generated
vendored
54
Godeps/_workspace/src/github.com/braintree/manners/transition_test.go
generated
vendored
|
@ -1,54 +0,0 @@
|
|||
package manners
|
||||
|
||||
import (
|
||||
helpers "github.com/braintree/manners/test_helpers"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStateTransitions(t *testing.T) {
|
||||
tests := []transitionTest{
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateActive}, 1},
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateClosed}, 0},
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateClosed}, 0},
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateHijacked}, 0},
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle}, 0},
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive}, 1},
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateIdle}, 0},
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateClosed}, 0},
|
||||
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateIdle, http.StateClosed}, 0},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testStateTransition(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
type transitionTest struct {
|
||||
states []http.ConnState
|
||||
expectedWgCount int
|
||||
}
|
||||
|
||||
func testStateTransition(t *testing.T, test transitionTest) {
|
||||
server := newServer()
|
||||
wg := helpers.NewWaitGroup()
|
||||
server.wg = wg
|
||||
startServer(t, server, nil)
|
||||
|
||||
conn := &helpers.Conn{}
|
||||
for _, newState := range test.states {
|
||||
server.ConnState(conn, newState)
|
||||
}
|
||||
|
||||
server.Close()
|
||||
waiting := <-wg.WaitCalled
|
||||
if waiting != test.expectedWgCount {
|
||||
names := make([]string, len(test.states))
|
||||
for i, s := range test.states {
|
||||
names[i] = s.String()
|
||||
}
|
||||
transitions := strings.Join(names, " -> ")
|
||||
t.Errorf("%s - Waitcount should be %d, got %d", transitions, test.expectedWgCount, waiting)
|
||||
}
|
||||
}
|
13
Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml
generated
vendored
13
Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml
generated
vendored
|
@ -1,13 +0,0 @@
|
|||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.0.3
|
||||
- 1.1.2
|
||||
- 1.2.2
|
||||
- 1.3.3
|
||||
- 1.4.2
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test -v ./...
|
21
Godeps/_workspace/src/github.com/codegangsta/cli/LICENSE
generated
vendored
21
Godeps/_workspace/src/github.com/codegangsta/cli/LICENSE
generated
vendored
|
@ -1,21 +0,0 @@
|
|||
Copyright (C) 2013 Jeremy Saenz
|
||||
All Rights Reserved.
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
308
Godeps/_workspace/src/github.com/codegangsta/cli/README.md
generated
vendored
308
Godeps/_workspace/src/github.com/codegangsta/cli/README.md
generated
vendored
|
@ -1,308 +0,0 @@
|
|||
[](https://travis-ci.org/codegangsta/cli)
|
||||
|
||||
# cli.go
|
||||
cli.go is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way.
|
||||
|
||||
You can view the API docs here:
|
||||
http://godoc.org/github.com/codegangsta/cli
|
||||
|
||||
## Overview
|
||||
Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app.
|
||||
|
||||
**This is where cli.go comes into play.** cli.go makes command line programming fun, organized, and expressive!
|
||||
|
||||
## Installation
|
||||
Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html).
|
||||
|
||||
To install `cli.go`, simply run:
|
||||
```
|
||||
$ go get github.com/codegangsta/cli
|
||||
```
|
||||
|
||||
Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used:
|
||||
```
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
One of the philosophies behind cli.go is that an API should be playful and full of discovery. So a cli.go app can be as little as one line of code in `main()`.
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.NewApp().Run(os.Args)
|
||||
}
|
||||
```
|
||||
|
||||
This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation:
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "boom"
|
||||
app.Usage = "make an explosive entrance"
|
||||
app.Action = func(c *cli.Context) {
|
||||
println("boom! I say!")
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
```
|
||||
|
||||
Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below.
|
||||
|
||||
## Example
|
||||
|
||||
Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness!
|
||||
|
||||
Start by creating a directory named `greet`, and within it, add a file, `greet.go` with the following code in it:
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "greet"
|
||||
app.Usage = "fight the loneliness!"
|
||||
app.Action = func(c *cli.Context) {
|
||||
println("Hello friend!")
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
```
|
||||
|
||||
Install our command to the `$GOPATH/bin` directory:
|
||||
|
||||
```
|
||||
$ go install
|
||||
```
|
||||
|
||||
Finally run our new command:
|
||||
|
||||
```
|
||||
$ greet
|
||||
Hello friend!
|
||||
```
|
||||
|
||||
cli.go also generates some bitchass help text:
|
||||
```
|
||||
$ greet help
|
||||
NAME:
|
||||
greet - fight the loneliness!
|
||||
|
||||
USAGE:
|
||||
greet [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
0.0.0
|
||||
|
||||
COMMANDS:
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS
|
||||
--version Shows version information
|
||||
```
|
||||
|
||||
### Arguments
|
||||
You can lookup arguments by calling the `Args` function on `cli.Context`.
|
||||
|
||||
``` go
|
||||
...
|
||||
app.Action = func(c *cli.Context) {
|
||||
println("Hello", c.Args()[0])
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### Flags
|
||||
Setting and querying flags is simple.
|
||||
``` go
|
||||
...
|
||||
app.Flags = []cli.Flag {
|
||||
cli.StringFlag{
|
||||
Name: "lang",
|
||||
Value: "english",
|
||||
Usage: "language for the greeting",
|
||||
},
|
||||
}
|
||||
app.Action = func(c *cli.Context) {
|
||||
name := "someone"
|
||||
if len(c.Args()) > 0 {
|
||||
name = c.Args()[0]
|
||||
}
|
||||
if c.String("lang") == "spanish" {
|
||||
println("Hola", name)
|
||||
} else {
|
||||
println("Hello", name)
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
See full list of flags at http://godoc.org/github.com/codegangsta/cli
|
||||
|
||||
#### Alternate Names
|
||||
|
||||
You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g.
|
||||
|
||||
``` go
|
||||
app.Flags = []cli.Flag {
|
||||
cli.StringFlag{
|
||||
Name: "lang, l",
|
||||
Value: "english",
|
||||
Usage: "language for the greeting",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error.
|
||||
|
||||
#### Values from the Environment
|
||||
|
||||
You can also have the default value set from the environment via `EnvVar`. e.g.
|
||||
|
||||
``` go
|
||||
app.Flags = []cli.Flag {
|
||||
cli.StringFlag{
|
||||
Name: "lang, l",
|
||||
Value: "english",
|
||||
Usage: "language for the greeting",
|
||||
EnvVar: "APP_LANG",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The `EnvVar` may also be given as a comma-delimited "cascade", where the first environment variable that resolves is used as the default.
|
||||
|
||||
``` go
|
||||
app.Flags = []cli.Flag {
|
||||
cli.StringFlag{
|
||||
Name: "lang, l",
|
||||
Value: "english",
|
||||
Usage: "language for the greeting",
|
||||
EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Subcommands
|
||||
|
||||
Subcommands can be defined for a more git-like command line app.
|
||||
```go
|
||||
...
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "add a task to the list",
|
||||
Action: func(c *cli.Context) {
|
||||
println("added task: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "complete",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "complete a task on the list",
|
||||
Action: func(c *cli.Context) {
|
||||
println("completed task: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "template",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "options for task templates",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "add a new template",
|
||||
Action: func(c *cli.Context) {
|
||||
println("new task template: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "remove an existing template",
|
||||
Action: func(c *cli.Context) {
|
||||
println("removed task template: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### Bash Completion
|
||||
|
||||
You can enable completion commands by setting the `EnableBashCompletion`
|
||||
flag on the `App` object. By default, this setting will only auto-complete to
|
||||
show an app's subcommands, but you can write your own completion methods for
|
||||
the App or its subcommands.
|
||||
```go
|
||||
...
|
||||
var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"}
|
||||
app := cli.NewApp()
|
||||
app.EnableBashCompletion = true
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "complete",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "complete a task on the list",
|
||||
Action: func(c *cli.Context) {
|
||||
println("completed task: ", c.Args().First())
|
||||
},
|
||||
BashComplete: func(c *cli.Context) {
|
||||
// This will complete if no args are passed
|
||||
if len(c.Args()) > 0 {
|
||||
return
|
||||
}
|
||||
for _, t := range tasks {
|
||||
fmt.Println(t)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
#### To Enable
|
||||
|
||||
Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while
|
||||
setting the `PROG` variable to the name of your program:
|
||||
|
||||
`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete`
|
||||
|
||||
#### To Distribute
|
||||
|
||||
Copy and modify `autocomplete/bash_autocomplete` to use your program name
|
||||
rather than `$PROG` and have the user copy the file into
|
||||
`/etc/bash_completion.d/` (or automatically install it there if you are
|
||||
distributing a package). Alternatively you can just document that users should
|
||||
source the generic `autocomplete/bash_autocomplete` with `$PROG` set to your
|
||||
program name in their bash configuration.
|
||||
|
||||
## Contribution Guidelines
|
||||
Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch.
|
||||
|
||||
If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together.
|
||||
|
||||
If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out.
|
308
Godeps/_workspace/src/github.com/codegangsta/cli/app.go
generated
vendored
308
Godeps/_workspace/src/github.com/codegangsta/cli/app.go
generated
vendored
|
@ -1,308 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// App is the main structure of a cli application. It is recomended that
|
||||
// an app be created with the cli.NewApp() function
|
||||
type App struct {
|
||||
// The name of the program. Defaults to os.Args[0]
|
||||
Name string
|
||||
// Description of the program.
|
||||
Usage string
|
||||
// Version of the program
|
||||
Version string
|
||||
// List of commands to execute
|
||||
Commands []Command
|
||||
// List of flags to parse
|
||||
Flags []Flag
|
||||
// Boolean to enable bash completion commands
|
||||
EnableBashCompletion bool
|
||||
// Boolean to hide built-in help command
|
||||
HideHelp bool
|
||||
// Boolean to hide built-in version flag
|
||||
HideVersion bool
|
||||
// An action to execute when the bash-completion flag is set
|
||||
BashComplete func(context *Context)
|
||||
// An action to execute before any subcommands are run, but after the context is ready
|
||||
// If a non-nil error is returned, no subcommands are run
|
||||
Before func(context *Context) error
|
||||
// An action to execute after any subcommands are run, but after the subcommand has finished
|
||||
// It is run even if Action() panics
|
||||
After func(context *Context) error
|
||||
// The action to execute when no subcommands are specified
|
||||
Action func(context *Context)
|
||||
// Execute this function if the proper command cannot be found
|
||||
CommandNotFound func(context *Context, command string)
|
||||
// Compilation date
|
||||
Compiled time.Time
|
||||
// List of all authors who contributed
|
||||
Authors []Author
|
||||
// Copyright of the binary if any
|
||||
Copyright string
|
||||
// Name of Author (Note: Use App.Authors, this is deprecated)
|
||||
Author string
|
||||
// Email of Author (Note: Use App.Authors, this is deprecated)
|
||||
Email string
|
||||
// Writer writer to write output to
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
// Tries to find out when this binary was compiled.
|
||||
// Returns the current time if it fails to find it.
|
||||
func compileTime() time.Time {
|
||||
info, err := os.Stat(os.Args[0])
|
||||
if err != nil {
|
||||
return time.Now()
|
||||
}
|
||||
return info.ModTime()
|
||||
}
|
||||
|
||||
// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action.
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
Name: os.Args[0],
|
||||
Usage: "A new cli application",
|
||||
Version: "0.0.0",
|
||||
BashComplete: DefaultAppComplete,
|
||||
Action: helpCommand.Action,
|
||||
Compiled: compileTime(),
|
||||
Writer: os.Stdout,
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination
|
||||
func (a *App) Run(arguments []string) (err error) {
|
||||
if a.Author != "" || a.Email != "" {
|
||||
a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email})
|
||||
}
|
||||
|
||||
// append help to commands
|
||||
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
|
||||
a.Commands = append(a.Commands, helpCommand)
|
||||
if (HelpFlag != BoolFlag{}) {
|
||||
a.appendFlag(HelpFlag)
|
||||
}
|
||||
}
|
||||
|
||||
//append version/help flags
|
||||
if a.EnableBashCompletion {
|
||||
a.appendFlag(BashCompletionFlag)
|
||||
}
|
||||
|
||||
if !a.HideVersion {
|
||||
a.appendFlag(VersionFlag)
|
||||
}
|
||||
|
||||
// parse flags
|
||||
set := flagSet(a.Name, a.Flags)
|
||||
set.SetOutput(ioutil.Discard)
|
||||
err = set.Parse(arguments[1:])
|
||||
nerr := normalizeFlags(a.Flags, set)
|
||||
if nerr != nil {
|
||||
fmt.Fprintln(a.Writer, nerr)
|
||||
context := NewContext(a, set, nil)
|
||||
ShowAppHelp(context)
|
||||
return nerr
|
||||
}
|
||||
context := NewContext(a, set, nil)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(a.Writer, "Incorrect Usage.")
|
||||
fmt.Fprintln(a.Writer)
|
||||
ShowAppHelp(context)
|
||||
return err
|
||||
}
|
||||
|
||||
if checkCompletions(context) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkHelp(context) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkVersion(context) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.After != nil {
|
||||
defer func() {
|
||||
afterErr := a.After(context)
|
||||
if afterErr != nil {
|
||||
if err != nil {
|
||||
err = NewMultiError(err, afterErr)
|
||||
} else {
|
||||
err = afterErr
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if a.Before != nil {
|
||||
err := a.Before(context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
args := context.Args()
|
||||
if args.Present() {
|
||||
name := args.First()
|
||||
c := a.Command(name)
|
||||
if c != nil {
|
||||
return c.Run(context)
|
||||
}
|
||||
}
|
||||
|
||||
// Run default Action
|
||||
a.Action(context)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Another entry point to the cli app, takes care of passing arguments and error handling
|
||||
func (a *App) RunAndExitOnError() {
|
||||
if err := a.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags
|
||||
func (a *App) RunAsSubcommand(ctx *Context) (err error) {
|
||||
// append help to commands
|
||||
if len(a.Commands) > 0 {
|
||||
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
|
||||
a.Commands = append(a.Commands, helpCommand)
|
||||
if (HelpFlag != BoolFlag{}) {
|
||||
a.appendFlag(HelpFlag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append flags
|
||||
if a.EnableBashCompletion {
|
||||
a.appendFlag(BashCompletionFlag)
|
||||
}
|
||||
|
||||
// parse flags
|
||||
set := flagSet(a.Name, a.Flags)
|
||||
set.SetOutput(ioutil.Discard)
|
||||
err = set.Parse(ctx.Args().Tail())
|
||||
nerr := normalizeFlags(a.Flags, set)
|
||||
context := NewContext(a, set, ctx)
|
||||
|
||||
if nerr != nil {
|
||||
fmt.Fprintln(a.Writer, nerr)
|
||||
fmt.Fprintln(a.Writer)
|
||||
if len(a.Commands) > 0 {
|
||||
ShowSubcommandHelp(context)
|
||||
} else {
|
||||
ShowCommandHelp(ctx, context.Args().First())
|
||||
}
|
||||
return nerr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(a.Writer, "Incorrect Usage.")
|
||||
fmt.Fprintln(a.Writer)
|
||||
ShowSubcommandHelp(context)
|
||||
return err
|
||||
}
|
||||
|
||||
if checkCompletions(context) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a.Commands) > 0 {
|
||||
if checkSubcommandHelp(context) {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if checkCommandHelp(ctx, context.Args().First()) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if a.After != nil {
|
||||
defer func() {
|
||||
afterErr := a.After(context)
|
||||
if afterErr != nil {
|
||||
if err != nil {
|
||||
err = NewMultiError(err, afterErr)
|
||||
} else {
|
||||
err = afterErr
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if a.Before != nil {
|
||||
err := a.Before(context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
args := context.Args()
|
||||
if args.Present() {
|
||||
name := args.First()
|
||||
c := a.Command(name)
|
||||
if c != nil {
|
||||
return c.Run(context)
|
||||
}
|
||||
}
|
||||
|
||||
// Run default Action
|
||||
a.Action(context)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the named command on App. Returns nil if the command does not exist
|
||||
func (a *App) Command(name string) *Command {
|
||||
for _, c := range a.Commands {
|
||||
if c.HasName(name) {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) hasFlag(flag Flag) bool {
|
||||
for _, f := range a.Flags {
|
||||
if flag == f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) appendFlag(flag Flag) {
|
||||
if !a.hasFlag(flag) {
|
||||
a.Flags = append(a.Flags, flag)
|
||||
}
|
||||
}
|
||||
|
||||
// Author represents someone who has contributed to a cli project.
|
||||
type Author struct {
|
||||
Name string // The Authors name
|
||||
Email string // The Authors email
|
||||
}
|
||||
|
||||
// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
|
||||
func (a Author) String() string {
|
||||
e := ""
|
||||
if a.Email != "" {
|
||||
e = "<" + a.Email + "> "
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v %v", a.Name, e)
|
||||
}
|
867
Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go
generated
vendored
867
Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go
generated
vendored
|
@ -1,867 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ExampleApp() {
|
||||
// set args for examples sake
|
||||
os.Args = []string{"greet", "--name", "Jeremy"}
|
||||
|
||||
app := NewApp()
|
||||
app.Name = "greet"
|
||||
app.Flags = []Flag{
|
||||
StringFlag{Name: "name", Value: "bob", Usage: "a name to say"},
|
||||
}
|
||||
app.Action = func(c *Context) {
|
||||
fmt.Printf("Hello %v\n", c.String("name"))
|
||||
}
|
||||
app.Author = "Harrison"
|
||||
app.Email = "harrison@lolwut.com"
|
||||
app.Authors = []Author{Author{Name: "Oliver Allen", Email: "oliver@toyshop.com"}}
|
||||
app.Run(os.Args)
|
||||
// Output:
|
||||
// Hello Jeremy
|
||||
}
|
||||
|
||||
func ExampleAppSubcommand() {
|
||||
// set args for examples sake
|
||||
os.Args = []string{"say", "hi", "english", "--name", "Jeremy"}
|
||||
app := NewApp()
|
||||
app.Name = "say"
|
||||
app.Commands = []Command{
|
||||
{
|
||||
Name: "hello",
|
||||
Aliases: []string{"hi"},
|
||||
Usage: "use it to see a description",
|
||||
Description: "This is how we describe hello the function",
|
||||
Subcommands: []Command{
|
||||
{
|
||||
Name: "english",
|
||||
Aliases: []string{"en"},
|
||||
Usage: "sends a greeting in english",
|
||||
Description: "greets someone in english",
|
||||
Flags: []Flag{
|
||||
StringFlag{
|
||||
Name: "name",
|
||||
Value: "Bob",
|
||||
Usage: "Name of the person to greet",
|
||||
},
|
||||
},
|
||||
Action: func(c *Context) {
|
||||
fmt.Println("Hello,", c.String("name"))
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
// Output:
|
||||
// Hello, Jeremy
|
||||
}
|
||||
|
||||
func ExampleAppHelp() {
|
||||
// set args for examples sake
|
||||
os.Args = []string{"greet", "h", "describeit"}
|
||||
|
||||
app := NewApp()
|
||||
app.Name = "greet"
|
||||
app.Flags = []Flag{
|
||||
StringFlag{Name: "name", Value: "bob", Usage: "a name to say"},
|
||||
}
|
||||
app.Commands = []Command{
|
||||
{
|
||||
Name: "describeit",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "use it to see a description",
|
||||
Description: "This is how we describe describeit the function",
|
||||
Action: func(c *Context) {
|
||||
fmt.Printf("i like to describe things")
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Run(os.Args)
|
||||
// Output:
|
||||
// NAME:
|
||||
// describeit - use it to see a description
|
||||
//
|
||||
// USAGE:
|
||||
// command describeit [arguments...]
|
||||
//
|
||||
// DESCRIPTION:
|
||||
// This is how we describe describeit the function
|
||||
}
|
||||
|
||||
func ExampleAppBashComplete() {
|
||||
// set args for examples sake
|
||||
os.Args = []string{"greet", "--generate-bash-completion"}
|
||||
|
||||
app := NewApp()
|
||||
app.Name = "greet"
|
||||
app.EnableBashCompletion = true
|
||||
app.Commands = []Command{
|
||||
{
|
||||
Name: "describeit",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "use it to see a description",
|
||||
Description: "This is how we describe describeit the function",
|
||||
Action: func(c *Context) {
|
||||
fmt.Printf("i like to describe things")
|
||||
},
|
||||
}, {
|
||||
Name: "next",
|
||||
Usage: "next example",
|
||||
Description: "more stuff to see when generating bash completion",
|
||||
Action: func(c *Context) {
|
||||
fmt.Printf("the next example")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
// Output:
|
||||
// describeit
|
||||
// d
|
||||
// next
|
||||
// help
|
||||
// h
|
||||
}
|
||||
|
||||
func TestApp_Run(t *testing.T) {
|
||||
s := ""
|
||||
|
||||
app := NewApp()
|
||||
app.Action = func(c *Context) {
|
||||
s = s + c.Args().First()
|
||||
}
|
||||
|
||||
err := app.Run([]string{"command", "foo"})
|
||||
expect(t, err, nil)
|
||||
err = app.Run([]string{"command", "bar"})
|
||||
expect(t, err, nil)
|
||||
expect(t, s, "foobar")
|
||||
}
|
||||
|
||||
var commandAppTests = []struct {
|
||||
name string
|
||||
expected bool
|
||||
}{
|
||||
{"foobar", true},
|
||||
{"batbaz", true},
|
||||
{"b", true},
|
||||
{"f", true},
|
||||
{"bat", false},
|
||||
{"nothing", false},
|
||||
}
|
||||
|
||||
func TestApp_Command(t *testing.T) {
|
||||
app := NewApp()
|
||||
fooCommand := Command{Name: "foobar", Aliases: []string{"f"}}
|
||||
batCommand := Command{Name: "batbaz", Aliases: []string{"b"}}
|
||||
app.Commands = []Command{
|
||||
fooCommand,
|
||||
batCommand,
|
||||
}
|
||||
|
||||
for _, test := range commandAppTests {
|
||||
expect(t, app.Command(test.name) != nil, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_CommandWithArgBeforeFlags(t *testing.T) {
|
||||
var parsedOption, firstArg string
|
||||
|
||||
app := NewApp()
|
||||
command := Command{
|
||||
Name: "cmd",
|
||||
Flags: []Flag{
|
||||
StringFlag{Name: "option", Value: "", Usage: "some option"},
|
||||
},
|
||||
Action: func(c *Context) {
|
||||
parsedOption = c.String("option")
|
||||
firstArg = c.Args().First()
|
||||
},
|
||||
}
|
||||
app.Commands = []Command{command}
|
||||
|
||||
app.Run([]string{"", "cmd", "my-arg", "--option", "my-option"})
|
||||
|
||||
expect(t, parsedOption, "my-option")
|
||||
expect(t, firstArg, "my-arg")
|
||||
}
|
||||
|
||||
func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
|
||||
var context *Context
|
||||
|
||||
a := NewApp()
|
||||
a.Commands = []Command{
|
||||
{
|
||||
Name: "foo",
|
||||
Action: func(c *Context) {
|
||||
context = c
|
||||
},
|
||||
Flags: []Flag{
|
||||
StringFlag{
|
||||
Name: "lang",
|
||||
Value: "english",
|
||||
Usage: "language for the greeting",
|
||||
},
|
||||
},
|
||||
Before: func(_ *Context) error { return nil },
|
||||
},
|
||||
}
|
||||
a.Run([]string{"", "foo", "--lang", "spanish", "abcd"})
|
||||
|
||||
expect(t, context.Args().Get(0), "abcd")
|
||||
expect(t, context.String("lang"), "spanish")
|
||||
}
|
||||
|
||||
func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {
|
||||
var parsedOption string
|
||||
var args []string
|
||||
|
||||
app := NewApp()
|
||||
command := Command{
|
||||
Name: "cmd",
|
||||
Flags: []Flag{
|
||||
StringFlag{Name: "option", Value: "", Usage: "some option"},
|
||||
},
|
||||
Action: func(c *Context) {
|
||||
parsedOption = c.String("option")
|
||||
args = c.Args()
|
||||
},
|
||||
}
|
||||
app.Commands = []Command{command}
|
||||
|
||||
app.Run([]string{"", "cmd", "my-arg", "--option", "my-option", "--", "--notARealFlag"})
|
||||
|
||||
expect(t, parsedOption, "my-option")
|
||||
expect(t, args[0], "my-arg")
|
||||
expect(t, args[1], "--")
|
||||
expect(t, args[2], "--notARealFlag")
|
||||
}
|
||||
|
||||
func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) {
|
||||
var args []string
|
||||
|
||||
app := NewApp()
|
||||
command := Command{
|
||||
Name: "cmd",
|
||||
Action: func(c *Context) {
|
||||
args = c.Args()
|
||||
},
|
||||
}
|
||||
app.Commands = []Command{command}
|
||||
|
||||
app.Run([]string{"", "cmd", "my-arg", "--", "notAFlagAtAll"})
|
||||
|
||||
expect(t, args[0], "my-arg")
|
||||
expect(t, args[1], "--")
|
||||
expect(t, args[2], "notAFlagAtAll")
|
||||
}
|
||||
|
||||
func TestApp_Float64Flag(t *testing.T) {
|
||||
var meters float64
|
||||
|
||||
app := NewApp()
|
||||
app.Flags = []Flag{
|
||||
Float64Flag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"},
|
||||
}
|
||||
app.Action = func(c *Context) {
|
||||
meters = c.Float64("height")
|
||||
}
|
||||
|
||||
app.Run([]string{"", "--height", "1.93"})
|
||||
expect(t, meters, 1.93)
|
||||
}
|
||||
|
||||
func TestApp_ParseSliceFlags(t *testing.T) {
|
||||
var parsedOption, firstArg string
|
||||
var parsedIntSlice []int
|
||||
var parsedStringSlice []string
|
||||
|
||||
app := NewApp()
|
||||
command := Command{
|
||||
Name: "cmd",
|
||||
Flags: []Flag{
|
||||
IntSliceFlag{Name: "p", Value: &IntSlice{}, Usage: "set one or more ip addr"},
|
||||
StringSliceFlag{Name: "ip", Value: &StringSlice{}, Usage: "set one or more ports to open"},
|
||||
},
|
||||
Action: func(c *Context) {
|
||||
parsedIntSlice = c.IntSlice("p")
|
||||
parsedStringSlice = c.StringSlice("ip")
|
||||
parsedOption = c.String("option")
|
||||
firstArg = c.Args().First()
|
||||
},
|
||||
}
|
||||
app.Commands = []Command{command}
|
||||
|
||||
app.Run([]string{"", "cmd", "my-arg", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4"})
|
||||
|
||||
IntsEquals := func(a, b []int) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
StrsEquals := func(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
var expectedIntSlice = []int{22, 80}
|
||||
var expectedStringSlice = []string{"8.8.8.8", "8.8.4.4"}
|
||||
|
||||
if !IntsEquals(parsedIntSlice, expectedIntSlice) {
|
||||
t.Errorf("%v does not match %v", parsedIntSlice, expectedIntSlice)
|
||||
}
|
||||
|
||||
if !StrsEquals(parsedStringSlice, expectedStringSlice) {
|
||||
t.Errorf("%v does not match %v", parsedStringSlice, expectedStringSlice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) {
|
||||
var parsedIntSlice []int
|
||||
var parsedStringSlice []string
|
||||
|
||||
app := NewApp()
|
||||
command := Command{
|
||||
Name: "cmd",
|
||||
Flags: []Flag{
|
||||
IntSliceFlag{Name: "a", Usage: "set numbers"},
|
||||
StringSliceFlag{Name: "str", Usage: "set strings"},
|
||||
},
|
||||
Action: func(c *Context) {
|
||||
parsedIntSlice = c.IntSlice("a")
|
||||
parsedStringSlice = c.StringSlice("str")
|
||||
},
|
||||
}
|
||||
app.Commands = []Command{command}
|
||||
|
||||
app.Run([]string{"", "cmd", "my-arg", "-a", "2", "-str", "A"})
|
||||
|
||||
var expectedIntSlice = []int{2}
|
||||
var expectedStringSlice = []string{"A"}
|
||||
|
||||
if parsedIntSlice[0] != expectedIntSlice[0] {
|
||||
t.Errorf("%v does not match %v", parsedIntSlice[0], expectedIntSlice[0])
|
||||
}
|
||||
|
||||
if parsedStringSlice[0] != expectedStringSlice[0] {
|
||||
t.Errorf("%v does not match %v", parsedIntSlice[0], expectedIntSlice[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_DefaultStdout(t *testing.T) {
|
||||
app := NewApp()
|
||||
|
||||
if app.Writer != os.Stdout {
|
||||
t.Error("Default output writer not set.")
|
||||
}
|
||||
}
|
||||
|
||||
type mockWriter struct {
|
||||
written []byte
|
||||
}
|
||||
|
||||
func (fw *mockWriter) Write(p []byte) (n int, err error) {
|
||||
if fw.written == nil {
|
||||
fw.written = p
|
||||
} else {
|
||||
fw.written = append(fw.written, p...)
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (fw *mockWriter) GetWritten() (b []byte) {
|
||||
return fw.written
|
||||
}
|
||||
|
||||
func TestApp_SetStdout(t *testing.T) {
|
||||
w := &mockWriter{}
|
||||
|
||||
app := NewApp()
|
||||
app.Name = "test"
|
||||
app.Writer = w
|
||||
|
||||
err := app.Run([]string{"help"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run error: %s", err)
|
||||
}
|
||||
|
||||
if len(w.written) == 0 {
|
||||
t.Error("App did not write output to desired writer.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_BeforeFunc(t *testing.T) {
|
||||
beforeRun, subcommandRun := false, false
|
||||
beforeError := fmt.Errorf("fail")
|
||||
var err error
|
||||
|
||||
app := NewApp()
|
||||
|
||||
app.Before = func(c *Context) error {
|
||||
beforeRun = true
|
||||
s := c.String("opt")
|
||||
if s == "fail" {
|
||||
return beforeError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
app.Commands = []Command{
|
||||
Command{
|
||||
Name: "sub",
|
||||
Action: func(c *Context) {
|
||||
subcommandRun = true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Flags = []Flag{
|
||||
StringFlag{Name: "opt"},
|
||||
}
|
||||
|
||||
// run with the Before() func succeeding
|
||||
err = app.Run([]string{"command", "--opt", "succeed", "sub"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run error: %s", err)
|
||||
}
|
||||
|
||||
if beforeRun == false {
|
||||
t.Errorf("Before() not executed when expected")
|
||||
}
|
||||
|
||||
if subcommandRun == false {
|
||||
t.Errorf("Subcommand not executed when expected")
|
||||
}
|
||||
|
||||
// reset
|
||||
beforeRun, subcommandRun = false, false
|
||||
|
||||
// run with the Before() func failing
|
||||
err = app.Run([]string{"command", "--opt", "fail", "sub"})
|
||||
|
||||
// should be the same error produced by the Before func
|
||||
if err != beforeError {
|
||||
t.Errorf("Run error expected, but not received")
|
||||
}
|
||||
|
||||
if beforeRun == false {
|
||||
t.Errorf("Before() not executed when expected")
|
||||
}
|
||||
|
||||
if subcommandRun == true {
|
||||
t.Errorf("Subcommand executed when NOT expected")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestApp_AfterFunc(t *testing.T) {
|
||||
afterRun, subcommandRun := false, false
|
||||
afterError := fmt.Errorf("fail")
|
||||
var err error
|
||||
|
||||
app := NewApp()
|
||||
|
||||
app.After = func(c *Context) error {
|
||||
afterRun = true
|
||||
s := c.String("opt")
|
||||
if s == "fail" {
|
||||
return afterError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
app.Commands = []Command{
|
||||
Command{
|
||||
Name: "sub",
|
||||
Action: func(c *Context) {
|
||||
subcommandRun = true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Flags = []Flag{
|
||||
StringFlag{Name: "opt"},
|
||||
}
|
||||
|
||||
// run with the After() func succeeding
|
||||
err = app.Run([]string{"command", "--opt", "succeed", "sub"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run error: %s", err)
|
||||
}
|
||||
|
||||
if afterRun == false {
|
||||
t.Errorf("After() not executed when expected")
|
||||
}
|
||||
|
||||
if subcommandRun == false {
|
||||
t.Errorf("Subcommand not executed when expected")
|
||||
}
|
||||
|
||||
// reset
|
||||
afterRun, subcommandRun = false, false
|
||||
|
||||
// run with the Before() func failing
|
||||
err = app.Run([]string{"command", "--opt", "fail", "sub"})
|
||||
|
||||
// should be the same error produced by the Before func
|
||||
if err != afterError {
|
||||
t.Errorf("Run error expected, but not received")
|
||||
}
|
||||
|
||||
if afterRun == false {
|
||||
t.Errorf("After() not executed when expected")
|
||||
}
|
||||
|
||||
if subcommandRun == false {
|
||||
t.Errorf("Subcommand not executed when expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppNoHelpFlag(t *testing.T) {
|
||||
oldFlag := HelpFlag
|
||||
defer func() {
|
||||
HelpFlag = oldFlag
|
||||
}()
|
||||
|
||||
HelpFlag = BoolFlag{}
|
||||
|
||||
app := NewApp()
|
||||
err := app.Run([]string{"test", "-h"})
|
||||
|
||||
if err != flag.ErrHelp {
|
||||
t.Errorf("expected error about missing help flag, but got: %s (%T)", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppHelpPrinter(t *testing.T) {
|
||||
oldPrinter := HelpPrinter
|
||||
defer func() {
|
||||
HelpPrinter = oldPrinter
|
||||
}()
|
||||
|
||||
var wasCalled = false
|
||||
HelpPrinter = func(w io.Writer, template string, data interface{}) {
|
||||
wasCalled = true
|
||||
}
|
||||
|
||||
app := NewApp()
|
||||
app.Run([]string{"-h"})
|
||||
|
||||
if wasCalled == false {
|
||||
t.Errorf("Help printer expected to be called, but was not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppVersionPrinter(t *testing.T) {
|
||||
oldPrinter := VersionPrinter
|
||||
defer func() {
|
||||
VersionPrinter = oldPrinter
|
||||
}()
|
||||
|
||||
var wasCalled = false
|
||||
VersionPrinter = func(c *Context) {
|
||||
wasCalled = true
|
||||
}
|
||||
|
||||
app := NewApp()
|
||||
ctx := NewContext(app, nil, nil)
|
||||
ShowVersion(ctx)
|
||||
|
||||
if wasCalled == false {
|
||||
t.Errorf("Version printer expected to be called, but was not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppCommandNotFound(t *testing.T) {
|
||||
beforeRun, subcommandRun := false, false
|
||||
app := NewApp()
|
||||
|
||||
app.CommandNotFound = func(c *Context, command string) {
|
||||
beforeRun = true
|
||||
}
|
||||
|
||||
app.Commands = []Command{
|
||||
Command{
|
||||
Name: "bar",
|
||||
Action: func(c *Context) {
|
||||
subcommandRun = true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run([]string{"command", "foo"})
|
||||
|
||||
expect(t, beforeRun, true)
|
||||
expect(t, subcommandRun, false)
|
||||
}
|
||||
|
||||
func TestGlobalFlag(t *testing.T) {
|
||||
var globalFlag string
|
||||
var globalFlagSet bool
|
||||
app := NewApp()
|
||||
app.Flags = []Flag{
|
||||
StringFlag{Name: "global, g", Usage: "global"},
|
||||
}
|
||||
app.Action = func(c *Context) {
|
||||
globalFlag = c.GlobalString("global")
|
||||
globalFlagSet = c.GlobalIsSet("global")
|
||||
}
|
||||
app.Run([]string{"command", "-g", "foo"})
|
||||
expect(t, globalFlag, "foo")
|
||||
expect(t, globalFlagSet, true)
|
||||
|
||||
}
|
||||
|
||||
func TestGlobalFlagsInSubcommands(t *testing.T) {
|
||||
subcommandRun := false
|
||||
parentFlag := false
|
||||
app := NewApp()
|
||||
|
||||
app.Flags = []Flag{
|
||||
BoolFlag{Name: "debug, d", Usage: "Enable debugging"},
|
||||
}
|
||||
|
||||
app.Commands = []Command{
|
||||
Command{
|
||||
Name: "foo",
|
||||
Flags: []Flag{
|
||||
BoolFlag{Name: "parent, p", Usage: "Parent flag"},
|
||||
},
|
||||
Subcommands: []Command{
|
||||
{
|
||||
Name: "bar",
|
||||
Action: func(c *Context) {
|
||||
if c.GlobalBool("debug") {
|
||||
subcommandRun = true
|
||||
}
|
||||
if c.GlobalBool("parent") {
|
||||
parentFlag = true
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run([]string{"command", "-d", "foo", "-p", "bar"})
|
||||
|
||||
expect(t, subcommandRun, true)
|
||||
expect(t, parentFlag, true)
|
||||
}
|
||||
|
||||
func TestApp_Run_CommandWithSubcommandHasHelpTopic(t *testing.T) {
|
||||
var subcommandHelpTopics = [][]string{
|
||||
{"command", "foo", "--help"},
|
||||
{"command", "foo", "-h"},
|
||||
{"command", "foo", "help"},
|
||||
}
|
||||
|
||||
for _, flagSet := range subcommandHelpTopics {
|
||||
t.Logf("==> checking with flags %v", flagSet)
|
||||
|
||||
app := NewApp()
|
||||
buf := new(bytes.Buffer)
|
||||
app.Writer = buf
|
||||
|
||||
subCmdBar := Command{
|
||||
Name: "bar",
|
||||
Usage: "does bar things",
|
||||
}
|
||||
subCmdBaz := Command{
|
||||
Name: "baz",
|
||||
Usage: "does baz things",
|
||||
}
|
||||
cmd := Command{
|
||||
Name: "foo",
|
||||
Description: "descriptive wall of text about how it does foo things",
|
||||
Subcommands: []Command{subCmdBar, subCmdBaz},
|
||||
}
|
||||
|
||||
app.Commands = []Command{cmd}
|
||||
err := app.Run(flagSet)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
t.Logf("output: %q\n", buf.Bytes())
|
||||
|
||||
if strings.Contains(output, "No help topic for") {
|
||||
t.Errorf("expect a help topic, got none: \n%q", output)
|
||||
}
|
||||
|
||||
for _, shouldContain := range []string{
|
||||
cmd.Name, cmd.Description,
|
||||
subCmdBar.Name, subCmdBar.Usage,
|
||||
subCmdBaz.Name, subCmdBaz.Usage,
|
||||
} {
|
||||
if !strings.Contains(output, shouldContain) {
|
||||
t.Errorf("want help to contain %q, did not: \n%q", shouldContain, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Run_SubcommandFullPath(t *testing.T) {
|
||||
app := NewApp()
|
||||
buf := new(bytes.Buffer)
|
||||
app.Writer = buf
|
||||
|
||||
subCmd := Command{
|
||||
Name: "bar",
|
||||
Usage: "does bar things",
|
||||
}
|
||||
cmd := Command{
|
||||
Name: "foo",
|
||||
Description: "foo commands",
|
||||
Subcommands: []Command{subCmd},
|
||||
}
|
||||
app.Commands = []Command{cmd}
|
||||
|
||||
err := app.Run([]string{"command", "foo", "bar", "--help"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "foo bar - does bar things") {
|
||||
t.Errorf("expected full path to subcommand: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "command foo bar [arguments...]") {
|
||||
t.Errorf("expected full path to subcommand: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Run_Help(t *testing.T) {
|
||||
var helpArguments = [][]string{{"boom", "--help"}, {"boom", "-h"}, {"boom", "help"}}
|
||||
|
||||
for _, args := range helpArguments {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
t.Logf("==> checking with arguments %v", args)
|
||||
|
||||
app := NewApp()
|
||||
app.Name = "boom"
|
||||
app.Usage = "make an explosive entrance"
|
||||
app.Writer = buf
|
||||
app.Action = func(c *Context) {
|
||||
buf.WriteString("boom I say!")
|
||||
}
|
||||
|
||||
err := app.Run(args)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
t.Logf("output: %q\n", buf.Bytes())
|
||||
|
||||
if !strings.Contains(output, "boom - make an explosive entrance") {
|
||||
t.Errorf("want help to contain %q, did not: \n%q", "boom - make an explosive entrance", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Run_Version(t *testing.T) {
|
||||
var versionArguments = [][]string{{"boom", "--version"}, {"boom", "-v"}}
|
||||
|
||||
for _, args := range versionArguments {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
t.Logf("==> checking with arguments %v", args)
|
||||
|
||||
app := NewApp()
|
||||
app.Name = "boom"
|
||||
app.Usage = "make an explosive entrance"
|
||||
app.Version = "0.1.0"
|
||||
app.Writer = buf
|
||||
app.Action = func(c *Context) {
|
||||
buf.WriteString("boom I say!")
|
||||
}
|
||||
|
||||
err := app.Run(args)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
t.Logf("output: %q\n", buf.Bytes())
|
||||
|
||||
if !strings.Contains(output, "0.1.0") {
|
||||
t.Errorf("want version to contain %q, did not: \n%q", "0.1.0", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) {
|
||||
app := NewApp()
|
||||
app.Action = func(c *Context) {}
|
||||
app.Before = func(c *Context) error { return fmt.Errorf("before error") }
|
||||
app.After = func(c *Context) error { return fmt.Errorf("after error") }
|
||||
|
||||
err := app.Run([]string{"foo"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to recieve error from Run, got none")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "before error") {
|
||||
t.Errorf("expected text of error from Before method, but got none in \"%v\"", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "after error") {
|
||||
t.Errorf("expected text of error from After method, but got none in \"%v\"", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) {
|
||||
app := NewApp()
|
||||
app.Commands = []Command{
|
||||
Command{
|
||||
Name: "bar",
|
||||
Before: func(c *Context) error { return fmt.Errorf("before error") },
|
||||
After: func(c *Context) error { return fmt.Errorf("after error") },
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run([]string{"foo", "bar"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to recieve error from Run, got none")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "before error") {
|
||||
t.Errorf("expected text of error from Before method, but got none in \"%v\"", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "after error") {
|
||||
t.Errorf("expected text of error from After method, but got none in \"%v\"", err)
|
||||
}
|
||||
}
|
13
Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete
generated
vendored
13
Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete
generated
vendored
|
@ -1,13 +0,0 @@
|
|||
#! /bin/bash
|
||||
|
||||
_cli_bash_autocomplete() {
|
||||
local cur prev opts base
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _cli_bash_autocomplete $PROG
|
5
Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete
generated
vendored
5
Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete
generated
vendored
|
@ -1,5 +0,0 @@
|
|||
autoload -U compinit && compinit
|
||||
autoload -U bashcompinit && bashcompinit
|
||||
|
||||
script_dir=$(dirname $0)
|
||||
source ${script_dir}/bash_autocomplete
|
98
Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go
generated
vendored
98
Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go
generated
vendored
|
@ -1,98 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
app := NewApp()
|
||||
app.Name = "todo"
|
||||
app.Usage = "task list on the command line"
|
||||
app.Commands = []Command{
|
||||
{
|
||||
Name: "add",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "add a task to the list",
|
||||
Action: func(c *Context) {
|
||||
println("added task: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "complete",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "complete a task on the list",
|
||||
Action: func(c *Context) {
|
||||
println("completed task: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func ExampleSubcommand() {
|
||||
app := NewApp()
|
||||
app.Name = "say"
|
||||
app.Commands = []Command{
|
||||
{
|
||||
Name: "hello",
|
||||
Aliases: []string{"hi"},
|
||||
Usage: "use it to see a description",
|
||||
Description: "This is how we describe hello the function",
|
||||
Subcommands: []Command{
|
||||
{
|
||||
Name: "english",
|
||||
Aliases: []string{"en"},
|
||||
Usage: "sends a greeting in english",
|
||||
Description: "greets someone in english",
|
||||
Flags: []Flag{
|
||||
StringFlag{
|
||||
Name: "name",
|
||||
Value: "Bob",
|
||||
Usage: "Name of the person to greet",
|
||||
},
|
||||
},
|
||||
Action: func(c *Context) {
|
||||
println("Hello, ", c.String("name"))
|
||||
},
|
||||
}, {
|
||||
Name: "spanish",
|
||||
Aliases: []string{"sp"},
|
||||
Usage: "sends a greeting in spanish",
|
||||
Flags: []Flag{
|
||||
StringFlag{
|
||||
Name: "surname",
|
||||
Value: "Jones",
|
||||
Usage: "Surname of the person to greet",
|
||||
},
|
||||
},
|
||||
Action: func(c *Context) {
|
||||
println("Hola, ", c.String("surname"))
|
||||
},
|
||||
}, {
|
||||
Name: "french",
|
||||
Aliases: []string{"fr"},
|
||||
Usage: "sends a greeting in french",
|
||||
Flags: []Flag{
|
||||
StringFlag{
|
||||
Name: "nickname",
|
||||
Value: "Stevie",
|
||||
Usage: "Nickname of the person to greet",
|
||||
},
|
||||
},
|
||||
Action: func(c *Context) {
|
||||
println("Bonjour, ", c.String("nickname"))
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "bye",
|
||||
Usage: "says goodbye",
|
||||
Action: func(c *Context) {
|
||||
println("bye")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
200
Godeps/_workspace/src/github.com/codegangsta/cli/command.go
generated
vendored
200
Godeps/_workspace/src/github.com/codegangsta/cli/command.go
generated
vendored
|
@ -1,200 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Command is a subcommand for a cli.App.
|
||||
type Command struct {
|
||||
// The name of the command
|
||||
Name string
|
||||
// short name of the command. Typically one character (deprecated, use `Aliases`)
|
||||
ShortName string
|
||||
// A list of aliases for the command
|
||||
Aliases []string
|
||||
// A short description of the usage of this command
|
||||
Usage string
|
||||
// A longer explanation of how the command works
|
||||
Description string
|
||||
// The function to call when checking for bash command completions
|
||||
BashComplete func(context *Context)
|
||||
// An action to execute before any sub-subcommands are run, but after the context is ready
|
||||
// If a non-nil error is returned, no sub-subcommands are run
|
||||
Before func(context *Context) error
|
||||
// An action to execute after any subcommands are run, but after the subcommand has finished
|
||||
// It is run even if Action() panics
|
||||
After func(context *Context) error
|
||||
// The function to call when this command is invoked
|
||||
Action func(context *Context)
|
||||
// List of child commands
|
||||
Subcommands []Command
|
||||
// List of flags to parse
|
||||
Flags []Flag
|
||||
// Treat all flags as normal arguments if true
|
||||
SkipFlagParsing bool
|
||||
// Boolean to hide built-in help command
|
||||
HideHelp bool
|
||||
|
||||
commandNamePath []string
|
||||
}
|
||||
|
||||
// Returns the full name of the command.
|
||||
// For subcommands this ensures that parent commands are part of the command path
|
||||
func (c Command) FullName() string {
|
||||
if c.commandNamePath == nil {
|
||||
return c.Name
|
||||
}
|
||||
return strings.Join(c.commandNamePath, " ")
|
||||
}
|
||||
|
||||
// Invokes the command given the context, parses ctx.Args() to generate command-specific flags
|
||||
func (c Command) Run(ctx *Context) error {
|
||||
if len(c.Subcommands) > 0 || c.Before != nil || c.After != nil {
|
||||
return c.startApp(ctx)
|
||||
}
|
||||
|
||||
if !c.HideHelp && (HelpFlag != BoolFlag{}) {
|
||||
// append help to flags
|
||||
c.Flags = append(
|
||||
c.Flags,
|
||||
HelpFlag,
|
||||
)
|
||||
}
|
||||
|
||||
if ctx.App.EnableBashCompletion {
|
||||
c.Flags = append(c.Flags, BashCompletionFlag)
|
||||
}
|
||||
|
||||
set := flagSet(c.Name, c.Flags)
|
||||
set.SetOutput(ioutil.Discard)
|
||||
|
||||
firstFlagIndex := -1
|
||||
terminatorIndex := -1
|
||||
for index, arg := range ctx.Args() {
|
||||
if arg == "--" {
|
||||
terminatorIndex = index
|
||||
break
|
||||
} else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 {
|
||||
firstFlagIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if firstFlagIndex > -1 && !c.SkipFlagParsing {
|
||||
args := ctx.Args()
|
||||
regularArgs := make([]string, len(args[1:firstFlagIndex]))
|
||||
copy(regularArgs, args[1:firstFlagIndex])
|
||||
|
||||
var flagArgs []string
|
||||
if terminatorIndex > -1 {
|
||||
flagArgs = args[firstFlagIndex:terminatorIndex]
|
||||
regularArgs = append(regularArgs, args[terminatorIndex:]...)
|
||||
} else {
|
||||
flagArgs = args[firstFlagIndex:]
|
||||
}
|
||||
|
||||
err = set.Parse(append(flagArgs, regularArgs...))
|
||||
} else {
|
||||
err = set.Parse(ctx.Args().Tail())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.")
|
||||
fmt.Fprintln(ctx.App.Writer)
|
||||
ShowCommandHelp(ctx, c.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
nerr := normalizeFlags(c.Flags, set)
|
||||
if nerr != nil {
|
||||
fmt.Fprintln(ctx.App.Writer, nerr)
|
||||
fmt.Fprintln(ctx.App.Writer)
|
||||
ShowCommandHelp(ctx, c.Name)
|
||||
return nerr
|
||||
}
|
||||
context := NewContext(ctx.App, set, ctx)
|
||||
|
||||
if checkCommandCompletions(context, c.Name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkCommandHelp(context, c.Name) {
|
||||
return nil
|
||||
}
|
||||
context.Command = c
|
||||
c.Action(context)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Command) Names() []string {
|
||||
names := []string{c.Name}
|
||||
|
||||
if c.ShortName != "" {
|
||||
names = append(names, c.ShortName)
|
||||
}
|
||||
|
||||
return append(names, c.Aliases...)
|
||||
}
|
||||
|
||||
// Returns true if Command.Name or Command.ShortName matches given name
|
||||
func (c Command) HasName(name string) bool {
|
||||
for _, n := range c.Names() {
|
||||
if n == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c Command) startApp(ctx *Context) error {
|
||||
app := NewApp()
|
||||
|
||||
// set the name and usage
|
||||
app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name)
|
||||
if c.Description != "" {
|
||||
app.Usage = c.Description
|
||||
} else {
|
||||
app.Usage = c.Usage
|
||||
}
|
||||
|
||||
// set CommandNotFound
|
||||
app.CommandNotFound = ctx.App.CommandNotFound
|
||||
|
||||
// set the flags and commands
|
||||
app.Commands = c.Subcommands
|
||||
app.Flags = c.Flags
|
||||
app.HideHelp = c.HideHelp
|
||||
|
||||
app.Version = ctx.App.Version
|
||||
app.HideVersion = ctx.App.HideVersion
|
||||
app.Compiled = ctx.App.Compiled
|
||||
app.Author = ctx.App.Author
|
||||
app.Email = ctx.App.Email
|
||||
app.Writer = ctx.App.Writer
|
||||
|
||||
// bash completion
|
||||
app.EnableBashCompletion = ctx.App.EnableBashCompletion
|
||||
if c.BashComplete != nil {
|
||||
app.BashComplete = c.BashComplete
|
||||
}
|
||||
|
||||
// set the actions
|
||||
app.Before = c.Before
|
||||
app.After = c.After
|
||||
if c.Action != nil {
|
||||
app.Action = c.Action
|
||||
} else {
|
||||
app.Action = helpSubcommand.Action
|
||||
}
|
||||
|
||||
var newCmds []Command
|
||||
for _, cc := range app.Commands {
|
||||
cc.commandNamePath = []string{c.Name, cc.Name}
|
||||
newCmds = append(newCmds, cc)
|
||||
}
|
||||
app.Commands = newCmds
|
||||
|
||||
return app.RunAsSubcommand(ctx)
|
||||
}
|
47
Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go
generated
vendored
47
Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go
generated
vendored
|
@ -1,47 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommandDoNotIgnoreFlags(t *testing.T) {
|
||||
app := NewApp()
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
test := []string{"blah", "blah", "-break"}
|
||||
set.Parse(test)
|
||||
|
||||
c := NewContext(app, set, nil)
|
||||
|
||||
command := Command{
|
||||
Name: "test-cmd",
|
||||
Aliases: []string{"tc"},
|
||||
Usage: "this is for testing",
|
||||
Description: "testing",
|
||||
Action: func(_ *Context) {},
|
||||
}
|
||||
err := command.Run(c)
|
||||
|
||||
expect(t, err.Error(), "flag provided but not defined: -break")
|
||||
}
|
||||
|
||||
func TestCommandIgnoreFlags(t *testing.T) {
|
||||
app := NewApp()
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
test := []string{"blah", "blah"}
|
||||
set.Parse(test)
|
||||
|
||||
c := NewContext(app, set, nil)
|
||||
|
||||
command := Command{
|
||||
Name: "test-cmd",
|
||||
Aliases: []string{"tc"},
|
||||
Usage: "this is for testing",
|
||||
Description: "testing",
|
||||
Action: func(_ *Context) {},
|
||||
SkipFlagParsing: true,
|
||||
}
|
||||
err := command.Run(c)
|
||||
|
||||
expect(t, err, nil)
|
||||
}
|
388
Godeps/_workspace/src/github.com/codegangsta/cli/context.go
generated
vendored
388
Godeps/_workspace/src/github.com/codegangsta/cli/context.go
generated
vendored
|
@ -1,388 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Context is a type that is passed through to
|
||||
// each Handler action in a cli application. Context
|
||||
// can be used to retrieve context-specific Args and
|
||||
// parsed command-line options.
|
||||
type Context struct {
|
||||
App *App
|
||||
Command Command
|
||||
flagSet *flag.FlagSet
|
||||
setFlags map[string]bool
|
||||
globalSetFlags map[string]bool
|
||||
parentContext *Context
|
||||
}
|
||||
|
||||
// Creates a new context. For use in when invoking an App or Command action.
|
||||
func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
|
||||
return &Context{App: app, flagSet: set, parentContext: parentCtx}
|
||||
}
|
||||
|
||||
// Looks up the value of a local int flag, returns 0 if no int flag exists
|
||||
func (c *Context) Int(name string) int {
|
||||
return lookupInt(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local time.Duration flag, returns 0 if no time.Duration flag exists
|
||||
func (c *Context) Duration(name string) time.Duration {
|
||||
return lookupDuration(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists
|
||||
func (c *Context) Float64(name string) float64 {
|
||||
return lookupFloat64(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local bool flag, returns false if no bool flag exists
|
||||
func (c *Context) Bool(name string) bool {
|
||||
return lookupBool(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local boolT flag, returns false if no bool flag exists
|
||||
func (c *Context) BoolT(name string) bool {
|
||||
return lookupBoolT(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local string flag, returns "" if no string flag exists
|
||||
func (c *Context) String(name string) string {
|
||||
return lookupString(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local string slice flag, returns nil if no string slice flag exists
|
||||
func (c *Context) StringSlice(name string) []string {
|
||||
return lookupStringSlice(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local int slice flag, returns nil if no int slice flag exists
|
||||
func (c *Context) IntSlice(name string) []int {
|
||||
return lookupIntSlice(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local generic flag, returns nil if no generic flag exists
|
||||
func (c *Context) Generic(name string) interface{} {
|
||||
return lookupGeneric(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a global int flag, returns 0 if no int flag exists
|
||||
func (c *Context) GlobalInt(name string) int {
|
||||
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||
return lookupInt(name, fs)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Looks up the value of a global time.Duration flag, returns 0 if no time.Duration flag exists
|
||||
func (c *Context) GlobalDuration(name string) time.Duration {
|
||||
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||
return lookupDuration(name, fs)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Looks up the value of a global bool flag, returns false if no bool flag exists
|
||||
func (c *Context) GlobalBool(name string) bool {
|
||||
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||
return lookupBool(name, fs)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Looks up the value of a global string flag, returns "" if no string flag exists
|
||||
func (c *Context) GlobalString(name string) string {
|
||||
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||
return lookupString(name, fs)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Looks up the value of a global string slice flag, returns nil if no string slice flag exists
|
||||
func (c *Context) GlobalStringSlice(name string) []string {
|
||||
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||
return lookupStringSlice(name, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Looks up the value of a global int slice flag, returns nil if no int slice flag exists
|
||||
func (c *Context) GlobalIntSlice(name string) []int {
|
||||
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||
return lookupIntSlice(name, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Looks up the value of a global generic flag, returns nil if no generic flag exists
|
||||
func (c *Context) GlobalGeneric(name string) interface{} {
|
||||
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||
return lookupGeneric(name, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the number of flags set
|
||||
func (c *Context) NumFlags() int {
|
||||
return c.flagSet.NFlag()
|
||||
}
|
||||
|
||||
// Determines if the flag was actually set
|
||||
func (c *Context) IsSet(name string) bool {
|
||||
if c.setFlags == nil {
|
||||
c.setFlags = make(map[string]bool)
|
||||
c.flagSet.Visit(func(f *flag.Flag) {
|
||||
c.setFlags[f.Name] = true
|
||||
})
|
||||
}
|
||||
return c.setFlags[name] == true
|
||||
}
|
||||
|
||||
// Determines if the global flag was actually set
|
||||
func (c *Context) GlobalIsSet(name string) bool {
|
||||
if c.globalSetFlags == nil {
|
||||
c.globalSetFlags = make(map[string]bool)
|
||||
ctx := c
|
||||
if ctx.parentContext != nil {
|
||||
ctx = ctx.parentContext
|
||||
}
|
||||
for ; ctx != nil && c.globalSetFlags[name] == false; ctx = ctx.parentContext {
|
||||
ctx.flagSet.Visit(func(f *flag.Flag) {
|
||||
c.globalSetFlags[f.Name] = true
|
||||
})
|
||||
}
|
||||
}
|
||||
return c.globalSetFlags[name]
|
||||
}
|
||||
|
||||
// Returns a slice of flag names used in this context.
|
||||
func (c *Context) FlagNames() (names []string) {
|
||||
for _, flag := range c.Command.Flags {
|
||||
name := strings.Split(flag.getName(), ",")[0]
|
||||
if name == "help" {
|
||||
continue
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Returns a slice of global flag names used by the app.
|
||||
func (c *Context) GlobalFlagNames() (names []string) {
|
||||
for _, flag := range c.App.Flags {
|
||||
name := strings.Split(flag.getName(), ",")[0]
|
||||
if name == "help" || name == "version" {
|
||||
continue
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the parent context, if any
|
||||
func (c *Context) Parent() *Context {
|
||||
return c.parentContext
|
||||
}
|
||||
|
||||
type Args []string
|
||||
|
||||
// Returns the command line arguments associated with the context.
|
||||
func (c *Context) Args() Args {
|
||||
args := Args(c.flagSet.Args())
|
||||
return args
|
||||
}
|
||||
|
||||
// Returns the nth argument, or else a blank string
|
||||
func (a Args) Get(n int) string {
|
||||
if len(a) > n {
|
||||
return a[n]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Returns the first argument, or else a blank string
|
||||
func (a Args) First() string {
|
||||
return a.Get(0)
|
||||
}
|
||||
|
||||
// Return the rest of the arguments (not the first one)
|
||||
// or else an empty string slice
|
||||
func (a Args) Tail() []string {
|
||||
if len(a) >= 2 {
|
||||
return []string(a)[1:]
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Checks if there are any arguments present
|
||||
func (a Args) Present() bool {
|
||||
return len(a) != 0
|
||||
}
|
||||
|
||||
// Swaps arguments at the given indexes
|
||||
func (a Args) Swap(from, to int) error {
|
||||
if from >= len(a) || to >= len(a) {
|
||||
return errors.New("index out of range")
|
||||
}
|
||||
a[from], a[to] = a[to], a[from]
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet {
|
||||
if ctx.parentContext != nil {
|
||||
ctx = ctx.parentContext
|
||||
}
|
||||
for ; ctx != nil; ctx = ctx.parentContext {
|
||||
if f := ctx.flagSet.Lookup(name); f != nil {
|
||||
return ctx.flagSet
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupInt(name string, set *flag.FlagSet) int {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
val, err := strconv.Atoi(f.Value.String())
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupDuration(name string, set *flag.FlagSet) time.Duration {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
val, err := time.ParseDuration(f.Value.String())
|
||||
if err == nil {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupFloat64(name string, set *flag.FlagSet) float64 {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
val, err := strconv.ParseFloat(f.Value.String(), 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupString(name string, set *flag.FlagSet) string {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
return f.Value.String()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func lookupStringSlice(name string, set *flag.FlagSet) []string {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
return (f.Value.(*StringSlice)).Value()
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupIntSlice(name string, set *flag.FlagSet) []int {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
return (f.Value.(*IntSlice)).Value()
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupGeneric(name string, set *flag.FlagSet) interface{} {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
return f.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupBool(name string, set *flag.FlagSet) bool {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
val, err := strconv.ParseBool(f.Value.String())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func lookupBoolT(name string, set *flag.FlagSet) bool {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
val, err := strconv.ParseBool(f.Value.String())
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
|
||||
switch ff.Value.(type) {
|
||||
case *StringSlice:
|
||||
default:
|
||||
set.Set(name, ff.Value.String())
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
|
||||
visited := make(map[string]bool)
|
||||
set.Visit(func(f *flag.Flag) {
|
||||
visited[f.Name] = true
|
||||
})
|
||||
for _, f := range flags {
|
||||
parts := strings.Split(f.getName(), ",")
|
||||
if len(parts) == 1 {
|
||||
continue
|
||||
}
|
||||
var ff *flag.Flag
|
||||
for _, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
if visited[name] {
|
||||
if ff != nil {
|
||||
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
|
||||
}
|
||||
ff = set.Lookup(name)
|
||||
}
|
||||
}
|
||||
if ff == nil {
|
||||
continue
|
||||
}
|
||||
for _, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
if !visited[name] {
|
||||
copyFlag(name, ff, set)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
113
Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go
generated
vendored
113
Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go
generated
vendored
|
@ -1,113 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Int("myflag", 12, "doc")
|
||||
globalSet := flag.NewFlagSet("test", 0)
|
||||
globalSet.Int("myflag", 42, "doc")
|
||||
globalCtx := NewContext(nil, globalSet, nil)
|
||||
command := Command{Name: "mycommand"}
|
||||
c := NewContext(nil, set, globalCtx)
|
||||
c.Command = command
|
||||
expect(t, c.Int("myflag"), 12)
|
||||
expect(t, c.GlobalInt("myflag"), 42)
|
||||
expect(t, c.Command.Name, "mycommand")
|
||||
}
|
||||
|
||||
func TestContext_Int(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Int("myflag", 12, "doc")
|
||||
c := NewContext(nil, set, nil)
|
||||
expect(t, c.Int("myflag"), 12)
|
||||
}
|
||||
|
||||
func TestContext_Duration(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Duration("myflag", time.Duration(12*time.Second), "doc")
|
||||
c := NewContext(nil, set, nil)
|
||||
expect(t, c.Duration("myflag"), time.Duration(12*time.Second))
|
||||
}
|
||||
|
||||
func TestContext_String(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.String("myflag", "hello world", "doc")
|
||||
c := NewContext(nil, set, nil)
|
||||
expect(t, c.String("myflag"), "hello world")
|
||||
}
|
||||
|
||||
func TestContext_Bool(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
c := NewContext(nil, set, nil)
|
||||
expect(t, c.Bool("myflag"), false)
|
||||
}
|
||||
|
||||
func TestContext_BoolT(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", true, "doc")
|
||||
c := NewContext(nil, set, nil)
|
||||
expect(t, c.BoolT("myflag"), true)
|
||||
}
|
||||
|
||||
func TestContext_Args(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
c := NewContext(nil, set, nil)
|
||||
set.Parse([]string{"--myflag", "bat", "baz"})
|
||||
expect(t, len(c.Args()), 2)
|
||||
expect(t, c.Bool("myflag"), true)
|
||||
}
|
||||
|
||||
func TestContext_IsSet(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
set.String("otherflag", "hello world", "doc")
|
||||
globalSet := flag.NewFlagSet("test", 0)
|
||||
globalSet.Bool("myflagGlobal", true, "doc")
|
||||
globalCtx := NewContext(nil, globalSet, nil)
|
||||
c := NewContext(nil, set, globalCtx)
|
||||
set.Parse([]string{"--myflag", "bat", "baz"})
|
||||
globalSet.Parse([]string{"--myflagGlobal", "bat", "baz"})
|
||||
expect(t, c.IsSet("myflag"), true)
|
||||
expect(t, c.IsSet("otherflag"), false)
|
||||
expect(t, c.IsSet("bogusflag"), false)
|
||||
expect(t, c.IsSet("myflagGlobal"), false)
|
||||
}
|
||||
|
||||
func TestContext_GlobalIsSet(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
set.String("otherflag", "hello world", "doc")
|
||||
globalSet := flag.NewFlagSet("test", 0)
|
||||
globalSet.Bool("myflagGlobal", true, "doc")
|
||||
globalSet.Bool("myflagGlobalUnset", true, "doc")
|
||||
globalCtx := NewContext(nil, globalSet, nil)
|
||||
c := NewContext(nil, set, globalCtx)
|
||||
set.Parse([]string{"--myflag", "bat", "baz"})
|
||||
globalSet.Parse([]string{"--myflagGlobal", "bat", "baz"})
|
||||
expect(t, c.GlobalIsSet("myflag"), false)
|
||||
expect(t, c.GlobalIsSet("otherflag"), false)
|
||||
expect(t, c.GlobalIsSet("bogusflag"), false)
|
||||
expect(t, c.GlobalIsSet("myflagGlobal"), true)
|
||||
expect(t, c.GlobalIsSet("myflagGlobalUnset"), false)
|
||||
expect(t, c.GlobalIsSet("bogusGlobal"), false)
|
||||
}
|
||||
|
||||
func TestContext_NumFlags(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
set.String("otherflag", "hello world", "doc")
|
||||
globalSet := flag.NewFlagSet("test", 0)
|
||||
globalSet.Bool("myflagGlobal", true, "doc")
|
||||
globalCtx := NewContext(nil, globalSet, nil)
|
||||
c := NewContext(nil, set, globalCtx)
|
||||
set.Parse([]string{"--myflag", "--otherflag=foo"})
|
||||
globalSet.Parse([]string{"--myflagGlobal"})
|
||||
expect(t, c.NumFlags(), 2)
|
||||
}
|
497
Godeps/_workspace/src/github.com/codegangsta/cli/flag.go
generated
vendored
497
Godeps/_workspace/src/github.com/codegangsta/cli/flag.go
generated
vendored
|
@ -1,497 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This flag enables bash-completion for all commands and subcommands
|
||||
var BashCompletionFlag = BoolFlag{
|
||||
Name: "generate-bash-completion",
|
||||
}
|
||||
|
||||
// This flag prints the version for the application
|
||||
var VersionFlag = BoolFlag{
|
||||
Name: "version, v",
|
||||
Usage: "print the version",
|
||||
}
|
||||
|
||||
// This flag prints the help for all commands and subcommands
|
||||
// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand
|
||||
// unless HideHelp is set to true)
|
||||
var HelpFlag = BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "show help",
|
||||
}
|
||||
|
||||
// Flag is a common interface related to parsing flags in cli.
|
||||
// For more advanced flag parsing techniques, it is recomended that
|
||||
// this interface be implemented.
|
||||
type Flag interface {
|
||||
fmt.Stringer
|
||||
// Apply Flag settings to the given flag set
|
||||
Apply(*flag.FlagSet)
|
||||
getName() string
|
||||
}
|
||||
|
||||
func flagSet(name string, flags []Flag) *flag.FlagSet {
|
||||
set := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||
|
||||
for _, f := range flags {
|
||||
f.Apply(set)
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func eachName(longName string, fn func(string)) {
|
||||
parts := strings.Split(longName, ",")
|
||||
for _, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
fn(name)
|
||||
}
|
||||
}
|
||||
|
||||
// Generic is a generic parseable type identified by a specific flag
|
||||
type Generic interface {
|
||||
Set(value string) error
|
||||
String() string
|
||||
}
|
||||
|
||||
// GenericFlag is the flag type for types implementing Generic
|
||||
type GenericFlag struct {
|
||||
Name string
|
||||
Value Generic
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns the string representation of the generic flag to display the
|
||||
// help text to the user (uses the String() method of the generic flag to show
|
||||
// the value)
|
||||
func (f GenericFlag) String() string {
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s \"%v\"\t%v", prefixFor(f.Name), f.Name, f.Value, f.Usage))
|
||||
}
|
||||
|
||||
// Apply takes the flagset and calls Set on the generic flag with the value
|
||||
// provided by the user for parsing by the flag
|
||||
func (f GenericFlag) Apply(set *flag.FlagSet) {
|
||||
val := f.Value
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
val.Set(envVal)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f GenericFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
// StringSlice is an opaque type for []string to satisfy flag.Value
|
||||
type StringSlice []string
|
||||
|
||||
// Set appends the string value to the list of values
|
||||
func (f *StringSlice) Set(value string) error {
|
||||
*f = append(*f, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a readable representation of this value (for usage defaults)
|
||||
func (f *StringSlice) String() string {
|
||||
return fmt.Sprintf("%s", *f)
|
||||
}
|
||||
|
||||
// Value returns the slice of strings set by this flag
|
||||
func (f *StringSlice) Value() []string {
|
||||
return *f
|
||||
}
|
||||
|
||||
// StringSlice is a string flag that can be specified multiple times on the
|
||||
// command-line
|
||||
type StringSliceFlag struct {
|
||||
Name string
|
||||
Value *StringSlice
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns the usage
|
||||
func (f StringSliceFlag) String() string {
|
||||
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
|
||||
pref := prefixFor(firstName)
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage))
|
||||
}
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
newVal := &StringSlice{}
|
||||
for _, s := range strings.Split(envVal, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
newVal.Set(s)
|
||||
}
|
||||
f.Value = newVal
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
if f.Value == nil {
|
||||
f.Value = &StringSlice{}
|
||||
}
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f StringSliceFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
// StringSlice is an opaque type for []int to satisfy flag.Value
|
||||
type IntSlice []int
|
||||
|
||||
// Set parses the value into an integer and appends it to the list of values
|
||||
func (f *IntSlice) Set(value string) error {
|
||||
tmp, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
*f = append(*f, tmp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a readable representation of this value (for usage defaults)
|
||||
func (f *IntSlice) String() string {
|
||||
return fmt.Sprintf("%d", *f)
|
||||
}
|
||||
|
||||
// Value returns the slice of ints set by this flag
|
||||
func (f *IntSlice) Value() []int {
|
||||
return *f
|
||||
}
|
||||
|
||||
// IntSliceFlag is an int flag that can be specified multiple times on the
|
||||
// command-line
|
||||
type IntSliceFlag struct {
|
||||
Name string
|
||||
Value *IntSlice
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns the usage
|
||||
func (f IntSliceFlag) String() string {
|
||||
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
|
||||
pref := prefixFor(firstName)
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage))
|
||||
}
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
newVal := &IntSlice{}
|
||||
for _, s := range strings.Split(envVal, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
err := newVal.Set(s)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
}
|
||||
}
|
||||
f.Value = newVal
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
if f.Value == nil {
|
||||
f.Value = &IntSlice{}
|
||||
}
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f IntSliceFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
// BoolFlag is a switch that defaults to false
|
||||
type BoolFlag struct {
|
||||
Name string
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns a readable representation of this value (for usage defaults)
|
||||
func (f BoolFlag) String() string {
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage))
|
||||
}
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f BoolFlag) Apply(set *flag.FlagSet) {
|
||||
val := false
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
envValBool, err := strconv.ParseBool(envVal)
|
||||
if err == nil {
|
||||
val = envValBool
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Bool(name, val, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f BoolFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
// BoolTFlag this represents a boolean flag that is true by default, but can
|
||||
// still be set to false by --some-flag=false
|
||||
type BoolTFlag struct {
|
||||
Name string
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns a readable representation of this value (for usage defaults)
|
||||
func (f BoolTFlag) String() string {
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage))
|
||||
}
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f BoolTFlag) Apply(set *flag.FlagSet) {
|
||||
val := true
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
envValBool, err := strconv.ParseBool(envVal)
|
||||
if err == nil {
|
||||
val = envValBool
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Bool(name, val, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f BoolTFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
// StringFlag represents a flag that takes as string value
|
||||
type StringFlag struct {
|
||||
Name string
|
||||
Value string
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns the usage
|
||||
func (f StringFlag) String() string {
|
||||
var fmtString string
|
||||
fmtString = "%s %v\t%v"
|
||||
|
||||
if len(f.Value) > 0 {
|
||||
fmtString = "%s \"%v\"\t%v"
|
||||
} else {
|
||||
fmtString = "%s %v\t%v"
|
||||
}
|
||||
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage))
|
||||
}
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f StringFlag) Apply(set *flag.FlagSet) {
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
f.Value = envVal
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
set.String(name, f.Value, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f StringFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
// IntFlag is a flag that takes an integer
|
||||
// Errors if the value provided cannot be parsed
|
||||
type IntFlag struct {
|
||||
Name string
|
||||
Value int
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns the usage
|
||||
func (f IntFlag) String() string {
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage))
|
||||
}
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f IntFlag) Apply(set *flag.FlagSet) {
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
envValInt, err := strconv.ParseInt(envVal, 0, 64)
|
||||
if err == nil {
|
||||
f.Value = int(envValInt)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Int(name, f.Value, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f IntFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
// DurationFlag is a flag that takes a duration specified in Go's duration
|
||||
// format: https://golang.org/pkg/time/#ParseDuration
|
||||
type DurationFlag struct {
|
||||
Name string
|
||||
Value time.Duration
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns a readable representation of this value (for usage defaults)
|
||||
func (f DurationFlag) String() string {
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage))
|
||||
}
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f DurationFlag) Apply(set *flag.FlagSet) {
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
envValDuration, err := time.ParseDuration(envVal)
|
||||
if err == nil {
|
||||
f.Value = envValDuration
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Duration(name, f.Value, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f DurationFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
// Float64Flag is a flag that takes an float value
|
||||
// Errors if the value provided cannot be parsed
|
||||
type Float64Flag struct {
|
||||
Name string
|
||||
Value float64
|
||||
Usage string
|
||||
EnvVar string
|
||||
}
|
||||
|
||||
// String returns the usage
|
||||
func (f Float64Flag) String() string {
|
||||
return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage))
|
||||
}
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f Float64Flag) Apply(set *flag.FlagSet) {
|
||||
if f.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
envValFloat, err := strconv.ParseFloat(envVal, 10)
|
||||
if err == nil {
|
||||
f.Value = float64(envValFloat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Float64(name, f.Value, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f Float64Flag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func prefixFor(name string) (prefix string) {
|
||||
if len(name) == 1 {
|
||||
prefix = "-"
|
||||
} else {
|
||||
prefix = "--"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func prefixedNames(fullName string) (prefixed string) {
|
||||
parts := strings.Split(fullName, ",")
|
||||
for i, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
prefixed += prefixFor(name) + name
|
||||
if i < len(parts)-1 {
|
||||
prefixed += ", "
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func withEnvHint(envVar, str string) string {
|
||||
envText := ""
|
||||
if envVar != "" {
|
||||
envText = fmt.Sprintf(" [$%s]", strings.Join(strings.Split(envVar, ","), ", $"))
|
||||
}
|
||||
return str + envText
|
||||
}
|
740
Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go
generated
vendored
740
Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go
generated
vendored
|
@ -1,740 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var boolFlagTests = []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"help", "--help\t"},
|
||||
{"h", "-h\t"},
|
||||
}
|
||||
|
||||
func TestBoolFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range boolFlagTests {
|
||||
flag := BoolFlag{Name: test.name}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stringFlagTests = []struct {
|
||||
name string
|
||||
value string
|
||||
expected string
|
||||
}{
|
||||
{"help", "", "--help \t"},
|
||||
{"h", "", "-h \t"},
|
||||
{"h", "", "-h \t"},
|
||||
{"test", "Something", "--test \"Something\"\t"},
|
||||
}
|
||||
|
||||
func TestStringFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range stringFlagTests {
|
||||
flag := StringFlag{Name: test.name, Value: test.value}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_FOO", "derp")
|
||||
for _, test := range stringFlagTests {
|
||||
flag := StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"}
|
||||
output := flag.String()
|
||||
|
||||
if !strings.HasSuffix(output, " [$APP_FOO]") {
|
||||
t.Errorf("%s does not end with [$APP_FOO]", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stringSliceFlagTests = []struct {
|
||||
name string
|
||||
value *StringSlice
|
||||
expected string
|
||||
}{
|
||||
{"help", func() *StringSlice {
|
||||
s := &StringSlice{}
|
||||
s.Set("")
|
||||
return s
|
||||
}(), "--help [--help option --help option]\t"},
|
||||
{"h", func() *StringSlice {
|
||||
s := &StringSlice{}
|
||||
s.Set("")
|
||||
return s
|
||||
}(), "-h [-h option -h option]\t"},
|
||||
{"h", func() *StringSlice {
|
||||
s := &StringSlice{}
|
||||
s.Set("")
|
||||
return s
|
||||
}(), "-h [-h option -h option]\t"},
|
||||
{"test", func() *StringSlice {
|
||||
s := &StringSlice{}
|
||||
s.Set("Something")
|
||||
return s
|
||||
}(), "--test [--test option --test option]\t"},
|
||||
}
|
||||
|
||||
func TestStringSliceFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range stringSliceFlagTests {
|
||||
flag := StringSliceFlag{Name: test.name, Value: test.value}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%q does not match %q", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_QWWX", "11,4")
|
||||
for _, test := range stringSliceFlagTests {
|
||||
flag := StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"}
|
||||
output := flag.String()
|
||||
|
||||
if !strings.HasSuffix(output, " [$APP_QWWX]") {
|
||||
t.Errorf("%q does not end with [$APP_QWWX]", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var intFlagTests = []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"help", "--help \"0\"\t"},
|
||||
{"h", "-h \"0\"\t"},
|
||||
}
|
||||
|
||||
func TestIntFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range intFlagTests {
|
||||
flag := IntFlag{Name: test.name}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_BAR", "2")
|
||||
for _, test := range intFlagTests {
|
||||
flag := IntFlag{Name: test.name, EnvVar: "APP_BAR"}
|
||||
output := flag.String()
|
||||
|
||||
if !strings.HasSuffix(output, " [$APP_BAR]") {
|
||||
t.Errorf("%s does not end with [$APP_BAR]", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var durationFlagTests = []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"help", "--help \"0\"\t"},
|
||||
{"h", "-h \"0\"\t"},
|
||||
}
|
||||
|
||||
func TestDurationFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range durationFlagTests {
|
||||
flag := DurationFlag{Name: test.name}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_BAR", "2h3m6s")
|
||||
for _, test := range durationFlagTests {
|
||||
flag := DurationFlag{Name: test.name, EnvVar: "APP_BAR"}
|
||||
output := flag.String()
|
||||
|
||||
if !strings.HasSuffix(output, " [$APP_BAR]") {
|
||||
t.Errorf("%s does not end with [$APP_BAR]", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var intSliceFlagTests = []struct {
|
||||
name string
|
||||
value *IntSlice
|
||||
expected string
|
||||
}{
|
||||
{"help", &IntSlice{}, "--help [--help option --help option]\t"},
|
||||
{"h", &IntSlice{}, "-h [-h option -h option]\t"},
|
||||
{"h", &IntSlice{}, "-h [-h option -h option]\t"},
|
||||
{"test", func() *IntSlice {
|
||||
i := &IntSlice{}
|
||||
i.Set("9")
|
||||
return i
|
||||
}(), "--test [--test option --test option]\t"},
|
||||
}
|
||||
|
||||
func TestIntSliceFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range intSliceFlagTests {
|
||||
flag := IntSliceFlag{Name: test.name, Value: test.value}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%q does not match %q", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_SMURF", "42,3")
|
||||
for _, test := range intSliceFlagTests {
|
||||
flag := IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"}
|
||||
output := flag.String()
|
||||
|
||||
if !strings.HasSuffix(output, " [$APP_SMURF]") {
|
||||
t.Errorf("%q does not end with [$APP_SMURF]", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var float64FlagTests = []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"help", "--help \"0\"\t"},
|
||||
{"h", "-h \"0\"\t"},
|
||||
}
|
||||
|
||||
func TestFloat64FlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range float64FlagTests {
|
||||
flag := Float64Flag{Name: test.name}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_BAZ", "99.4")
|
||||
for _, test := range float64FlagTests {
|
||||
flag := Float64Flag{Name: test.name, EnvVar: "APP_BAZ"}
|
||||
output := flag.String()
|
||||
|
||||
if !strings.HasSuffix(output, " [$APP_BAZ]") {
|
||||
t.Errorf("%s does not end with [$APP_BAZ]", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var genericFlagTests = []struct {
|
||||
name string
|
||||
value Generic
|
||||
expected string
|
||||
}{
|
||||
{"test", &Parser{"abc", "def"}, "--test \"abc,def\"\ttest flag"},
|
||||
{"t", &Parser{"abc", "def"}, "-t \"abc,def\"\ttest flag"},
|
||||
}
|
||||
|
||||
func TestGenericFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range genericFlagTests {
|
||||
flag := GenericFlag{Name: test.name, Value: test.value, Usage: "test flag"}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%q does not match %q", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_ZAP", "3")
|
||||
for _, test := range genericFlagTests {
|
||||
flag := GenericFlag{Name: test.name, EnvVar: "APP_ZAP"}
|
||||
output := flag.String()
|
||||
|
||||
if !strings.HasSuffix(output, " [$APP_ZAP]") {
|
||||
t.Errorf("%s does not end with [$APP_ZAP]", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMultiString(t *testing.T) {
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
StringFlag{Name: "serve, s"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.String("serve") != "10" {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.String("s") != "10" {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run", "-s", "10"})
|
||||
}
|
||||
|
||||
func TestParseMultiStringFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_COUNT", "20")
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
StringFlag{Name: "count, c", EnvVar: "APP_COUNT"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.String("count") != "20" {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.String("c") != "20" {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiStringFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_COUNT", "20")
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
StringFlag{Name: "count, c", EnvVar: "COMPAT_COUNT,APP_COUNT"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.String("count") != "20" {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.String("c") != "20" {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiStringSlice(t *testing.T) {
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
StringSliceFlag{Name: "serve, s", Value: &StringSlice{}},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.StringSlice("serve"), []string{"10", "20"}) {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if !reflect.DeepEqual(ctx.StringSlice("s"), []string{"10", "20"}) {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run", "-s", "10", "-s", "20"})
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
StringSliceFlag{Name: "intervals, i", Value: &StringSlice{}, EnvVar: "APP_INTERVALS"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
StringSliceFlag{Name: "intervals, i", Value: &StringSlice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiInt(t *testing.T) {
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
IntFlag{Name: "serve, s"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Int("serve") != 10 {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Int("s") != 10 {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run", "-s", "10"})
|
||||
}
|
||||
|
||||
func TestParseMultiIntFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_TIMEOUT_SECONDS", "10")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
IntFlag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Int("timeout") != 10 {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Int("t") != 10 {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiIntFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_TIMEOUT_SECONDS", "10")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
IntFlag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Int("timeout") != 10 {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Int("t") != 10 {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiIntSlice(t *testing.T) {
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
IntSliceFlag{Name: "serve, s", Value: &IntSlice{}},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.IntSlice("serve"), []int{10, 20}) {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if !reflect.DeepEqual(ctx.IntSlice("s"), []int{10, 20}) {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run", "-s", "10", "-s", "20"})
|
||||
}
|
||||
|
||||
func TestParseMultiIntSliceFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
IntSliceFlag{Name: "intervals, i", Value: &IntSlice{}, EnvVar: "APP_INTERVALS"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiIntSliceFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
(&App{
|
||||
Flags: []Flag{
|
||||
IntSliceFlag{Name: "intervals, i", Value: &IntSlice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiFloat64(t *testing.T) {
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
Float64Flag{Name: "serve, s"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Float64("serve") != 10.2 {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Float64("s") != 10.2 {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run", "-s", "10.2"})
|
||||
}
|
||||
|
||||
func TestParseMultiFloat64FromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Float64("timeout") != 15.5 {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Float64("t") != 15.5 {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiFloat64FromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
Float64Flag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Float64("timeout") != 15.5 {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Float64("t") != 15.5 {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiBool(t *testing.T) {
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
BoolFlag{Name: "serve, s"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Bool("serve") != true {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Bool("s") != true {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run", "--serve"})
|
||||
}
|
||||
|
||||
func TestParseMultiBoolFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_DEBUG", "1")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
BoolFlag{Name: "debug, d", EnvVar: "APP_DEBUG"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Bool("debug") != true {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if ctx.Bool("d") != true {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiBoolFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_DEBUG", "1")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
BoolFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.Bool("debug") != true {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if ctx.Bool("d") != true {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiBoolT(t *testing.T) {
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
BoolTFlag{Name: "serve, s"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.BoolT("serve") != true {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.BoolT("s") != true {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run", "--serve"})
|
||||
}
|
||||
|
||||
func TestParseMultiBoolTFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_DEBUG", "0")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.BoolT("debug") != false {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if ctx.BoolT("d") != false {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseMultiBoolTFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_DEBUG", "0")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
BoolTFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if ctx.BoolT("debug") != false {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if ctx.BoolT("d") != false {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
type Parser [2]string
|
||||
|
||||
func (p *Parser) Set(value string) error {
|
||||
parts := strings.Split(value, ",")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid format")
|
||||
}
|
||||
|
||||
(*p)[0] = parts[0]
|
||||
(*p)[1] = parts[1]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) String() string {
|
||||
return fmt.Sprintf("%s,%s", p[0], p[1])
|
||||
}
|
||||
|
||||
func TestParseGeneric(t *testing.T) {
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
GenericFlag{Name: "serve, s", Value: &Parser{}},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"10", "20"}) {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"10", "20"}) {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run", "-s", "10,20"})
|
||||
}
|
||||
|
||||
func TestParseGenericFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_SERVE", "20,30")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
GenericFlag{Name: "serve, s", Value: &Parser{}, EnvVar: "APP_SERVE"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"20", "30"}) {
|
||||
t.Errorf("main name not set from env")
|
||||
}
|
||||
if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"20", "30"}) {
|
||||
t.Errorf("short name not set from env")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
||||
|
||||
func TestParseGenericFromEnvCascade(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_FOO", "99,2000")
|
||||
a := App{
|
||||
Flags: []Flag{
|
||||
GenericFlag{Name: "foos", Value: &Parser{}, EnvVar: "COMPAT_FOO,APP_FOO"},
|
||||
},
|
||||
Action: func(ctx *Context) {
|
||||
if !reflect.DeepEqual(ctx.Generic("foos"), &Parser{"99", "2000"}) {
|
||||
t.Errorf("value not set from env")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run"})
|
||||
}
|
238
Godeps/_workspace/src/github.com/codegangsta/cli/help.go
generated
vendored
238
Godeps/_workspace/src/github.com/codegangsta/cli/help.go
generated
vendored
|
@ -1,238 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// The text template for the Default help topic.
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
var AppHelpTemplate = `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} [arguments...]
|
||||
{{if .Version}}
|
||||
VERSION:
|
||||
{{.Version}}
|
||||
{{end}}{{if len .Authors}}
|
||||
AUTHOR(S):
|
||||
{{range .Authors}}{{ . }}{{end}}
|
||||
{{end}}{{if .Commands}}
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}{{end}}{{if .Flags}}
|
||||
GLOBAL OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}{{end}}{{if .Copyright }}
|
||||
COPYRIGHT:
|
||||
{{.Copyright}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
// The text template for the command help topic.
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
var CommandHelpTemplate = `NAME:
|
||||
{{.FullName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
command {{.FullName}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}{{end}}{{if .Flags}}
|
||||
|
||||
OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}{{ end }}
|
||||
`
|
||||
|
||||
// The text template for the subcommand help topic.
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
var SubcommandHelpTemplate = `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}{{if .Flags}}
|
||||
OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
`
|
||||
|
||||
var helpCommand = Command{
|
||||
Name: "help",
|
||||
Aliases: []string{"h"},
|
||||
Usage: "Shows a list of commands or help for one command",
|
||||
Action: func(c *Context) {
|
||||
args := c.Args()
|
||||
if args.Present() {
|
||||
ShowCommandHelp(c, args.First())
|
||||
} else {
|
||||
ShowAppHelp(c)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var helpSubcommand = Command{
|
||||
Name: "help",
|
||||
Aliases: []string{"h"},
|
||||
Usage: "Shows a list of commands or help for one command",
|
||||
Action: func(c *Context) {
|
||||
args := c.Args()
|
||||
if args.Present() {
|
||||
ShowCommandHelp(c, args.First())
|
||||
} else {
|
||||
ShowSubcommandHelp(c)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Prints help for the App or Command
|
||||
type helpPrinter func(w io.Writer, templ string, data interface{})
|
||||
|
||||
var HelpPrinter helpPrinter = printHelp
|
||||
|
||||
// Prints version for the App
|
||||
var VersionPrinter = printVersion
|
||||
|
||||
func ShowAppHelp(c *Context) {
|
||||
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
|
||||
}
|
||||
|
||||
// Prints the list of subcommands as the default app completion method
|
||||
func DefaultAppComplete(c *Context) {
|
||||
for _, command := range c.App.Commands {
|
||||
for _, name := range command.Names() {
|
||||
fmt.Fprintln(c.App.Writer, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prints help for the given command
|
||||
func ShowCommandHelp(ctx *Context, command string) {
|
||||
// show the subcommand help for a command with subcommands
|
||||
if command == "" {
|
||||
HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App)
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range ctx.App.Commands {
|
||||
if c.HasName(command) {
|
||||
HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.App.CommandNotFound != nil {
|
||||
ctx.App.CommandNotFound(ctx, command)
|
||||
} else {
|
||||
fmt.Fprintf(ctx.App.Writer, "No help topic for '%v'\n", command)
|
||||
}
|
||||
}
|
||||
|
||||
// Prints help for the given subcommand
|
||||
func ShowSubcommandHelp(c *Context) {
|
||||
ShowCommandHelp(c, c.Command.Name)
|
||||
}
|
||||
|
||||
// Prints the version number of the App
|
||||
func ShowVersion(c *Context) {
|
||||
VersionPrinter(c)
|
||||
}
|
||||
|
||||
func printVersion(c *Context) {
|
||||
fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version)
|
||||
}
|
||||
|
||||
// Prints the lists of commands within a given context
|
||||
func ShowCompletions(c *Context) {
|
||||
a := c.App
|
||||
if a != nil && a.BashComplete != nil {
|
||||
a.BashComplete(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Prints the custom completions for a given command
|
||||
func ShowCommandCompletions(ctx *Context, command string) {
|
||||
c := ctx.App.Command(command)
|
||||
if c != nil && c.BashComplete != nil {
|
||||
c.BashComplete(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp(out io.Writer, templ string, data interface{}) {
|
||||
funcMap := template.FuncMap{
|
||||
"join": strings.Join,
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0)
|
||||
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
|
||||
err := t.Execute(w, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func checkVersion(c *Context) bool {
|
||||
if c.GlobalBool("version") || c.GlobalBool("v") || c.Bool("version") || c.Bool("v") {
|
||||
ShowVersion(c)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkHelp(c *Context) bool {
|
||||
if c.GlobalBool("h") || c.GlobalBool("help") || c.Bool("h") || c.Bool("help") {
|
||||
ShowAppHelp(c)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkCommandHelp(c *Context, name string) bool {
|
||||
if c.Bool("h") || c.Bool("help") {
|
||||
ShowCommandHelp(c, name)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkSubcommandHelp(c *Context) bool {
|
||||
if c.GlobalBool("h") || c.GlobalBool("help") {
|
||||
ShowSubcommandHelp(c)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkCompletions(c *Context) bool {
|
||||
if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion {
|
||||
ShowCompletions(c)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkCommandCompletions(c *Context, name string) bool {
|
||||
if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion {
|
||||
ShowCommandCompletions(c, name)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
36
Godeps/_workspace/src/github.com/codegangsta/cli/help_test.go
generated
vendored
36
Godeps/_workspace/src/github.com/codegangsta/cli/help_test.go
generated
vendored
|
@ -1,36 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ShowAppHelp_NoAuthor(t *testing.T) {
|
||||
output := new(bytes.Buffer)
|
||||
app := NewApp()
|
||||
app.Writer = output
|
||||
|
||||
c := NewContext(app, nil, nil)
|
||||
|
||||
ShowAppHelp(c)
|
||||
|
||||
if bytes.Index(output.Bytes(), []byte("AUTHOR(S):")) != -1 {
|
||||
t.Errorf("expected\n%snot to include %s", output.String(), "AUTHOR(S):")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ShowAppHelp_NoVersion(t *testing.T) {
|
||||
output := new(bytes.Buffer)
|
||||
app := NewApp()
|
||||
app.Writer = output
|
||||
|
||||
app.Version = ""
|
||||
|
||||
c := NewContext(app, nil, nil)
|
||||
|
||||
ShowAppHelp(c)
|
||||
|
||||
if bytes.Index(output.Bytes(), []byte("VERSION:")) != -1 {
|
||||
t.Errorf("expected\n%snot to include %s", output.String(), "VERSION:")
|
||||
}
|
||||
}
|
19
Godeps/_workspace/src/github.com/codegangsta/cli/helpers_test.go
generated
vendored
19
Godeps/_workspace/src/github.com/codegangsta/cli/helpers_test.go
generated
vendored
|
@ -1,19 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/* Test Helpers */
|
||||
func expect(t *testing.T, a interface{}, b interface{}) {
|
||||
if a != b {
|
||||
t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
|
||||
func refute(t *testing.T, a interface{}, b interface{}) {
|
||||
if a == b {
|
||||
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
97
Godeps/_workspace/src/github.com/elazarl/go-bindata-assetfs/go-bindata-assetfs/main.go
generated
vendored
97
Godeps/_workspace/src/github.com/elazarl/go-bindata-assetfs/go-bindata-assetfs/main.go
generated
vendored
|
@ -1,97 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const bindatafile = "bindata.go"
|
||||
|
||||
func isDebug(args []string) bool {
|
||||
flagset := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
debug := flagset.Bool("debug", false, "")
|
||||
debugArgs := make([]string, 0)
|
||||
for _, arg := range args {
|
||||
if strings.HasPrefix(arg, "-debug") {
|
||||
debugArgs = append(debugArgs, arg)
|
||||
}
|
||||
}
|
||||
flagset.Parse(debugArgs)
|
||||
if debug == nil {
|
||||
return false
|
||||
}
|
||||
return *debug
|
||||
}
|
||||
|
||||
func main() {
|
||||
if _, err := exec.LookPath("go-bindata"); err != nil {
|
||||
fmt.Println("Cannot find go-bindata executable in path")
|
||||
fmt.Println("Maybe you need: go get github.com/elazarl/go-bindata-assetfs/...")
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := exec.Command("go-bindata", os.Args[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
in, err := os.Open(bindatafile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Cannot read", bindatafile, err)
|
||||
return
|
||||
}
|
||||
out, err := os.Create("bindata_assetfs.go")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Cannot write 'bindata_assetfs.go'", err)
|
||||
return
|
||||
}
|
||||
debug := isDebug(os.Args[1:])
|
||||
r := bufio.NewReader(in)
|
||||
done := false
|
||||
for line, isPrefix, err := r.ReadLine(); err == nil; line, isPrefix, err = r.ReadLine() {
|
||||
if !isPrefix {
|
||||
line = append(line, '\n')
|
||||
}
|
||||
if _, err := out.Write(line); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Cannot write to 'bindata_assetfs.go'", err)
|
||||
return
|
||||
}
|
||||
if !done && !isPrefix && bytes.HasPrefix(line, []byte("import (")) {
|
||||
if debug {
|
||||
fmt.Fprintln(out, "\t\"net/http\"")
|
||||
} else {
|
||||
fmt.Fprintln(out, "\t\"github.com/elazarl/go-bindata-assetfs\"")
|
||||
}
|
||||
done = true
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
fmt.Fprintln(out, `
|
||||
func assetFS() http.FileSystem {
|
||||
for k := range _bintree.Children {
|
||||
return http.Dir(k)
|
||||
}
|
||||
panic("unreachable")
|
||||
}`)
|
||||
} else {
|
||||
fmt.Fprintln(out, `
|
||||
func assetFS() *assetfs.AssetFS {
|
||||
for k := range _bintree.Children {
|
||||
return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: k}
|
||||
}
|
||||
panic("unreachable")
|
||||
}`)
|
||||
}
|
||||
// Close files BEFORE remove calls (don't use defer).
|
||||
in.Close()
|
||||
out.Close()
|
||||
if err := os.Remove(bindatafile); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Cannot remove", bindatafile, err)
|
||||
}
|
||||
}
|
19
Godeps/_workspace/src/github.com/gorilla/websocket/bench_test.go
generated
vendored
19
Godeps/_workspace/src/github.com/gorilla/websocket/bench_test.go
generated
vendored
|
@ -1,19 +0,0 @@
|
|||
// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkMaskBytes(b *testing.B) {
|
||||
var key [4]byte
|
||||
data := make([]byte, 1024)
|
||||
pos := 0
|
||||
for i := 0; i < b.N; i++ {
|
||||
pos = maskBytes(key, pos, data)
|
||||
}
|
||||
b.SetBytes(int64(len(data)))
|
||||
}
|
323
Godeps/_workspace/src/github.com/gorilla/websocket/client_server_test.go
generated
vendored
323
Godeps/_workspace/src/github.com/gorilla/websocket/client_server_test.go
generated
vendored
|
@ -1,323 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var cstUpgrader = Upgrader{
|
||||
Subprotocols: []string{"p0", "p1"},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
||||
http.Error(w, reason.Error(), status)
|
||||
},
|
||||
}
|
||||
|
||||
var cstDialer = Dialer{
|
||||
Subprotocols: []string{"p1", "p2"},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
type cstHandler struct{ *testing.T }
|
||||
|
||||
type cstServer struct {
|
||||
*httptest.Server
|
||||
URL string
|
||||
}
|
||||
|
||||
func newServer(t *testing.T) *cstServer {
|
||||
var s cstServer
|
||||
s.Server = httptest.NewServer(cstHandler{t})
|
||||
s.URL = makeWsProto(s.Server.URL)
|
||||
return &s
|
||||
}
|
||||
|
||||
func newTLSServer(t *testing.T) *cstServer {
|
||||
var s cstServer
|
||||
s.Server = httptest.NewTLSServer(cstHandler{t})
|
||||
s.URL = makeWsProto(s.Server.URL)
|
||||
return &s
|
||||
}
|
||||
|
||||
func (t cstHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
t.Logf("method %s not allowed", r.Method)
|
||||
http.Error(w, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
subprotos := Subprotocols(r)
|
||||
if !reflect.DeepEqual(subprotos, cstDialer.Subprotocols) {
|
||||
t.Logf("subprotols=%v, want %v", subprotos, cstDialer.Subprotocols)
|
||||
http.Error(w, "bad protocol", 400)
|
||||
return
|
||||
}
|
||||
ws, err := cstUpgrader.Upgrade(w, r, http.Header{"Set-Cookie": {"sessionID=1234"}})
|
||||
if err != nil {
|
||||
t.Logf("Upgrade: %v", err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
if ws.Subprotocol() != "p1" {
|
||||
t.Logf("Subprotocol() = %s, want p1", ws.Subprotocol())
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
op, rd, err := ws.NextReader()
|
||||
if err != nil {
|
||||
t.Logf("NextReader: %v", err)
|
||||
return
|
||||
}
|
||||
wr, err := ws.NextWriter(op)
|
||||
if err != nil {
|
||||
t.Logf("NextWriter: %v", err)
|
||||
return
|
||||
}
|
||||
if _, err = io.Copy(wr, rd); err != nil {
|
||||
t.Logf("NextWriter: %v", err)
|
||||
return
|
||||
}
|
||||
if err := wr.Close(); err != nil {
|
||||
t.Logf("Close: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func makeWsProto(s string) string {
|
||||
return "ws" + strings.TrimPrefix(s, "http")
|
||||
}
|
||||
|
||||
func sendRecv(t *testing.T, ws *Conn) {
|
||||
const message = "Hello World!"
|
||||
if err := ws.SetWriteDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
t.Fatalf("SetWriteDeadline: %v", err)
|
||||
}
|
||||
if err := ws.WriteMessage(TextMessage, []byte(message)); err != nil {
|
||||
t.Fatalf("WriteMessage: %v", err)
|
||||
}
|
||||
if err := ws.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
_, p, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
t.Fatalf("ReadMessage: %v", err)
|
||||
}
|
||||
if string(p) != message {
|
||||
t.Fatalf("message=%s, want %s", p, message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDial(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
|
||||
func TestDialTLS(t *testing.T) {
|
||||
s := newTLSServer(t)
|
||||
defer s.Close()
|
||||
|
||||
certs := x509.NewCertPool()
|
||||
for _, c := range s.TLS.Certificates {
|
||||
roots, err := x509.ParseCertificates(c.Certificate[len(c.Certificate)-1])
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing server's root cert: %v", err)
|
||||
}
|
||||
for _, root := range roots {
|
||||
certs.AddCert(root)
|
||||
}
|
||||
}
|
||||
|
||||
u, _ := url.Parse(s.URL)
|
||||
d := cstDialer
|
||||
d.NetDial = func(network, addr string) (net.Conn, error) { return net.Dial(network, u.Host) }
|
||||
d.TLSClientConfig = &tls.Config{RootCAs: certs}
|
||||
ws, _, err := d.Dial("wss://example.com/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
|
||||
func xTestDialTLSBadCert(t *testing.T) {
|
||||
// This test is deactivated because of noisy logging from the net/http package.
|
||||
s := newTLSServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.URL, nil)
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
}
|
||||
|
||||
func xTestDialTLSNoVerify(t *testing.T) {
|
||||
s := newTLSServer(t)
|
||||
defer s.Close()
|
||||
|
||||
d := cstDialer
|
||||
d.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
ws, _, err := d.Dial(s.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
|
||||
func TestDialTimeout(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
d := cstDialer
|
||||
d.HandshakeTimeout = -1
|
||||
ws, _, err := d.Dial(s.URL, nil)
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialBadScheme(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, _, err := cstDialer.Dial(s.Server.URL, nil)
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialBadOrigin(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Origin": {"bad"}})
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("resp=nil, err=%v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("status=%d, want %d", resp.StatusCode, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandshake(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Origin": {s.URL}})
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
var sessionID string
|
||||
for _, c := range resp.Cookies() {
|
||||
if c.Name == "sessionID" {
|
||||
sessionID = c.Value
|
||||
}
|
||||
}
|
||||
if sessionID != "1234" {
|
||||
t.Error("Set-Cookie not received from the server.")
|
||||
}
|
||||
|
||||
if ws.Subprotocol() != "p1" {
|
||||
t.Errorf("ws.Subprotocol() = %s, want p1", ws.Subprotocol())
|
||||
}
|
||||
sendRecv(t, ws)
|
||||
}
|
||||
|
||||
func TestRespOnBadHandshake(t *testing.T) {
|
||||
const expectedStatus = http.StatusGone
|
||||
const expectedBody = "This is the response body."
|
||||
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(expectedStatus)
|
||||
io.WriteString(w, expectedBody)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ws, resp, err := cstDialer.Dial(makeWsProto(s.URL), nil)
|
||||
if err == nil {
|
||||
ws.Close()
|
||||
t.Fatalf("Dial: nil")
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatalf("resp=nil, err=%v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != expectedStatus {
|
||||
t.Errorf("resp.StatusCode=%d, want %d", resp.StatusCode, expectedStatus)
|
||||
}
|
||||
|
||||
p, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFull(resp.Body) returned error %v", err)
|
||||
}
|
||||
|
||||
if string(p) != expectedBody {
|
||||
t.Errorf("resp.Body=%s, want %s", p, expectedBody)
|
||||
}
|
||||
}
|
||||
|
||||
// If the Host header is specified in `Dial()`, the server must receive it as
|
||||
// the `Host:` header.
|
||||
func TestHostHeader(t *testing.T) {
|
||||
s := newServer(t)
|
||||
defer s.Close()
|
||||
|
||||
specifiedHost := make(chan string, 1)
|
||||
origHandler := s.Server.Config.Handler
|
||||
|
||||
// Capture the request Host header.
|
||||
s.Server.Config.Handler = http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
specifiedHost <- r.Host
|
||||
origHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
ws, resp, err := cstDialer.Dial(s.URL, http.Header{"Host": {"testhost"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
t.Fatalf("resp.StatusCode = %v, want http.StatusSwitchingProtocols", resp.StatusCode)
|
||||
}
|
||||
|
||||
if gotHost := <-specifiedHost; gotHost != "testhost" {
|
||||
t.Fatalf("gotHost = %q, want \"testhost\"", gotHost)
|
||||
}
|
||||
|
||||
sendRecv(t, ws)
|
||||
}
|
64
Godeps/_workspace/src/github.com/gorilla/websocket/client_test.go
generated
vendored
64
Godeps/_workspace/src/github.com/gorilla/websocket/client_test.go
generated
vendored
|
@ -1,64 +0,0 @@
|
|||
// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var parseURLTests = []struct {
|
||||
s string
|
||||
u *url.URL
|
||||
}{
|
||||
{"ws://example.com/", &url.URL{Scheme: "ws", Host: "example.com", Opaque: "/"}},
|
||||
{"ws://example.com", &url.URL{Scheme: "ws", Host: "example.com", Opaque: "/"}},
|
||||
{"ws://example.com:7777/", &url.URL{Scheme: "ws", Host: "example.com:7777", Opaque: "/"}},
|
||||
{"wss://example.com/", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/"}},
|
||||
{"wss://example.com/a/b", &url.URL{Scheme: "wss", Host: "example.com", Opaque: "/a/b"}},
|
||||
{"ss://example.com/a/b", nil},
|
||||
{"ws://webmaster@example.com/", nil},
|
||||
}
|
||||
|
||||
func TestParseURL(t *testing.T) {
|
||||
for _, tt := range parseURLTests {
|
||||
u, err := parseURL(tt.s)
|
||||
if tt.u != nil && err != nil {
|
||||
t.Errorf("parseURL(%q) returned error %v", tt.s, err)
|
||||
continue
|
||||
}
|
||||
if tt.u == nil && err == nil {
|
||||
t.Errorf("parseURL(%q) did not return error", tt.s)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(u, tt.u) {
|
||||
t.Errorf("parseURL(%q) returned %v, want %v", tt.s, u, tt.u)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hostPortNoPortTests = []struct {
|
||||
u *url.URL
|
||||
hostPort, hostNoPort string
|
||||
}{
|
||||
{&url.URL{Scheme: "ws", Host: "example.com"}, "example.com:80", "example.com"},
|
||||
{&url.URL{Scheme: "wss", Host: "example.com"}, "example.com:443", "example.com"},
|
||||
{&url.URL{Scheme: "ws", Host: "example.com:7777"}, "example.com:7777", "example.com"},
|
||||
{&url.URL{Scheme: "wss", Host: "example.com:7777"}, "example.com:7777", "example.com"},
|
||||
}
|
||||
|
||||
func TestHostPortNoPort(t *testing.T) {
|
||||
for _, tt := range hostPortNoPortTests {
|
||||
hostPort, hostNoPort := hostPortNoPort(tt.u)
|
||||
if hostPort != tt.hostPort {
|
||||
t.Errorf("hostPortNoPort(%v) returned hostPort %q, want %q", tt.u, hostPort, tt.hostPort)
|
||||
}
|
||||
if hostNoPort != tt.hostNoPort {
|
||||
t.Errorf("hostPortNoPort(%v) returned hostNoPort %q, want %q", tt.u, hostNoPort, tt.hostNoPort)
|
||||
}
|
||||
}
|
||||
}
|
241
Godeps/_workspace/src/github.com/gorilla/websocket/conn_test.go
generated
vendored
241
Godeps/_workspace/src/github.com/gorilla/websocket/conn_test.go
generated
vendored
|
@ -1,241 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ net.Error = errWriteTimeout
|
||||
|
||||
type fakeNetConn struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (c fakeNetConn) Close() error { return nil }
|
||||
func (c fakeNetConn) LocalAddr() net.Addr { return nil }
|
||||
func (c fakeNetConn) RemoteAddr() net.Addr { return nil }
|
||||
func (c fakeNetConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c fakeNetConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c fakeNetConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func TestFraming(t *testing.T) {
|
||||
frameSizes := []int{0, 1, 2, 124, 125, 126, 127, 128, 129, 65534, 65535, 65536, 65537}
|
||||
var readChunkers = []struct {
|
||||
name string
|
||||
f func(io.Reader) io.Reader
|
||||
}{
|
||||
{"half", iotest.HalfReader},
|
||||
{"one", iotest.OneByteReader},
|
||||
{"asis", func(r io.Reader) io.Reader { return r }},
|
||||
}
|
||||
|
||||
writeBuf := make([]byte, 65537)
|
||||
for i := range writeBuf {
|
||||
writeBuf[i] = byte(i)
|
||||
}
|
||||
|
||||
for _, isServer := range []bool{true, false} {
|
||||
for _, chunker := range readChunkers {
|
||||
|
||||
var connBuf bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &connBuf}, isServer, 1024, 1024)
|
||||
rc := newConn(fakeNetConn{Reader: chunker.f(&connBuf), Writer: nil}, !isServer, 1024, 1024)
|
||||
|
||||
for _, n := range frameSizes {
|
||||
for _, iocopy := range []bool{true, false} {
|
||||
name := fmt.Sprintf("s:%v, r:%s, n:%d c:%v", isServer, chunker.name, n, iocopy)
|
||||
|
||||
w, err := wc.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
t.Errorf("%s: wc.NextWriter() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
var nn int
|
||||
if iocopy {
|
||||
var n64 int64
|
||||
n64, err = io.Copy(w, bytes.NewReader(writeBuf[:n]))
|
||||
nn = int(n64)
|
||||
} else {
|
||||
nn, err = w.Write(writeBuf[:n])
|
||||
}
|
||||
if err != nil || nn != n {
|
||||
t.Errorf("%s: w.Write(writeBuf[:n]) returned %d, %v", name, nn, err)
|
||||
continue
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
t.Errorf("%s: w.Close() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
opCode, r, err := rc.NextReader()
|
||||
if err != nil || opCode != TextMessage {
|
||||
t.Errorf("%s: NextReader() returned %d, r, %v", name, opCode, err)
|
||||
continue
|
||||
}
|
||||
rbuf, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Errorf("%s: ReadFull() returned rbuf, %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(rbuf) != n {
|
||||
t.Errorf("%s: len(rbuf) is %d, want %d", name, len(rbuf), n)
|
||||
continue
|
||||
}
|
||||
|
||||
for i, b := range rbuf {
|
||||
if byte(i) != b {
|
||||
t.Errorf("%s: bad byte at offset %d", name, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestControl(t *testing.T) {
|
||||
const message = "this is a ping/pong messsage"
|
||||
for _, isServer := range []bool{true, false} {
|
||||
for _, isWriteControl := range []bool{true, false} {
|
||||
name := fmt.Sprintf("s:%v, wc:%v", isServer, isWriteControl)
|
||||
var connBuf bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &connBuf}, isServer, 1024, 1024)
|
||||
rc := newConn(fakeNetConn{Reader: &connBuf, Writer: nil}, !isServer, 1024, 1024)
|
||||
if isWriteControl {
|
||||
wc.WriteControl(PongMessage, []byte(message), time.Now().Add(time.Second))
|
||||
} else {
|
||||
w, err := wc.NextWriter(PongMessage)
|
||||
if err != nil {
|
||||
t.Errorf("%s: wc.NextWriter() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
if _, err := w.Write([]byte(message)); err != nil {
|
||||
t.Errorf("%s: w.Write() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Errorf("%s: w.Close() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
var actualMessage string
|
||||
rc.SetPongHandler(func(s string) error { actualMessage = s; return nil })
|
||||
rc.NextReader()
|
||||
if actualMessage != message {
|
||||
t.Errorf("%s: pong=%q, want %q", name, actualMessage, message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseBeforeFinalFrame(t *testing.T) {
|
||||
const bufSize = 512
|
||||
|
||||
expectedErr := &CloseError{Code: CloseNormalClosure, Text: "hello"}
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, bufSize)
|
||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024)
|
||||
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
w.Write(make([]byte, bufSize+bufSize/2))
|
||||
wc.WriteControl(CloseMessage, FormatCloseMessage(expectedErr.Code, expectedErr.Text), time.Now().Add(10*time.Second))
|
||||
w.Close()
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(ioutil.Discard, r)
|
||||
if !reflect.DeepEqual(err, expectedErr) {
|
||||
t.Fatalf("io.Copy() returned %v, want %v", err, expectedErr)
|
||||
}
|
||||
_, _, err = rc.NextReader()
|
||||
if !reflect.DeepEqual(err, expectedErr) {
|
||||
t.Fatalf("NextReader() returned %v, want %v", err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEOFBeforeFinalFrame(t *testing.T) {
|
||||
const bufSize = 512
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, bufSize)
|
||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024)
|
||||
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
w.Write(make([]byte, bufSize+bufSize/2))
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(ioutil.Discard, r)
|
||||
if err != errUnexpectedEOF {
|
||||
t.Fatalf("io.Copy() returned %v, want %v", err, errUnexpectedEOF)
|
||||
}
|
||||
_, _, err = rc.NextReader()
|
||||
if err != errUnexpectedEOF {
|
||||
t.Fatalf("NextReader() returned %v, want %v", err, errUnexpectedEOF)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLimit(t *testing.T) {
|
||||
|
||||
const readLimit = 512
|
||||
message := make([]byte, readLimit+1)
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, readLimit-2)
|
||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, 1024, 1024)
|
||||
rc.SetReadLimit(readLimit)
|
||||
|
||||
// Send message at the limit with interleaved pong.
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
w.Write(message[:readLimit-1])
|
||||
wc.WriteControl(PongMessage, []byte("this is a pong"), time.Now().Add(10*time.Second))
|
||||
w.Write(message[:1])
|
||||
w.Close()
|
||||
|
||||
// Send message larger than the limit.
|
||||
wc.WriteMessage(BinaryMessage, message[:readLimit+1])
|
||||
|
||||
op, _, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("1: NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("2: NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(ioutil.Discard, r)
|
||||
if err != ErrReadLimit {
|
||||
t.Fatalf("io.Copy() returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlyingConn(t *testing.T) {
|
||||
var b1, b2 bytes.Buffer
|
||||
fc := fakeNetConn{Reader: &b1, Writer: &b2}
|
||||
c := newConn(fc, true, 1024, 1024)
|
||||
ul := c.UnderlyingConn()
|
||||
if ul != fc {
|
||||
t.Fatalf("Underlying conn is not what it should be.")
|
||||
}
|
||||
}
|
13
Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/README.md
generated
vendored
13
Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/README.md
generated
vendored
|
@ -1,13 +0,0 @@
|
|||
# Test Server
|
||||
|
||||
This package contains a server for the [Autobahn WebSockets Test Suite](http://autobahn.ws/testsuite).
|
||||
|
||||
To test the server, run
|
||||
|
||||
go run server.go
|
||||
|
||||
and start the client test driver
|
||||
|
||||
wstest -m fuzzingclient -s fuzzingclient.json
|
||||
|
||||
When the client completes, it writes a report to reports/clients/index.html.
|
14
Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/fuzzingclient.json
generated
vendored
14
Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/fuzzingclient.json
generated
vendored
|
@ -1,14 +0,0 @@
|
|||
|
||||
{
|
||||
"options": {"failByDrop": false},
|
||||
"outdir": "./reports/clients",
|
||||
"servers": [
|
||||
{"agent": "ReadAllWriteMessage", "url": "ws://localhost:9000/m", "options": {"version": 18}},
|
||||
{"agent": "ReadAllWrite", "url": "ws://localhost:9000/r", "options": {"version": 18}},
|
||||
{"agent": "CopyFull", "url": "ws://localhost:9000/f", "options": {"version": 18}},
|
||||
{"agent": "CopyWriterOnly", "url": "ws://localhost:9000/c", "options": {"version": 18}}
|
||||
],
|
||||
"cases": ["*"],
|
||||
"exclude-cases": [],
|
||||
"exclude-agent-cases": {}
|
||||
}
|
246
Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/server.go
generated
vendored
246
Godeps/_workspace/src/github.com/gorilla/websocket/examples/autobahn/server.go
generated
vendored
|
@ -1,246 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Command server is a test server for the Autobahn WebSockets Test Suite.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"github.com/gorilla/websocket"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// echoCopy echoes messages from the client using io.Copy.
|
||||
func echoCopy(w http.ResponseWriter, r *http.Request, writerOnly bool) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, r, err := conn.NextReader()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Println("NextReader:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
r = &validator{r: r}
|
||||
}
|
||||
w, err := conn.NextWriter(mt)
|
||||
if err != nil {
|
||||
log.Println("NextWriter:", err)
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
r = &validator{r: r}
|
||||
}
|
||||
if writerOnly {
|
||||
_, err = io.Copy(struct{ io.Writer }{w}, r)
|
||||
} else {
|
||||
_, err = io.Copy(w, r)
|
||||
}
|
||||
if err != nil {
|
||||
if err == errInvalidUTF8 {
|
||||
conn.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""),
|
||||
time.Time{})
|
||||
}
|
||||
log.Println("Copy:", err)
|
||||
return
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
log.Println("Close:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func echoCopyWriterOnly(w http.ResponseWriter, r *http.Request) {
|
||||
echoCopy(w, r, true)
|
||||
}
|
||||
|
||||
func echoCopyFull(w http.ResponseWriter, r *http.Request) {
|
||||
echoCopy(w, r, false)
|
||||
}
|
||||
|
||||
// echoReadAll echoes messages from the client by reading the entire message
|
||||
// with ioutil.ReadAll.
|
||||
func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage bool) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, b, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Println("NextReader:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
if !utf8.Valid(b) {
|
||||
conn.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""),
|
||||
time.Time{})
|
||||
log.Println("ReadAll: invalid utf8")
|
||||
}
|
||||
}
|
||||
if writeMessage {
|
||||
err = conn.WriteMessage(mt, b)
|
||||
if err != nil {
|
||||
log.Println("WriteMessage:", err)
|
||||
}
|
||||
} else {
|
||||
w, err := conn.NextWriter(mt)
|
||||
if err != nil {
|
||||
log.Println("NextWriter:", err)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
log.Println("Writer:", err)
|
||||
return
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
log.Println("Close:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func echoReadAllWriter(w http.ResponseWriter, r *http.Request) {
|
||||
echoReadAll(w, r, false)
|
||||
}
|
||||
|
||||
func echoReadAllWriteMessage(w http.ResponseWriter, r *http.Request) {
|
||||
echoReadAll(w, r, true)
|
||||
}
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found.", 404)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
io.WriteString(w, "<html><body>Echo Server</body></html>")
|
||||
}
|
||||
|
||||
var addr = flag.String("addr", ":9000", "http service address")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/c", echoCopyWriterOnly)
|
||||
http.HandleFunc("/f", echoCopyFull)
|
||||
http.HandleFunc("/r", echoReadAllWriter)
|
||||
http.HandleFunc("/m", echoReadAllWriteMessage)
|
||||
err := http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
type validator struct {
|
||||
state int
|
||||
x rune
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
var errInvalidUTF8 = errors.New("invalid utf8")
|
||||
|
||||
func (r *validator) Read(p []byte) (int, error) {
|
||||
n, err := r.r.Read(p)
|
||||
state := r.state
|
||||
x := r.x
|
||||
for _, b := range p[:n] {
|
||||
state, x = decode(state, x, b)
|
||||
if state == utf8Reject {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.state = state
|
||||
r.x = x
|
||||
if state == utf8Reject || (err == io.EOF && state != utf8Accept) {
|
||||
return n, errInvalidUTF8
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// UTF-8 decoder from http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||
//
|
||||
// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
var utf8d = [...]byte{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7f
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9f
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..bf
|
||||
8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..df
|
||||
0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef
|
||||
0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff
|
||||
0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
|
||||
1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
|
||||
1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
|
||||
1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // s7..s8
|
||||
}
|
||||
|
||||
const (
|
||||
utf8Accept = 0
|
||||
utf8Reject = 1
|
||||
)
|
||||
|
||||
func decode(state int, x rune, b byte) (int, rune) {
|
||||
t := utf8d[b]
|
||||
if state != utf8Accept {
|
||||
x = rune(b&0x3f) | (x << 6)
|
||||
} else {
|
||||
x = rune((0xff >> t) & b)
|
||||
}
|
||||
state = int(utf8d[256+state*16+int(t)])
|
||||
return state, x
|
||||
}
|
20
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/README.md
generated
vendored
20
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/README.md
generated
vendored
|
@ -1,20 +0,0 @@
|
|||
# Chat Example
|
||||
|
||||
This application shows how to use use the
|
||||
[websocket](https://github.com/gorilla/websocket) package and
|
||||
[jQuery](http://jquery.com) to implement a simple web chat application.
|
||||
|
||||
## Running the example
|
||||
|
||||
The example requires a working Go development environment. The [Getting
|
||||
Started](http://golang.org/doc/install) page describes how to install the
|
||||
development environment.
|
||||
|
||||
Once you have Go up and running, you can download, build and run the example
|
||||
using the following commands.
|
||||
|
||||
$ go get github.com/gorilla/websocket
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/chat`
|
||||
$ go run *.go
|
||||
|
||||
To use the chat example, open http://localhost:8080/ in your browser.
|
106
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/conn.go
generated
vendored
106
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/conn.go
generated
vendored
|
@ -1,106 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Maximum message size allowed from peer.
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// connection is an middleman between the websocket connection and the hub.
|
||||
type connection struct {
|
||||
// The websocket connection.
|
||||
ws *websocket.Conn
|
||||
|
||||
// Buffered channel of outbound messages.
|
||||
send chan []byte
|
||||
}
|
||||
|
||||
// readPump pumps messages from the websocket connection to the hub.
|
||||
func (c *connection) readPump() {
|
||||
defer func() {
|
||||
h.unregister <- c
|
||||
c.ws.Close()
|
||||
}()
|
||||
c.ws.SetReadLimit(maxMessageSize)
|
||||
c.ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, message, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
h.broadcast <- message
|
||||
}
|
||||
}
|
||||
|
||||
// write writes a message with the given message type and payload.
|
||||
func (c *connection) write(mt int, payload []byte) error {
|
||||
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
return c.ws.WriteMessage(mt, payload)
|
||||
}
|
||||
|
||||
// writePump pumps messages from the hub to the websocket connection.
|
||||
func (c *connection) writePump() {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
c.ws.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-c.send:
|
||||
if !ok {
|
||||
c.write(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
if err := c.write(websocket.TextMessage, message); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serverWs handles websocket requests from the peer.
|
||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
c := &connection{send: make(chan []byte, 256), ws: ws}
|
||||
h.register <- c
|
||||
go c.writePump()
|
||||
c.readPump()
|
||||
}
|
92
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/home.html
generated
vendored
92
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/home.html
generated
vendored
|
@ -1,92 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Chat Example</title>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
|
||||
var conn;
|
||||
var msg = $("#msg");
|
||||
var log = $("#log");
|
||||
|
||||
function appendLog(msg) {
|
||||
var d = log[0]
|
||||
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
|
||||
msg.appendTo(log)
|
||||
if (doScroll) {
|
||||
d.scrollTop = d.scrollHeight - d.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
$("#form").submit(function() {
|
||||
if (!conn) {
|
||||
return false;
|
||||
}
|
||||
if (!msg.val()) {
|
||||
return false;
|
||||
}
|
||||
conn.send(msg.val());
|
||||
msg.val("");
|
||||
return false
|
||||
});
|
||||
|
||||
if (window["WebSocket"]) {
|
||||
conn = new WebSocket("ws://{{$}}/ws");
|
||||
conn.onclose = function(evt) {
|
||||
appendLog($("<div><b>Connection closed.</b></div>"))
|
||||
}
|
||||
conn.onmessage = function(evt) {
|
||||
appendLog($("<div/>").text(evt.data))
|
||||
}
|
||||
} else {
|
||||
appendLog($("<div><b>Your browser does not support WebSockets.</b></div>"))
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
#log {
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
right: 0.5em;
|
||||
bottom: 3em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#form {
|
||||
padding: 0 0.5em 0 0.5em;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="log"></div>
|
||||
<form id="form">
|
||||
<input type="submit" value="Send" />
|
||||
<input type="text" id="msg" size="64"/>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
51
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/hub.go
generated
vendored
51
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/hub.go
generated
vendored
|
@ -1,51 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
// hub maintains the set of active connections and broadcasts messages to the
|
||||
// connections.
|
||||
type hub struct {
|
||||
// Registered connections.
|
||||
connections map[*connection]bool
|
||||
|
||||
// Inbound messages from the connections.
|
||||
broadcast chan []byte
|
||||
|
||||
// Register requests from the connections.
|
||||
register chan *connection
|
||||
|
||||
// Unregister requests from connections.
|
||||
unregister chan *connection
|
||||
}
|
||||
|
||||
var h = hub{
|
||||
broadcast: make(chan []byte),
|
||||
register: make(chan *connection),
|
||||
unregister: make(chan *connection),
|
||||
connections: make(map[*connection]bool),
|
||||
}
|
||||
|
||||
func (h *hub) run() {
|
||||
for {
|
||||
select {
|
||||
case c := <-h.register:
|
||||
h.connections[c] = true
|
||||
case c := <-h.unregister:
|
||||
if _, ok := h.connections[c]; ok {
|
||||
delete(h.connections, c)
|
||||
close(c.send)
|
||||
}
|
||||
case m := <-h.broadcast:
|
||||
for c := range h.connections {
|
||||
select {
|
||||
case c.send <- m:
|
||||
default:
|
||||
close(c.send)
|
||||
delete(h.connections, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/main.go
generated
vendored
39
Godeps/_workspace/src/github.com/gorilla/websocket/examples/chat/main.go
generated
vendored
|
@ -1,39 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var addr = flag.String("addr", ":8080", "http service address")
|
||||
var homeTempl = template.Must(template.ParseFiles("home.html"))
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found", 404)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
homeTempl.Execute(w, r.Host)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
go h.run()
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", serveWs)
|
||||
err := http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
9
Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/README.md
generated
vendored
9
Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/README.md
generated
vendored
|
@ -1,9 +0,0 @@
|
|||
# File Watch example.
|
||||
|
||||
This example sends a file to the browser client for display whenever the file is modified.
|
||||
|
||||
$ go get github.com/gorilla/websocket
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/filewatch`
|
||||
$ go run main.go <name of file to watch>
|
||||
# Open http://localhost:8080/ .
|
||||
# Modify the file to see it update in the browser.
|
193
Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/main.go
generated
vendored
193
Godeps/_workspace/src/github.com/gorilla/websocket/examples/filewatch/main.go
generated
vendored
|
@ -1,193 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write the file to the client.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the client.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to client with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Poll file for changes with this period.
|
||||
filePeriod = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":8080", "http service address")
|
||||
homeTempl = template.Must(template.New("").Parse(homeHTML))
|
||||
filename string
|
||||
upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
)
|
||||
|
||||
func readFileIfModified(lastMod time.Time) ([]byte, time.Time, error) {
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return nil, lastMod, err
|
||||
}
|
||||
if !fi.ModTime().After(lastMod) {
|
||||
return nil, lastMod, nil
|
||||
}
|
||||
p, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fi.ModTime(), err
|
||||
}
|
||||
return p, fi.ModTime(), nil
|
||||
}
|
||||
|
||||
func reader(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
ws.SetReadLimit(512)
|
||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, _, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writer(ws *websocket.Conn, lastMod time.Time) {
|
||||
lastError := ""
|
||||
pingTicker := time.NewTicker(pingPeriod)
|
||||
fileTicker := time.NewTicker(filePeriod)
|
||||
defer func() {
|
||||
pingTicker.Stop()
|
||||
fileTicker.Stop()
|
||||
ws.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-fileTicker.C:
|
||||
var p []byte
|
||||
var err error
|
||||
|
||||
p, lastMod, err = readFileIfModified(lastMod)
|
||||
|
||||
if err != nil {
|
||||
if s := err.Error(); s != lastError {
|
||||
lastError = s
|
||||
p = []byte(lastError)
|
||||
}
|
||||
} else {
|
||||
lastError = ""
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-pingTicker.C:
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
if _, ok := err.(websocket.HandshakeError); !ok {
|
||||
log.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var lastMod time.Time
|
||||
if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err != nil {
|
||||
lastMod = time.Unix(0, n)
|
||||
}
|
||||
|
||||
go writer(ws, lastMod)
|
||||
reader(ws)
|
||||
}
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found", 404)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
p, lastMod, err := readFileIfModified(time.Time{})
|
||||
if err != nil {
|
||||
p = []byte(err.Error())
|
||||
lastMod = time.Unix(0, 0)
|
||||
}
|
||||
var v = struct {
|
||||
Host string
|
||||
Data string
|
||||
LastMod string
|
||||
}{
|
||||
r.Host,
|
||||
string(p),
|
||||
strconv.FormatInt(lastMod.UnixNano(), 16),
|
||||
}
|
||||
homeTempl.Execute(w, &v)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
log.Fatal("filename not specified")
|
||||
}
|
||||
filename = flag.Args()[0]
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", serveWs)
|
||||
if err := http.ListenAndServe(*addr, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const homeHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>WebSocket Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="fileData">{{.Data}}</pre>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var data = document.getElementById("fileData");
|
||||
var conn = new WebSocket("ws://{{.Host}}/ws?lastMod={{.LastMod}}");
|
||||
conn.onclose = function(evt) {
|
||||
data.textContent = 'Connection closed';
|
||||
}
|
||||
conn.onmessage = function(evt) {
|
||||
console.log('file updated');
|
||||
data.textContent = evt.data;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
119
Godeps/_workspace/src/github.com/gorilla/websocket/json_test.go
generated
vendored
119
Godeps/_workspace/src/github.com/gorilla/websocket/json_test.go
generated
vendored
|
@ -1,119 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := fakeNetConn{&buf, &buf}
|
||||
wc := newConn(c, true, 1024, 1024)
|
||||
rc := newConn(c, false, 1024, 1024)
|
||||
|
||||
var actual, expect struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
expect.A = 1
|
||||
expect.B = "hello"
|
||||
|
||||
if err := wc.WriteJSON(&expect); err != nil {
|
||||
t.Fatal("write", err)
|
||||
}
|
||||
|
||||
if err := rc.ReadJSON(&actual); err != nil {
|
||||
t.Fatal("read", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&actual, &expect) {
|
||||
t.Fatal("equal", actual, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialJSONRead(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := fakeNetConn{&buf, &buf}
|
||||
wc := newConn(c, true, 1024, 1024)
|
||||
rc := newConn(c, false, 1024, 1024)
|
||||
|
||||
var v struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
v.A = 1
|
||||
v.B = "hello"
|
||||
|
||||
messageCount := 0
|
||||
|
||||
// Partial JSON values.
|
||||
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := len(data) - 1; i >= 0; i-- {
|
||||
if err := wc.WriteMessage(TextMessage, data[:i]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
messageCount++
|
||||
}
|
||||
|
||||
// Whitespace.
|
||||
|
||||
if err := wc.WriteMessage(TextMessage, []byte(" ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
messageCount++
|
||||
|
||||
// Close.
|
||||
|
||||
if err := wc.WriteMessage(CloseMessage, FormatCloseMessage(CloseNormalClosure, "")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < messageCount; i++ {
|
||||
err := rc.ReadJSON(&v)
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
t.Error("read", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = rc.ReadJSON(&v)
|
||||
if _, ok := err.(*CloseError); !ok {
|
||||
t.Error("final", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeprecatedJSON(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := fakeNetConn{&buf, &buf}
|
||||
wc := newConn(c, true, 1024, 1024)
|
||||
rc := newConn(c, false, 1024, 1024)
|
||||
|
||||
var actual, expect struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
expect.A = 1
|
||||
expect.B = "hello"
|
||||
|
||||
if err := WriteJSON(wc, &expect); err != nil {
|
||||
t.Fatal("write", err)
|
||||
}
|
||||
|
||||
if err := ReadJSON(rc, &actual); err != nil {
|
||||
t.Fatal("read", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&actual, &expect) {
|
||||
t.Fatal("equal", actual, expect)
|
||||
}
|
||||
}
|
33
Godeps/_workspace/src/github.com/gorilla/websocket/server_test.go
generated
vendored
33
Godeps/_workspace/src/github.com/gorilla/websocket/server_test.go
generated
vendored
|
@ -1,33 +0,0 @@
|
|||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var subprotocolTests = []struct {
|
||||
h string
|
||||
protocols []string
|
||||
}{
|
||||
{"", nil},
|
||||
{"foo", []string{"foo"}},
|
||||
{"foo,bar", []string{"foo", "bar"}},
|
||||
{"foo, bar", []string{"foo", "bar"}},
|
||||
{" foo, bar", []string{"foo", "bar"}},
|
||||
{" foo, bar ", []string{"foo", "bar"}},
|
||||
}
|
||||
|
||||
func TestSubprotocols(t *testing.T) {
|
||||
for _, st := range subprotocolTests {
|
||||
r := http.Request{Header: http.Header{"Sec-Websocket-Protocol": {st.h}}}
|
||||
protocols := Subprotocols(&r)
|
||||
if !reflect.DeepEqual(st.protocols, protocols) {
|
||||
t.Errorf("SubProtocols(%q) returned %#v, want %#v", st.h, protocols, st.protocols)
|
||||
}
|
||||
}
|
||||
}
|
34
Godeps/_workspace/src/github.com/gorilla/websocket/util_test.go
generated
vendored
34
Godeps/_workspace/src/github.com/gorilla/websocket/util_test.go
generated
vendored
|
@ -1,34 +0,0 @@
|
|||
// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var tokenListContainsValueTests = []struct {
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"WebSocket", true},
|
||||
{"WEBSOCKET", true},
|
||||
{"websocket", true},
|
||||
{"websockets", false},
|
||||
{"x websocket", false},
|
||||
{"websocket x", false},
|
||||
{"other,websocket,more", true},
|
||||
{"other, websocket, more", true},
|
||||
}
|
||||
|
||||
func TestTokenListContainsValue(t *testing.T) {
|
||||
for _, tt := range tokenListContainsValueTests {
|
||||
h := http.Header{"Upgrade": {tt.value}}
|
||||
ok := tokenListContainsValue(h, "Upgrade", "websocket")
|
||||
if ok != tt.ok {
|
||||
t.Errorf("tokenListContainsValue(h, n, %q) = %v, want %v", tt.value, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
69
Godeps/_workspace/src/github.com/yudai/utf8reader/README.md
generated
vendored
69
Godeps/_workspace/src/github.com/yudai/utf8reader/README.md
generated
vendored
|
@ -1,69 +0,0 @@
|
|||
# UTF8Reader for Go
|
||||
|
||||
UTF8Reader is a simple wrapper Reader that fills the given buffer with a "tail-safe" UTF8 byte sequence.
|
||||
|
||||
## Tail-Safe?
|
||||
|
||||
Let's say you have a buffer of 7 bytes and your Reader is going to fill your buffer with a UTF8 byte sequence.
|
||||
|
||||
```go
|
||||
buf := make([]byte, 7)
|
||||
reader := strings.NewReader("いろは")
|
||||
|
||||
reader.Read(buf)
|
||||
```
|
||||
|
||||
The byte length of UTF8 characters is not fixed and some characters like the examples above have 3 byte length. There are others which have a single byte, 2 byte and 4 byte length as well. This means your buffer will be sometimes filled with incomplete bytes as an Unicode character at the tail.
|
||||
|
||||
By `reader.Read(buf)`, your `buf` will be like below:
|
||||
|
||||
```go
|
||||
[]byte{
|
||||
// い
|
||||
byte(0xe3), // 1
|
||||
byte(0x81), // 2
|
||||
byte(0x84), // 3
|
||||
// ろ
|
||||
byte(0xe3), // 4
|
||||
byte(0x82), // 5
|
||||
byte(0x8d), // 6
|
||||
// は (incomplete)
|
||||
byte(0xe3), // 7
|
||||
}
|
||||
```
|
||||
|
||||
The last character `は` is incomplete and the buffer is now invalid as a UTF8 string.
|
||||
|
||||
UTF8Reader detects incomplete bytes like above and aborts filling up the buffer in such cases.
|
||||
|
||||
```go
|
||||
buf := make([]byte, 7)
|
||||
reader := strings.NewReader("いろは")
|
||||
utfReader := utf8reader.New(reader)
|
||||
|
||||
utfReader.Read(buf)
|
||||
```
|
||||
|
||||
Then you will get:
|
||||
|
||||
```go
|
||||
[]byte{
|
||||
// い
|
||||
byte(0xe3), // 1
|
||||
byte(0x81), // 2
|
||||
byte(0x84), // 3
|
||||
// ろ
|
||||
byte(0xe3), // 4
|
||||
byte(0x82), // 5
|
||||
byte(0x8d), // 6
|
||||
}
|
||||
```
|
||||
Of course, bytes left behind will be used to fill up the buffer on next `Read()`.
|
||||
|
||||
## Note
|
||||
|
||||
UTF8Reader just checks incomplete bytes at the tail of the buffer. Even if the original byte sequence given to UTF8Reader is broken, UTF8Reader reports no errors and just fills up the buffer.
|
||||
|
||||
## License
|
||||
|
||||
The MIT License
|
56
Godeps/_workspace/src/github.com/yudai/utf8reader/utf8reader.go
generated
vendored
56
Godeps/_workspace/src/github.com/yudai/utf8reader/utf8reader.go
generated
vendored
|
@ -1,56 +0,0 @@
|
|||
package utf8reader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var SmallBufferError = errors.New("Buffer size must be larger than utf8.UTFMax.")
|
||||
|
||||
type UTF8Reader struct {
|
||||
reader io.Reader
|
||||
buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func New(reader io.Reader) *UTF8Reader {
|
||||
return &UTF8Reader{
|
||||
reader: reader,
|
||||
buffer: bytes.NewBuffer(make([]byte, 0)),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UTF8Reader) Read(p []byte) (n int, err error) {
|
||||
size := 0
|
||||
|
||||
if cap(p) < utf8.UTFMax {
|
||||
return size, SmallBufferError
|
||||
}
|
||||
|
||||
if r.buffer.Len() > 0 {
|
||||
n, err = r.buffer.Read(p)
|
||||
size += n
|
||||
if err != nil {
|
||||
return size, err
|
||||
}
|
||||
}
|
||||
|
||||
n, err = r.reader.Read(p[size:])
|
||||
size += n
|
||||
if err != nil {
|
||||
return size, err
|
||||
}
|
||||
|
||||
leftOver := 0
|
||||
for ; leftOver < utf8.UTFMax && size-leftOver > 0; leftOver++ {
|
||||
rune, _ := utf8.DecodeLastRune(p[:size-leftOver])
|
||||
if rune != utf8.RuneError {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
r.buffer.Write(p[size-leftOver : size])
|
||||
|
||||
return size - leftOver, nil
|
||||
}
|
97
Godeps/_workspace/src/github.com/yudai/utf8reader/utf8reader_test.go
generated
vendored
97
Godeps/_workspace/src/github.com/yudai/utf8reader/utf8reader_test.go
generated
vendored
|
@ -1,97 +0,0 @@
|
|||
package utf8reader
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
str := "日本語"
|
||||
or := strings.NewReader(str)
|
||||
r := New(or)
|
||||
|
||||
buf := make([]byte, 512)
|
||||
n, err := r.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error")
|
||||
}
|
||||
if bytes.Compare(buf[:n], []byte(str)) != 0 {
|
||||
t.Errorf("Failed to read bytes")
|
||||
}
|
||||
|
||||
n, err = r.Read(buf)
|
||||
if err.Error() != "EOF" {
|
||||
t.Errorf("Unexpected error")
|
||||
}
|
||||
|
||||
// 3 byte runes
|
||||
str = "いろはにほ"
|
||||
or = strings.NewReader(str)
|
||||
r = New(or)
|
||||
buf = make([]byte, 7) // 7 % 3 = 1
|
||||
|
||||
n, err = r.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error")
|
||||
}
|
||||
if n != 6 {
|
||||
t.Errorf("Read length error")
|
||||
}
|
||||
if bytes.Compare(buf[:n], []byte(str)[:6]) != 0 {
|
||||
t.Errorf("Failed to read bytes")
|
||||
}
|
||||
|
||||
n, err = r.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error")
|
||||
}
|
||||
if n != 6 {
|
||||
t.Errorf("Read length error")
|
||||
}
|
||||
if bytes.Compare(buf[:n], []byte(str)[6:12]) != 0 {
|
||||
t.Errorf("Failed to read bytes")
|
||||
}
|
||||
|
||||
n, err = r.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error")
|
||||
}
|
||||
if n != 3 {
|
||||
t.Errorf("Read length error")
|
||||
}
|
||||
if bytes.Compare(buf[:n], []byte(str)[12:15]) != 0 {
|
||||
t.Errorf("Failed to read bytes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadWithSmallBuffer(t *testing.T) {
|
||||
str := "日本語"
|
||||
or := strings.NewReader(str)
|
||||
r := New(or)
|
||||
|
||||
buf := make([]byte, 2) // too small
|
||||
_, err := r.Read(buf)
|
||||
if err != SmallBufferError {
|
||||
t.Errorf("Expected error were not returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadWithSmallRead(t *testing.T) {
|
||||
input := []byte("いろは")
|
||||
or := bytes.NewBuffer(input[0:2]) // small read
|
||||
r := New(or)
|
||||
buf := make([]byte, 512)
|
||||
|
||||
_, err := r.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error")
|
||||
}
|
||||
|
||||
or.Write(input[2:6])
|
||||
_, err = r.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error")
|
||||
}
|
||||
}
|
226
LICENSE
226
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Iwasaki Yudai
|
||||
Copyright (c) 2015-2017 Iwasaki Yudai
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -19,227 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
=============================================================================
|
||||
|
||||
This software is built with following third party open source software.
|
||||
|
||||
|
||||
# golng/go
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
# libapps/hterm
|
||||
|
||||
Copyright (c) 2014, Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
* Neither the name of Google Inc. nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
# odegangsta/cli
|
||||
|
||||
Copyright (C) 2013 Jeremy Saenz
|
||||
All Rights Reserved.
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
# jteeuwen/go-bindata
|
||||
|
||||
This work is subject to the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
||||
license. Its contents can be found at:
|
||||
http://creativecommons.org/publicdomain/zero/1.0
|
||||
|
||||
# elazarl/go-bindata-assetfs
|
||||
|
||||
Copyright (c) 2014, Elazar Leibovich
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
# gorilla/websocket
|
||||
|
||||
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
# kr/pty
|
||||
|
||||
Copyright (c) 2011 Keith Rarick
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall
|
||||
be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
# krishnasrinivas/wetty
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Krishna Srinivas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
# braintree/manners
|
||||
|
||||
Copyright (c) 2014 Braintree, a division of PayPal, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
82
Makefile
82
Makefile
|
@ -1,11 +1,18 @@
|
|||
gotty: app/resource.go main.go app/*.go
|
||||
go build
|
||||
OUTPUT_DIR = ./builds
|
||||
GIT_COMMIT = `git rev-parse HEAD | cut -c1-7`
|
||||
VERSION = 2.0.0-alpha.3
|
||||
BUILD_OPTIONS = -ldflags "-X main.Version=$(VERSION) -X main.CommitID=$(GIT_COMMIT)"
|
||||
|
||||
resource: app/resource.go
|
||||
gotty: main.go server/*.go webtty/*.go backend/*.go Makefile
|
||||
godep go build ${BUILD_OPTIONS}
|
||||
|
||||
app/resource.go: bindata/static/hterm.js bindata/static/gotty.js bindata/static/index.html
|
||||
go-bindata -prefix bindata -pkg app -ignore=\\.gitkeep -o app/resource.go bindata/...
|
||||
gofmt -w app/resource.go
|
||||
.PHONY: asset
|
||||
asset: bindata/static/js/gotty-bundle.js bindata/static/index.html bindata/static/favicon.png bindata/static/css/index.css bindata/static/css/xterm.css bindata/static/css/xterm_customize.css
|
||||
go-bindata -prefix bindata -pkg server -ignore=\\.gitkeep -o server/asset.go bindata/...
|
||||
gofmt -w server/asset.go
|
||||
|
||||
.PHONY: all
|
||||
all: asset gotty
|
||||
|
||||
bindata:
|
||||
mkdir bindata
|
||||
|
@ -13,12 +20,61 @@ bindata:
|
|||
bindata/static: bindata
|
||||
mkdir bindata/static
|
||||
|
||||
bindata/static/hterm.js: bindata/static libapps/hterm/js/*.js
|
||||
cd libapps && \
|
||||
LIBDOT_SEARCH_PATH=`pwd` ./libdot/bin/concat.sh -i ./hterm/concat/hterm_all.concat -o ../bindata/static/hterm.js
|
||||
|
||||
bindata/static/gotty.js: bindata/static resources/gotty.js
|
||||
cp resources/gotty.js bindata/static/gotty.js
|
||||
|
||||
bindata/static/index.html: bindata/static resources/index.html
|
||||
cp resources/index.html bindata/static/index.html
|
||||
|
||||
bindata/static/favicon.png: bindata/static resources/favicon.png
|
||||
cp resources/favicon.png bindata/static/favicon.png
|
||||
|
||||
bindata/static/js: bindata/static
|
||||
mkdir -p bindata/static/js
|
||||
|
||||
|
||||
bindata/static/js/gotty-bundle.js: bindata/static/js js/dist/gotty-bundle.js
|
||||
cp js/dist/gotty-bundle.js bindata/static/js/gotty-bundle.js
|
||||
|
||||
bindata/static/css: bindata/static
|
||||
mkdir -p bindata/static/css
|
||||
|
||||
bindata/static/css/index.css: bindata/static/css resources/index.css
|
||||
cp resources/index.css bindata/static/css/index.css
|
||||
|
||||
bindata/static/css/xterm_customize.css: bindata/static/css resources/xterm_customize.css
|
||||
cp resources/xterm_customize.css bindata/static/css/xterm_customize.css
|
||||
|
||||
bindata/static/css/xterm.css: bindata/static/css js/node_modules/xterm/dist/xterm.css
|
||||
cp js/node_modules/xterm/dist/xterm.css bindata/static/css/xterm.css
|
||||
|
||||
js/node_modules/xterm/dist/xterm.css:
|
||||
cd js && \
|
||||
npm install
|
||||
|
||||
js/dist/gotty-bundle.js: js/src/* js/node_modules/webpack
|
||||
cd js && \
|
||||
`npm bin`/webpack
|
||||
|
||||
js/node_modules/webpack:
|
||||
cd js && \
|
||||
npm install
|
||||
|
||||
tools:
|
||||
go get github.com/tools/godep
|
||||
go get github.com/mitchellh/gox
|
||||
go get github.com/tcnksm/ghr
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
|
||||
test:
|
||||
if [ `go fmt $(go list ./... | grep -v /vendor/) | wc -l` -gt 0 ]; then echo "go fmt error"; exit 1; fi
|
||||
|
||||
cross_compile:
|
||||
GOARM=5 gox -os="darwin linux freebsd netbsd openbsd" -arch="386 amd64 arm" -osarch="!darwin/arm" -output "${OUTPUT_DIR}/pkg/{{.OS}}_{{.Arch}}/{{.Dir}}"
|
||||
|
||||
targz:
|
||||
mkdir -p ${OUTPUT_DIR}/dist
|
||||
cd ${OUTPUT_DIR}/pkg/; for osarch in *; do (cd $$osarch; tar zcvf ../../dist/gotty_${VERSION}_$$osarch.tar.gz ./*); done;
|
||||
|
||||
shasums:
|
||||
cd ${OUTPUT_DIR}/dist; sha256sum * > ./SHA256SUMS
|
||||
|
||||
release:
|
||||
ghr -c ${GIT_COMMIT} --delete --prerelease -u yudai -r gotty pre-release ${OUTPUT_DIR}/dist
|
||||
|
|
124
README.md
124
README.md
|
@ -1,4 +1,4 @@
|
|||
# GoTTY - Share your terminal as a web application
|
||||
#  GoTTY - Share your terminal as a web application
|
||||
|
||||
[][release]
|
||||
[][wercker]
|
||||
|
@ -8,29 +8,27 @@
|
|||
[wercker]: https://app.wercker.com/project/bykey/03b91f441bebeda34f80e09a9f14126f
|
||||
[license]: https://github.com/yudai/gotty/blob/master/LICENSE
|
||||
|
||||
|
||||
GoTTY is a simple command line tool that turns your CLI tools into web applications.
|
||||
|
||||

|
||||
|
||||
# Installation
|
||||
|
||||
Download the latest binary file from the [Releases](https://github.com/yudai/gotty/releases) page.
|
||||
Download the latest stable binary file from the [Releases](https://github.com/yudai/gotty/releases) page. Note that the release marked `Pre-release` is built for testing purpose, which can include unstable or breaking changes. Download a release marked [Latest release](https://github.com/yudai/gotty/releases/latest) for a stabale build.
|
||||
|
||||
(`darwin_amd64.tar.gz` is for Mac OS X users)
|
||||
(Files named with `darwin_amd64` are for Mac OS X users)
|
||||
|
||||
## Homebrew Installation
|
||||
|
||||
You can install gotty with [Homebrew](http://brew.sh/) as well.
|
||||
You can install GoTTY with [Homebrew](http://brew.sh/) as well.
|
||||
|
||||
```sh
|
||||
$ brew tap yudai/gotty
|
||||
$ brew install gotty
|
||||
$ brew install yudai/gotty/gotty
|
||||
```
|
||||
|
||||
## `go get` Installation
|
||||
## `go get` Installation (Development)
|
||||
|
||||
If you have a Go language environment, you can install gotty with the `go get` command.
|
||||
If you have a Go language environment, you can install GoTTY with the `go get` command. However, this command builds a binary file from the latest master branch, which can include unstable or breaking changes. GoTTY requires go1.9 or later.
|
||||
|
||||
```sh
|
||||
$ go get github.com/yudai/gotty
|
||||
|
@ -44,55 +42,81 @@ Usage: gotty [options] <command> [<arguments...>]
|
|||
|
||||
Run `gotty` with your preferred command as its arguments (e.g. `gotty top`).
|
||||
|
||||
By default, gotty starts a web server at port 8080. Open the URL on your web browser and you can see the running command as if it's running on your terminal.
|
||||
By default, GoTTY starts a web server at port 8080. Open the URL on your web browser and you can see the running command as if it were running on your terminal.
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
--addr, -a IP address to listen [$GOTTY_ADDR]
|
||||
--port, -p "8080" Port number to listen [$GOTTY_PORT]
|
||||
--permit-write, -w Permit clients to write to the TTY (BE CAREFUL) [$GOTTY_PERMIT_WRITE]
|
||||
--credential, -c Credential for Basic Authentication (ex: user:pass) [$GOTTY_CREDENTIAL]
|
||||
--random-url, -r Add a random string to the URL [$GOTTY_RANDOM_URL]
|
||||
--profile-file, -f "~/.gotty" Path to profile file [$GOTTY_PROFILE_FILE]
|
||||
--enable-tls, -t Enable TLS/SSL [$GOTTY_ENABLE_TLS]
|
||||
--tls-crt "~/.gotty.crt" TLS/SSL cert [$GOTTY_TLS_CRT]
|
||||
--tls-key "~/.gotty.key" TLS/SSL key [$GOTTY_TLS_KEY]
|
||||
--title-format "GoTTY - {{ .Command }} ({{ .Hostname }})" Title format of browser window [$GOTTY_TITLE_FORMAT]
|
||||
--auto-reconnect "-1" Seconds to automatically reconnect to the server when the connection is closed (default: disabled) [$GOTTY_AUTO_RECONNECT]
|
||||
--once Accept only one client and exit on disconnection [$GOTTY_ONCE]
|
||||
--address value, -a value IP address to listen (default: "0.0.0.0") [$GOTTY_ADDRESS]
|
||||
--port value, -p value Port number to liten (default: "8080") [$GOTTY_PORT]
|
||||
--permit-write, -w Permit clients to write to the TTY (BE CAREFUL) [$GOTTY_PERMIT_WRITE]
|
||||
--credential value, -c value Credential for Basic Authentication (ex: user:pass, default disabled) [$GOTTY_CREDENTIAL]
|
||||
--random-url, -r Add a random string to the URL [$GOTTY_RANDOM_URL]
|
||||
--random-url-length value Random URL length (default: 8) [$GOTTY_RANDOM_URL_LENGTH]
|
||||
--tls, -t Enable TLS/SSL [$GOTTY_TLS]
|
||||
--tls-crt value TLS/SSL certificate file path (default: "~/.gotty.crt") [$GOTTY_TLS_CRT]
|
||||
--tls-key value TLS/SSL key file path (default: "~/.gotty.key") [$GOTTY_TLS_KEY]
|
||||
--tls-ca-crt value TLS/SSL CA certificate file for client certifications (default: "~/.gotty.ca.crt") [$GOTTY_TLS_CA_CRT]
|
||||
--index value Custom index.html file [$GOTTY_INDEX]
|
||||
--title-format value Title format of browser window (default: "{{ .command }}@{{ .hostname }}") [$GOTTY_TITLE_FORMAT]
|
||||
--reconnect Enable reconnection [$GOTTY_RECONNECT]
|
||||
--reconnect-time value Time to reconnect (default: 10) [$GOTTY_RECONNECT_TIME]
|
||||
--max-connection value Maximum connection to gotty (default: 0) [$GOTTY_MAX_CONNECTION]
|
||||
--once Accept only one client and exit on disconnection [$GOTTY_ONCE]
|
||||
--timeout value Timeout seconds for waiting a client(0 to disable) (default: 0) [$GOTTY_TIMEOUT]
|
||||
--permit-arguments Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB) [$GOTTY_PERMIT_ARGUMENTS]
|
||||
--width value Static width of the screen, 0(default) means dynamically resize (default: 0) [$GOTTY_WIDTH]
|
||||
--height value Static height of the screen, 0(default) means dynamically resize (default: 0) [$GOTTY_HEIGHT]
|
||||
--ws-origin value A regular expression that matches origin URLs to be accepted by WebSocket. No cross origin requests are acceptable by default [$GOTTY_WS_ORIGIN]
|
||||
--term value Terminal name to use on the browser, one of xterm or hterm. (default: "xterm") [$GOTTY_TERM]
|
||||
--close-signal value Signal sent to the command process when gotty close it (default: SIGHUP) (default: 1) [$GOTTY_CLOSE_SIGNAL]
|
||||
--close-timeout value Time in seconds to force kill process after client is disconnected (default: -1) (default: -1) [$GOTTY_CLOSE_TIMEOUT]
|
||||
--config value Config file path (default: "~/.gotty") [$GOTTY_CONFIG]
|
||||
--version, -v print the version
|
||||
```
|
||||
|
||||
### Profile File
|
||||
### Config File
|
||||
|
||||
You can customize your terminal (hterm) by providing a profile file to the `gotty` command, which is a JSON file that has a map of preference keys and values. Gotty loads a profile file at `~/.gotty` by default when it exists.
|
||||
You can customize default options and your terminal (hterm) by providing a config file to the `gotty` command. GoTTY loads a profile file at `~/.gotty` by default when it exists.
|
||||
|
||||
The following example makes the font size smaller and the background color a little bit blue.
|
||||
```
|
||||
// Listen at port 9000 by default
|
||||
port = "9000"
|
||||
|
||||
```json
|
||||
{
|
||||
"font-size": 5,
|
||||
"background-color": "rgb(16, 16, 32)"
|
||||
// Enable TSL/SSL by default
|
||||
enable_tls = true
|
||||
|
||||
// hterm preferences
|
||||
// Smaller font and a little bit bluer background color
|
||||
preferences {
|
||||
font_size = 5
|
||||
background_color = "rgb(16, 16, 32)"
|
||||
}
|
||||
```
|
||||
|
||||
Available preferences are listed in [the hterm source code](https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_preference_manager.js)
|
||||
See the [`.gotty`](https://github.com/yudai/gotty/blob/master/.gotty) file in this repository for the list of configuration options.
|
||||
|
||||
### Security Options
|
||||
|
||||
By default, gotty doesn't allow clients to send any keystrokes or commands except terminal window resizing. When you want to permit clients to write input to the PTY, add the `-w` option. However, accepting input from remote clients is dangerous for most commands. When you need interaction with the PTY for some reasons, consider starting gotty with tmux or GNU Screen and run your main command on it (see "Sharing with Multiple Clients" section for detail).
|
||||
By default, GoTTY doesn't allow clients to send any keystrokes or commands except terminal window resizing. When you want to permit clients to write input to the TTY, add the `-w` option. However, accepting input from remote clients is dangerous for most commands. When you need interaction with the TTY for some reasons, consider starting GoTTY with tmux or GNU Screen and run your command on it (see "Sharing with Multiple Clients" section for detail).
|
||||
|
||||
To restrict client access, you can use the `-c` option to enable the basic authentication. With option, clients need to input the specified username and passwords to connect to the gotty server. The `-r` option is a little bit casualer way to restrict access. With this option, gotty generates a random URL so that only people who know the URL can get access to the server.
|
||||
To restrict client access, you can use the `-c` option to enable the basic authentication. With this option, clients need to input the specified username and password to connect to the GoTTY server. Note that the credentical will be transmitted between the server and clients in plain text. For more strict authentication, consider the SSL/TLS client certificate authentication described below.
|
||||
|
||||
All traffic between servers and clients are NOT encrypted by default. When you send secret information through gotty, we strongly recommend you use the `-t` option which enables TLS/SSL on the session. By default, gotty loads the crt and key files placed at `~/.gotty.crt` and `~/.gotty.key`. You can overwrite these file paths with the `--tls-cert` and `--tls-key` options. When you need to generate a self-signed certification file, you can use the `openssl` command.
|
||||
The `-r` option is a little bit casualer way to restrict access. With this option, GoTTY generates a random URL so that only people who know the URL can get access to the server.
|
||||
|
||||
All traffic between the server and clients are NOT encrypted by default. When you send secret information through GoTTY, we strongly recommend you use the `-t` option which enables TLS/SSL on the session. By default, GoTTY loads the crt and key files placed at `~/.gotty.crt` and `~/.gotty.key`. You can overwrite these file paths with the `--tls-crt` and `--tls-key` options. When you need to generate a self-signed certification file, you can use the `openssl` command.
|
||||
|
||||
```sh
|
||||
openssl req -x509 -nodes -days 9999 -newkey rsa:2048 -keyout ~/.gotty.key -out ~/.gotty.crt
|
||||
```
|
||||
|
||||
(NOTE: For Safari uses, see [how to enable self-signed certificates for WebSockets](http://blog.marcon.me/post/24874118286/secure-websockets-safari) when use self-signed certificates)
|
||||
|
||||
For additional security, you can use the SSL/TLS client certificate authentication by providing a CA certificate file to the `--tls-ca-crt` option (this option requires the `-t` or `--tls` to be set). This option requires all clients to send valid client certificates that are signed by the specified certification authority.
|
||||
|
||||
## Sharing with Multiple Clients
|
||||
|
||||
Gotty starts a new process when a new client connects to the server. This means users cannot share a single terminal with others by default. However, you can use terminal multiplexers for sharing a single process with multiple clients.
|
||||
GoTTY starts a new process with the given command when a new client connects to the server. This means users cannot share a single terminal with others by default. However, you can use terminal multiplexers for sharing a single process with multiple clients.
|
||||
|
||||
For example, you can start a new tmux session named `gotty` with `top` command by the command below.
|
||||
|
||||
|
@ -113,7 +137,7 @@ By using terminal multiplexers, you can have the control of your terminal and al
|
|||
To share your current session with others by a shortcut key, you can add a line like below to your `.tmux.conf`.
|
||||
|
||||
```
|
||||
# Start gotty in a new window with C-t
|
||||
# Start GoTTY in a new window with C-t
|
||||
bind-key C-t new-window "gotty tmux attach -t `tmux display -p '#S'`"
|
||||
```
|
||||
|
||||
|
@ -127,26 +151,40 @@ $ gotty -w docker run -it --rm busybox
|
|||
|
||||
## Development
|
||||
|
||||
You can build a binary yourself using following commands. Windows is not supported now.
|
||||
You can build a binary using the following commands. Windows is not supported now. go1.9 is required.
|
||||
|
||||
```sh
|
||||
# Install tools
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
go get github.com/tools/godep
|
||||
|
||||
# Checkout hterm
|
||||
git submodule sync && git submodule update --init --recursive
|
||||
|
||||
# Restore libraries in Godeps
|
||||
godep restore
|
||||
|
||||
# Build
|
||||
make
|
||||
```
|
||||
|
||||
To build the frontend part (JS files and other static files), you need `npm`.
|
||||
|
||||
## Architecture
|
||||
|
||||
Gotty uses [hterm](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm) to run a JavaScript based terminal on web browsers. Gotty itself provides a websocket server that simply relays output from the PTY to clients and receives input from clients and forwards to the PTY. This hterm + websocket idea is highly inspired by [Wetty](https://github.com/krishnasrinivas/wetty).
|
||||
GoTTY uses [xterm.js](https://xtermjs.org/) and [hterm](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm) to run a JavaScript based terminal on web browsers. GoTTY itself provides a websocket server that simply relays output from the TTY to clients and receives input from clients and forwards it to the TTY. This hterm + websocket idea is inspired by [Wetty](https://github.com/krishnasrinivas/wetty).
|
||||
|
||||
## Alternatives
|
||||
|
||||
### Command line client
|
||||
|
||||
* [gotty-client](https://github.com/moul/gotty-client): If you want to connect to GoTTY server from your terminal
|
||||
|
||||
### Terminal/SSH on Web Browsers
|
||||
|
||||
* [Secure Shell (Chrome App)](https://chrome.google.com/webstore/detail/secure-shell/pnhechapfaindjhompbnflcldabbghjo): If you are a chrome user and need a "real" SSH client on your web browser, perhaps the Secure Shell app is what you want
|
||||
* [Wetty](https://github.com/krishnasrinivas/wetty): Node based web terminal (SSH/login)
|
||||
* [ttyd](https://tsl0922.github.io/ttyd): C port of GoTTY with CJK and IME support
|
||||
|
||||
### Terminal Sharing
|
||||
|
||||
* [tmate](http://tmate.io/): Forked-Tmux based Terminal-Terminal sharing
|
||||
* [termshare](https://termsha.re): Terminal-Terminal sharing through a HTTP server
|
||||
* [tmux](https://tmux.github.io/): Tmux itself also supports TTY sharing through SSH)
|
||||
|
||||
# License
|
||||
|
||||
|
|
303
app/app.go
303
app/app.go
|
@ -1,303 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/braintree/manners"
|
||||
"github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/kr/pty"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
options Options
|
||||
|
||||
upgrader *websocket.Upgrader
|
||||
server *manners.GracefulServer
|
||||
|
||||
preferences map[string]interface{}
|
||||
titleTemplate *template.Template
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Address string
|
||||
Port string
|
||||
PermitWrite bool
|
||||
Credential string
|
||||
RandomUrl bool
|
||||
ProfileFile string
|
||||
EnableTLS bool
|
||||
TLSCrt string
|
||||
TLSKey string
|
||||
TitleFormat string
|
||||
AutoReconnect int
|
||||
Once bool
|
||||
Command []string
|
||||
}
|
||||
|
||||
const DefaultProfileFilePath = "~/.gotty"
|
||||
const DefaultTLSKeyPath = "~/.gotty.key"
|
||||
const DefaultTLSCrtPath = "~/.gotty.crt"
|
||||
|
||||
func New(options Options) (*App, error) {
|
||||
titleTemplate, err := template.New("title").Parse(options.TitleFormat)
|
||||
if err != nil {
|
||||
return nil, errors.New("Title format string syntax error")
|
||||
}
|
||||
|
||||
prefMap, err := loadProfileFile(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &App{
|
||||
options: options,
|
||||
|
||||
upgrader: &websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
Subprotocols: []string{"gotty"},
|
||||
},
|
||||
|
||||
preferences: prefMap,
|
||||
titleTemplate: titleTemplate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadProfileFile(options Options) (map[string]interface{}, error) {
|
||||
prefString := []byte{}
|
||||
prefPath := options.ProfileFile
|
||||
if options.ProfileFile == DefaultProfileFilePath {
|
||||
prefPath = os.Getenv("HOME") + "/.gotty"
|
||||
}
|
||||
if _, err := os.Stat(prefPath); os.IsNotExist(err) {
|
||||
if options.ProfileFile != DefaultProfileFilePath {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.Printf("Loading profile path: %s", prefPath)
|
||||
prefString, _ = ioutil.ReadFile(prefPath)
|
||||
}
|
||||
if len(prefString) == 0 {
|
||||
prefString = []byte(("{}"))
|
||||
}
|
||||
var prefMap map[string]interface{}
|
||||
err := json.Unmarshal(prefString, &prefMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prefMap, nil
|
||||
}
|
||||
|
||||
func (app *App) Run() error {
|
||||
if app.options.PermitWrite {
|
||||
log.Printf("Permitting clients to write input to the PTY.")
|
||||
}
|
||||
|
||||
path := ""
|
||||
if app.options.RandomUrl {
|
||||
path += "/" + generateRandomString(8)
|
||||
}
|
||||
|
||||
endpoint := net.JoinHostPort(app.options.Address, app.options.Port)
|
||||
|
||||
wsHandler := http.HandlerFunc(app.handleWS)
|
||||
staticHandler := http.FileServer(
|
||||
&assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "static"},
|
||||
)
|
||||
|
||||
if app.options.Once {
|
||||
log.Printf("Once option is provided, accepting only one client")
|
||||
}
|
||||
|
||||
var siteMux = http.NewServeMux()
|
||||
siteMux.Handle(path+"/", http.StripPrefix(path+"/", staticHandler))
|
||||
siteMux.Handle(path+"/ws", wsHandler)
|
||||
|
||||
siteHandler := http.Handler(siteMux)
|
||||
|
||||
if app.options.Credential != "" {
|
||||
log.Printf("Using Basic Authentication")
|
||||
siteHandler = wrapBasicAuth(siteHandler, app.options.Credential)
|
||||
}
|
||||
|
||||
siteHandler = wrapLogger(siteHandler)
|
||||
|
||||
scheme := "http"
|
||||
if app.options.EnableTLS {
|
||||
scheme = "https"
|
||||
}
|
||||
log.Printf(
|
||||
"Server is starting with command: %s",
|
||||
strings.Join(app.options.Command, " "),
|
||||
)
|
||||
if app.options.Address != "" {
|
||||
log.Printf(
|
||||
"URL: %s",
|
||||
(&url.URL{Scheme: scheme, Host: endpoint, Path: path + "/"}).String(),
|
||||
)
|
||||
} else {
|
||||
for _, address := range listAddresses() {
|
||||
log.Printf(
|
||||
"URL: %s",
|
||||
(&url.URL{
|
||||
Scheme: scheme,
|
||||
Host: net.JoinHostPort(address, app.options.Port),
|
||||
Path: path + "/",
|
||||
}).String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
app.server = manners.NewWithServer(
|
||||
&http.Server{Addr: endpoint, Handler: siteHandler},
|
||||
)
|
||||
if app.options.EnableTLS {
|
||||
crt, key := app.loadTLSFiles()
|
||||
err = app.server.ListenAndServeTLS(crt, key)
|
||||
} else {
|
||||
err = app.server.ListenAndServe()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Exiting...")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *App) loadTLSFiles() (crt string, key string) {
|
||||
crt = app.options.TLSCrt
|
||||
if app.options.TLSCrt == DefaultTLSCrtPath {
|
||||
crt = os.Getenv("HOME") + "/.gotty.crt"
|
||||
}
|
||||
|
||||
key = app.options.TLSKey
|
||||
if app.options.TLSKey == DefaultTLSKeyPath {
|
||||
key = os.Getenv("HOME") + "/.gotty.key"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("New client connected: %s", r.RemoteAddr)
|
||||
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := app.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Print("Failed to upgrade connection")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(app.options.Command[0], app.options.Command[1:]...)
|
||||
ptyIo, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
log.Print("Failed to execute command")
|
||||
return
|
||||
}
|
||||
log.Printf("Command is running for client %s with PID %d", r.RemoteAddr, cmd.Process.Pid)
|
||||
|
||||
context := &clientContext{
|
||||
app: app,
|
||||
request: r,
|
||||
connection: conn,
|
||||
command: cmd,
|
||||
pty: ptyIo,
|
||||
}
|
||||
|
||||
context.goHandleClient()
|
||||
}
|
||||
|
||||
func (app *App) Exit() (firstCall bool) {
|
||||
if app.server != nil {
|
||||
log.Printf("Received Exit command, waiting for all clients to close sessions...")
|
||||
return app.server.Close()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func wrapLogger(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s", r.Method, r.URL.Path)
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func wrapBasicAuth(handler http.Handler, credential string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
|
||||
|
||||
if len(token) != 2 || strings.ToLower(token[0]) != "basic" {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="GoTTY"`)
|
||||
http.Error(w, "Bad Request", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := base64.StdEncoding.DecodeString(token[1])
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if credential != string(payload) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="GoTTY"`)
|
||||
http.Error(w, "authorization failed", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Basic Authentication Succeeded: %s", r.RemoteAddr)
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func generateRandomString(length int) string {
|
||||
const base = 36
|
||||
size := big.NewInt(base)
|
||||
n := make([]byte, length)
|
||||
for i, _ := range n {
|
||||
c, _ := rand.Int(rand.Reader, size)
|
||||
n[i] = strconv.FormatInt(c.Int64(), base)[0]
|
||||
}
|
||||
return string(n)
|
||||
}
|
||||
|
||||
func listAddresses() (addresses []string) {
|
||||
ifaces, _ := net.Interfaces()
|
||||
|
||||
addresses = make([]string, 0, len(ifaces))
|
||||
|
||||
for _, iface := range ifaces {
|
||||
ifAddrs, _ := iface.Addrs()
|
||||
for _, ifAddr := range ifAddrs {
|
||||
switch v := ifAddr.(type) {
|
||||
case *net.IPNet:
|
||||
addresses = append(addresses, v.IP.String())
|
||||
case *net.IPAddr:
|
||||
addresses = append(addresses, v.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/yudai/utf8reader"
|
||||
)
|
||||
|
||||
type clientContext struct {
|
||||
app *App
|
||||
request *http.Request
|
||||
connection *websocket.Conn
|
||||
command *exec.Cmd
|
||||
pty *os.File
|
||||
}
|
||||
|
||||
const (
|
||||
Input = '0'
|
||||
ResizeTerminal = '1'
|
||||
)
|
||||
|
||||
const (
|
||||
Output = '0'
|
||||
SetWindowTitle = '1'
|
||||
SetPreferences = '2'
|
||||
SetAutoReconnect = '3'
|
||||
)
|
||||
|
||||
type argResizeTerminal struct {
|
||||
Columns float64
|
||||
Rows float64
|
||||
}
|
||||
|
||||
type ContextVars struct {
|
||||
Command string
|
||||
Pid int
|
||||
Hostname string
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
func (context *clientContext) goHandleClient() {
|
||||
exit := make(chan bool, 2)
|
||||
|
||||
go func() {
|
||||
defer func() { exit <- true }()
|
||||
|
||||
context.processSend()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer func() { exit <- true }()
|
||||
|
||||
context.processReceive()
|
||||
}()
|
||||
|
||||
context.app.server.StartRoutine()
|
||||
|
||||
if context.app.options.Once {
|
||||
log.Printf("Last client accepted, closing the listener.")
|
||||
context.app.server.Close()
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer context.app.server.FinishRoutine()
|
||||
|
||||
<-exit
|
||||
context.pty.Close()
|
||||
context.command.Wait()
|
||||
context.connection.Close()
|
||||
log.Printf("Connection closed: %s", context.request.RemoteAddr)
|
||||
}()
|
||||
}
|
||||
|
||||
func (context *clientContext) processSend() {
|
||||
if err := context.sendInitialize(); err != nil {
|
||||
log.Printf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
utf8f := utf8reader.New(context.pty)
|
||||
|
||||
for {
|
||||
size, err := utf8f.Read(buf)
|
||||
if err != nil {
|
||||
log.Printf("Command exited for: %s", context.request.RemoteAddr)
|
||||
return
|
||||
}
|
||||
|
||||
writer, err := context.connection.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
writer.Write([]byte{Output})
|
||||
writer.Write(buf[:size])
|
||||
writer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (context *clientContext) sendInitialize() error {
|
||||
hostname, _ := os.Hostname()
|
||||
titleVars := ContextVars{
|
||||
Command: strings.Join(context.app.options.Command, " "),
|
||||
Pid: context.command.Process.Pid,
|
||||
Hostname: hostname,
|
||||
RemoteAddr: context.request.RemoteAddr,
|
||||
}
|
||||
|
||||
writer, err := context.connection.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Write([]byte{SetWindowTitle})
|
||||
if err = context.app.titleTemplate.Execute(writer, titleVars); err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
prefs, _ := json.Marshal(context.app.preferences)
|
||||
writer, err = context.connection.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Write([]byte{SetPreferences})
|
||||
writer.Write(prefs)
|
||||
writer.Close()
|
||||
|
||||
if context.app.options.AutoReconnect >= 0 {
|
||||
autoReconnect, _ := json.Marshal(context.app.options.AutoReconnect)
|
||||
writer, err = context.connection.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Write([]byte{SetAutoReconnect})
|
||||
writer.Write(autoReconnect)
|
||||
writer.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (context *clientContext) processReceive() {
|
||||
for {
|
||||
_, data, err := context.connection.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch data[0] {
|
||||
case Input:
|
||||
if !context.app.options.PermitWrite {
|
||||
break
|
||||
}
|
||||
|
||||
_, err := context.pty.Write(data[1:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case ResizeTerminal:
|
||||
var args argResizeTerminal
|
||||
err = json.Unmarshal(data[1:], &args)
|
||||
if err != nil {
|
||||
log.Print("Malformed remote command")
|
||||
return
|
||||
}
|
||||
|
||||
window := struct {
|
||||
row uint16
|
||||
col uint16
|
||||
x uint16
|
||||
y uint16
|
||||
}{
|
||||
uint16(args.Rows),
|
||||
uint16(args.Columns),
|
||||
0,
|
||||
0,
|
||||
}
|
||||
syscall.Syscall(
|
||||
syscall.SYS_IOCTL,
|
||||
context.pty.Fd(),
|
||||
syscall.TIOCSWINSZ,
|
||||
uintptr(unsafe.Pointer(&window)),
|
||||
)
|
||||
|
||||
default:
|
||||
log.Print("Unknown message type")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
308
app/resource.go
308
app/resource.go
File diff suppressed because one or more lines are too long
1
backend/doc.go
Normal file
1
backend/doc.go
Normal file
|
@ -0,0 +1 @@
|
|||
package backend
|
3
backend/localcommand/doc.go
Normal file
3
backend/localcommand/doc.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Package localcommand provides an implementation of webtty.Slave
|
||||
// that launches a local command with a PTY.
|
||||
package localcommand
|
48
backend/localcommand/factory.go
Normal file
48
backend/localcommand/factory.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package localcommand
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/yudai/gotty/server"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
CloseSignal int `hcl:"close_signal" flagName:"close-signal" flagSName:"" flagDescribe:"Signal sent to the command process when gotty close it (default: SIGHUP)" default:"1"`
|
||||
CloseTimeout int `hcl:"close_timeout" flagName:"close-timeout" flagSName:"" flagDescribe:"Time in seconds to force kill process after client is disconnected (default: -1)" default:"-1"`
|
||||
}
|
||||
|
||||
type Factory struct {
|
||||
command string
|
||||
argv []string
|
||||
options *Options
|
||||
opts []Option
|
||||
}
|
||||
|
||||
func NewFactory(command string, argv []string, options *Options) (*Factory, error) {
|
||||
opts := []Option{WithCloseSignal(syscall.Signal(options.CloseSignal))}
|
||||
if options.CloseTimeout >= 0 {
|
||||
opts = append(opts, WithCloseTimeout(time.Duration(options.CloseTimeout)*time.Second))
|
||||
}
|
||||
|
||||
return &Factory{
|
||||
command: command,
|
||||
argv: argv,
|
||||
options: options,
|
||||
opts: opts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (factory *Factory) Name() string {
|
||||
return "local command"
|
||||
}
|
||||
|
||||
func (factory *Factory) New(params map[string][]string) (server.Slave, error) {
|
||||
argv := make([]string, len(factory.argv))
|
||||
copy(argv, factory.argv)
|
||||
if params["arg"] != nil && len(params["arg"]) > 0 {
|
||||
argv = append(argv, params["arg"]...)
|
||||
}
|
||||
|
||||
return New(factory.command, argv, factory.opts...)
|
||||
}
|
132
backend/localcommand/local_command.go
Normal file
132
backend/localcommand/local_command.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package localcommand
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/kr/pty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCloseSignal = syscall.SIGINT
|
||||
DefaultCloseTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type LocalCommand struct {
|
||||
command string
|
||||
argv []string
|
||||
|
||||
closeSignal syscall.Signal
|
||||
closeTimeout time.Duration
|
||||
|
||||
cmd *exec.Cmd
|
||||
pty *os.File
|
||||
ptyClosed chan struct{}
|
||||
}
|
||||
|
||||
func New(command string, argv []string, options ...Option) (*LocalCommand, error) {
|
||||
cmd := exec.Command(command, argv...)
|
||||
|
||||
pty, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
// todo close cmd?
|
||||
return nil, errors.Wrapf(err, "failed to start command `%s`", command)
|
||||
}
|
||||
ptyClosed := make(chan struct{})
|
||||
|
||||
lcmd := &LocalCommand{
|
||||
command: command,
|
||||
argv: argv,
|
||||
|
||||
closeSignal: DefaultCloseSignal,
|
||||
closeTimeout: DefaultCloseTimeout,
|
||||
|
||||
cmd: cmd,
|
||||
pty: pty,
|
||||
ptyClosed: ptyClosed,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(lcmd)
|
||||
}
|
||||
|
||||
// When the process is closed by the user,
|
||||
// close pty so that Read() on the pty breaks with an EOF.
|
||||
go func() {
|
||||
defer func() {
|
||||
lcmd.pty.Close()
|
||||
close(lcmd.ptyClosed)
|
||||
}()
|
||||
|
||||
lcmd.cmd.Wait()
|
||||
}()
|
||||
|
||||
return lcmd, nil
|
||||
}
|
||||
|
||||
func (lcmd *LocalCommand) Read(p []byte) (n int, err error) {
|
||||
return lcmd.pty.Read(p)
|
||||
}
|
||||
|
||||
func (lcmd *LocalCommand) Write(p []byte) (n int, err error) {
|
||||
return lcmd.pty.Write(p)
|
||||
}
|
||||
|
||||
func (lcmd *LocalCommand) Close() error {
|
||||
if lcmd.cmd != nil && lcmd.cmd.Process != nil {
|
||||
lcmd.cmd.Process.Signal(lcmd.closeSignal)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-lcmd.ptyClosed:
|
||||
return nil
|
||||
case <-lcmd.closeTimeoutC():
|
||||
lcmd.cmd.Process.Signal(syscall.SIGKILL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (lcmd *LocalCommand) WindowTitleVariables() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"command": lcmd.command,
|
||||
"argv": lcmd.argv,
|
||||
"pid": lcmd.cmd.Process.Pid,
|
||||
}
|
||||
}
|
||||
|
||||
func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error {
|
||||
window := struct {
|
||||
row uint16
|
||||
col uint16
|
||||
x uint16
|
||||
y uint16
|
||||
}{
|
||||
uint16(height),
|
||||
uint16(width),
|
||||
0,
|
||||
0,
|
||||
}
|
||||
_, _, errno := syscall.Syscall(
|
||||
syscall.SYS_IOCTL,
|
||||
lcmd.pty.Fd(),
|
||||
syscall.TIOCSWINSZ,
|
||||
uintptr(unsafe.Pointer(&window)),
|
||||
)
|
||||
if errno != 0 {
|
||||
return errno
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (lcmd *LocalCommand) closeTimeoutC() <-chan time.Time {
|
||||
if lcmd.closeTimeout >= 0 {
|
||||
return time.After(lcmd.closeTimeout)
|
||||
}
|
||||
|
||||
return make(chan time.Time)
|
||||
}
|
20
backend/localcommand/options.go
Normal file
20
backend/localcommand/options.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package localcommand
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Option func(*LocalCommand)
|
||||
|
||||
func WithCloseSignal(signal syscall.Signal) Option {
|
||||
return func(lcmd *LocalCommand) {
|
||||
lcmd.closeSignal = signal
|
||||
}
|
||||
}
|
||||
|
||||
func WithCloseTimeout(timeout time.Duration) Option {
|
||||
return func(lcmd *LocalCommand) {
|
||||
lcmd.closeTimeout = timeout
|
||||
}
|
||||
}
|
BIN
favicon.psd
Executable file
BIN
favicon.psd
Executable file
Binary file not shown.
88
js/dist/gotty-bundle.js
vendored
Normal file
88
js/dist/gotty-bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
24
js/dist/hterm.d.ts
vendored
Normal file
24
js/dist/hterm.d.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as bare from "libapps";
|
||||
export declare class Hterm {
|
||||
elem: HTMLElement;
|
||||
term: bare.hterm.Terminal;
|
||||
io: bare.hterm.IO;
|
||||
columns: number;
|
||||
rows: number;
|
||||
message: string;
|
||||
constructor(elem: HTMLElement);
|
||||
info(): {
|
||||
columns: number;
|
||||
rows: number;
|
||||
};
|
||||
output(data: string): void;
|
||||
showMessage(message: string, timeout: number): void;
|
||||
removeMessage(): void;
|
||||
setWindowTitle(title: string): void;
|
||||
setPreferences(value: object): void;
|
||||
onInput(callback: (input: string) => void): void;
|
||||
onResize(callback: (colmuns: number, rows: number) => void): void;
|
||||
deactivate(): void;
|
||||
reset(): void;
|
||||
close(): void;
|
||||
}
|
0
js/dist/main.d.ts
vendored
Normal file
0
js/dist/main.d.ts
vendored
Normal file
17
js/dist/websocket.d.ts
vendored
Normal file
17
js/dist/websocket.d.ts
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
export declare class ConnectionFactory {
|
||||
url: string;
|
||||
protocols: string[];
|
||||
constructor(url: string, protocols: string[]);
|
||||
create(): Connection;
|
||||
}
|
||||
export declare class Connection {
|
||||
bare: WebSocket;
|
||||
constructor(url: string, protocols: string[]);
|
||||
open(): void;
|
||||
close(): void;
|
||||
send(data: string): void;
|
||||
isOpen(): boolean;
|
||||
onOpen(callback: () => void): void;
|
||||
onReceive(callback: (data: string) => void): void;
|
||||
onClose(callback: () => void): void;
|
||||
}
|
48
js/dist/webtty.d.ts
vendored
Normal file
48
js/dist/webtty.d.ts
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
export declare const protocols: string[];
|
||||
export declare const msgInputUnknown = "0";
|
||||
export declare const msgInput = "1";
|
||||
export declare const msgPing = "2";
|
||||
export declare const msgResizeTerminal = "3";
|
||||
export declare const msgUnknownOutput = "0";
|
||||
export declare const msgOutput = "1";
|
||||
export declare const msgPong = "2";
|
||||
export declare const msgSetWindowTitle = "3";
|
||||
export declare const msgSetPreferences = "4";
|
||||
export declare const msgSetReconnect = "5";
|
||||
export interface Terminal {
|
||||
info(): {
|
||||
columns: number;
|
||||
rows: number;
|
||||
};
|
||||
output(data: string): void;
|
||||
showMessage(message: string, timeout: number): void;
|
||||
removeMessage(): void;
|
||||
setWindowTitle(title: string): void;
|
||||
setPreferences(value: object): void;
|
||||
onInput(callback: (input: string) => void): void;
|
||||
onResize(callback: (colmuns: number, rows: number) => void): void;
|
||||
reset(): void;
|
||||
deactivate(): void;
|
||||
close(): void;
|
||||
}
|
||||
export interface Connection {
|
||||
open(): void;
|
||||
close(): void;
|
||||
send(data: string): void;
|
||||
isOpen(): boolean;
|
||||
onOpen(callback: () => void): void;
|
||||
onReceive(callback: (data: string) => void): void;
|
||||
onClose(callback: () => void): void;
|
||||
}
|
||||
export interface ConnectionFactory {
|
||||
create(): Connection;
|
||||
}
|
||||
export declare class WebTTY {
|
||||
term: Terminal;
|
||||
connectionFactory: ConnectionFactory;
|
||||
args: string;
|
||||
authToken: string;
|
||||
reconnect: number;
|
||||
constructor(term: Terminal, connectionFactory: ConnectionFactory, args: string, authToken: string);
|
||||
open(): () => void;
|
||||
}
|
26
js/dist/xterm.d.ts
vendored
Normal file
26
js/dist/xterm.d.ts
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
import * as bare from "xterm";
|
||||
import { lib } from "libapps";
|
||||
export declare class Xterm {
|
||||
elem: HTMLElement;
|
||||
term: bare;
|
||||
resizeListener: () => void;
|
||||
decoder: lib.UTF8Decoder;
|
||||
message: HTMLElement;
|
||||
messageTimeout: number;
|
||||
messageTimer: number;
|
||||
constructor(elem: HTMLElement);
|
||||
info(): {
|
||||
columns: number;
|
||||
rows: number;
|
||||
};
|
||||
output(data: string): void;
|
||||
showMessage(message: string, timeout: number): void;
|
||||
removeMessage(): void;
|
||||
setWindowTitle(title: string): void;
|
||||
setPreferences(value: object): void;
|
||||
onInput(callback: (input: string) => void): void;
|
||||
onResize(callback: (colmuns: number, rows: number) => void): void;
|
||||
deactivate(): void;
|
||||
reset(): void;
|
||||
close(): void;
|
||||
}
|
2404
js/package-lock.json
generated
Normal file
2404
js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
13
js/package.json
Normal file
13
js/package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"license-loader": "^0.5.0",
|
||||
"ts-loader": "^2.0.3",
|
||||
"typescript": "^2.3.2",
|
||||
"uglifyjs-webpack-plugin": "^1.0.0-beta.2",
|
||||
"webpack": "^2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"libapps": "github:yudai/libapps#release-hterm-1.70",
|
||||
"xterm": "^2.7.0"
|
||||
}
|
||||
}
|
92
js/src/hterm.ts
Normal file
92
js/src/hterm.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import * as bare from "libapps";
|
||||
|
||||
export class Hterm {
|
||||
elem: HTMLElement;
|
||||
|
||||
term: bare.hterm.Terminal;
|
||||
io: bare.hterm.IO;
|
||||
|
||||
columns: number;
|
||||
rows: number;
|
||||
|
||||
// to "show" the current message when removeMessage() is called
|
||||
message: string;
|
||||
|
||||
constructor(elem: HTMLElement) {
|
||||
this.elem = elem;
|
||||
bare.hterm.defaultStorage = new bare.lib.Storage.Memory();
|
||||
this.term = new bare.hterm.Terminal();
|
||||
this.term.getPrefs().set("send-encoding", "raw");
|
||||
this.term.decorate(this.elem);
|
||||
|
||||
this.io = this.term.io.push();
|
||||
this.term.installKeyboard();
|
||||
};
|
||||
|
||||
info(): { columns: number, rows: number } {
|
||||
return { columns: this.columns, rows: this.rows };
|
||||
};
|
||||
|
||||
output(data: string) {
|
||||
if (this.term.io != null) {
|
||||
this.term.io.writeUTF8(data);
|
||||
}
|
||||
};
|
||||
|
||||
showMessage(message: string, timeout: number) {
|
||||
this.message = message;
|
||||
if (timeout > 0) {
|
||||
this.term.io.showOverlay(message, timeout);
|
||||
} else {
|
||||
this.term.io.showOverlay(message, null);
|
||||
}
|
||||
};
|
||||
|
||||
removeMessage(): void {
|
||||
// there is no hideOverlay(), so show the same message with 0 sec
|
||||
this.term.io.showOverlay(this.message, 0);
|
||||
}
|
||||
|
||||
setWindowTitle(title: string) {
|
||||
this.term.setWindowTitle(title);
|
||||
};
|
||||
|
||||
setPreferences(value: object) {
|
||||
Object.keys(value).forEach((key) => {
|
||||
this.term.getPrefs().set(key, value[key]);
|
||||
});
|
||||
};
|
||||
|
||||
onInput(callback: (input: string) => void) {
|
||||
this.io.onVTKeystroke = (data) => {
|
||||
callback(data);
|
||||
};
|
||||
this.io.sendString = (data) => {
|
||||
callback(data);
|
||||
};
|
||||
};
|
||||
|
||||
onResize(callback: (colmuns: number, rows: number) => void) {
|
||||
this.io.onTerminalResize = (columns: number, rows: number) => {
|
||||
this.columns = columns;
|
||||
this.rows = rows;
|
||||
callback(columns, rows);
|
||||
};
|
||||
};
|
||||
|
||||
deactivate(): void {
|
||||
this.io.onVTKeystroke = function(){};
|
||||
this.io.sendString = function(){};
|
||||
this.io.onTerminalResize = function(){};
|
||||
this.term.uninstallKeyboard();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.removeMessage();
|
||||
this.term.installKeyboard();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.term.uninstallKeyboard();
|
||||
}
|
||||
}
|
30
js/src/main.ts
Normal file
30
js/src/main.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Hterm } from "./hterm";
|
||||
import { Xterm } from "./xterm";
|
||||
import { Terminal, WebTTY, protocols } from "./webtty";
|
||||
import { ConnectionFactory } from "./websocket";
|
||||
|
||||
// @TODO remove these
|
||||
declare var gotty_auth_token: string;
|
||||
declare var gotty_term: string;
|
||||
|
||||
const elem = document.getElementById("terminal")
|
||||
|
||||
if (elem !== null) {
|
||||
var term: Terminal;
|
||||
if (gotty_term == "hterm") {
|
||||
term = new Hterm(elem);
|
||||
} else {
|
||||
term = new Xterm(elem);
|
||||
}
|
||||
const httpsEnabled = window.location.protocol == "https:";
|
||||
const url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws';
|
||||
const args = window.location.search;
|
||||
const factory = new ConnectionFactory(url, protocols);
|
||||
const wt = new WebTTY(term, factory, args, gotty_auth_token);
|
||||
const closer = wt.open();
|
||||
|
||||
window.addEventListener("unload", () => {
|
||||
closer();
|
||||
term.close();
|
||||
});
|
||||
};
|
60
js/src/websocket.ts
Normal file
60
js/src/websocket.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
export class ConnectionFactory {
|
||||
url: string;
|
||||
protocols: string[];
|
||||
|
||||
constructor(url: string, protocols: string[]) {
|
||||
this.url = url;
|
||||
this.protocols = protocols;
|
||||
};
|
||||
|
||||
create(): Connection {
|
||||
return new Connection(this.url, this.protocols);
|
||||
};
|
||||
}
|
||||
|
||||
export class Connection {
|
||||
bare: WebSocket;
|
||||
|
||||
|
||||
constructor(url: string, protocols: string[]) {
|
||||
this.bare = new WebSocket(url, protocols);
|
||||
}
|
||||
|
||||
open() {
|
||||
// nothing todo for websocket
|
||||
};
|
||||
|
||||
close() {
|
||||
this.bare.close();
|
||||
};
|
||||
|
||||
send(data: string) {
|
||||
this.bare.send(data);
|
||||
};
|
||||
|
||||
isOpen(): boolean {
|
||||
if (this.bare.readyState == WebSocket.CONNECTING ||
|
||||
this.bare.readyState == WebSocket.OPEN) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
onOpen(callback: () => void) {
|
||||
this.bare.onopen = (event) => {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
onReceive(callback: (data: string) => void) {
|
||||
this.bare.onmessage = (event) => {
|
||||
callback(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
onClose(callback: () => void) {
|
||||
this.bare.onclose = (event) => {
|
||||
callback();
|
||||
};
|
||||
};
|
||||
}
|
148
js/src/webtty.ts
Normal file
148
js/src/webtty.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
export const protocols = ["webtty"];
|
||||
|
||||
export const msgInputUnknown = '0';
|
||||
export const msgInput = '1';
|
||||
export const msgPing = '2';
|
||||
export const msgResizeTerminal = '3';
|
||||
|
||||
export const msgUnknownOutput = '0';
|
||||
export const msgOutput = '1';
|
||||
export const msgPong = '2';
|
||||
export const msgSetWindowTitle = '3';
|
||||
export const msgSetPreferences = '4';
|
||||
export const msgSetReconnect = '5';
|
||||
|
||||
|
||||
export interface Terminal {
|
||||
info(): { columns: number, rows: number };
|
||||
output(data: string): void;
|
||||
showMessage(message: string, timeout: number): void;
|
||||
removeMessage(): void;
|
||||
setWindowTitle(title: string): void;
|
||||
setPreferences(value: object): void;
|
||||
onInput(callback: (input: string) => void): void;
|
||||
onResize(callback: (colmuns: number, rows: number) => void): void;
|
||||
reset(): void;
|
||||
deactivate(): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
open(): void;
|
||||
close(): void;
|
||||
send(data: string): void;
|
||||
isOpen(): boolean;
|
||||
onOpen(callback: () => void): void;
|
||||
onReceive(callback: (data: string) => void): void;
|
||||
onClose(callback: () => void): void;
|
||||
}
|
||||
|
||||
export interface ConnectionFactory {
|
||||
create(): Connection;
|
||||
}
|
||||
|
||||
|
||||
export class WebTTY {
|
||||
term: Terminal;
|
||||
connectionFactory: ConnectionFactory;
|
||||
args: string;
|
||||
authToken: string;
|
||||
reconnect: number;
|
||||
|
||||
constructor(term: Terminal, connectionFactory: ConnectionFactory, args: string, authToken: string) {
|
||||
this.term = term;
|
||||
this.connectionFactory = connectionFactory;
|
||||
this.args = args;
|
||||
this.authToken = authToken;
|
||||
this.reconnect = -1;
|
||||
};
|
||||
|
||||
open() {
|
||||
let connection = this.connectionFactory.create();
|
||||
let pingTimer: number;
|
||||
let reconnectTimeout: number;
|
||||
|
||||
const setup = () => {
|
||||
connection.onOpen(() => {
|
||||
const termInfo = this.term.info();
|
||||
|
||||
connection.send(JSON.stringify(
|
||||
{
|
||||
Arguments: this.args,
|
||||
AuthToken: this.authToken,
|
||||
}
|
||||
));
|
||||
|
||||
|
||||
const resizeHandler = (colmuns: number, rows: number) => {
|
||||
connection.send(
|
||||
msgResizeTerminal + JSON.stringify(
|
||||
{
|
||||
columns: colmuns,
|
||||
rows: rows
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
this.term.onResize(resizeHandler);
|
||||
resizeHandler(termInfo.columns, termInfo.rows);
|
||||
|
||||
this.term.onInput(
|
||||
(input: string) => {
|
||||
connection.send(msgInput + input);
|
||||
}
|
||||
);
|
||||
|
||||
pingTimer = setInterval(() => {
|
||||
connection.send(msgPing)
|
||||
}, 30 * 1000);
|
||||
|
||||
});
|
||||
|
||||
connection.onReceive((data) => {
|
||||
const payload = data.slice(1);
|
||||
switch (data[0]) {
|
||||
case msgOutput:
|
||||
this.term.output(atob(payload));
|
||||
break;
|
||||
case msgPong:
|
||||
break;
|
||||
case msgSetWindowTitle:
|
||||
this.term.setWindowTitle(payload);
|
||||
break;
|
||||
case msgSetPreferences:
|
||||
const preferences = JSON.parse(payload);
|
||||
this.term.setPreferences(preferences);
|
||||
break;
|
||||
case msgSetReconnect:
|
||||
const autoReconnect = JSON.parse(payload);
|
||||
console.log("Enabling reconnect: " + autoReconnect + " seconds")
|
||||
this.reconnect = autoReconnect;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
connection.onClose(() => {
|
||||
clearInterval(pingTimer);
|
||||
this.term.deactivate();
|
||||
this.term.showMessage("Connection Closed", 0);
|
||||
if (this.reconnect > 0) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
connection = this.connectionFactory.create();
|
||||
this.term.reset();
|
||||
setup();
|
||||
}, this.reconnect * 1000);
|
||||
}
|
||||
});
|
||||
|
||||
connection.open();
|
||||
}
|
||||
|
||||
setup();
|
||||
return () => {
|
||||
clearTimeout(reconnectTimeout);
|
||||
connection.close();
|
||||
}
|
||||
};
|
||||
};
|
105
js/src/xterm.ts
Normal file
105
js/src/xterm.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import * as bare from "xterm";
|
||||
import { lib } from "libapps"
|
||||
|
||||
|
||||
bare.loadAddon("fit");
|
||||
|
||||
export class Xterm {
|
||||
elem: HTMLElement;
|
||||
term: bare;
|
||||
resizeListener: () => void;
|
||||
decoder: lib.UTF8Decoder;
|
||||
|
||||
message: HTMLElement;
|
||||
messageTimeout: number;
|
||||
messageTimer: number;
|
||||
|
||||
|
||||
constructor(elem: HTMLElement) {
|
||||
this.elem = elem;
|
||||
this.term = new bare();
|
||||
|
||||
this.message = elem.ownerDocument.createElement("div");
|
||||
this.message.className = "xterm-overlay";
|
||||
this.messageTimeout = 2000;
|
||||
|
||||
this.resizeListener = () => {
|
||||
this.term.fit();
|
||||
this.term.scrollToBottom();
|
||||
this.showMessage(String(this.term.cols) + "x" + String(this.term.rows), this.messageTimeout);
|
||||
};
|
||||
|
||||
this.term.on("open", () => {
|
||||
this.resizeListener();
|
||||
window.addEventListener("resize", () => { this.resizeListener(); });
|
||||
});
|
||||
|
||||
this.term.open(elem, true);
|
||||
|
||||
this.decoder = new lib.UTF8Decoder()
|
||||
};
|
||||
|
||||
info(): { columns: number, rows: number } {
|
||||
return { columns: this.term.cols, rows: this.term.rows };
|
||||
};
|
||||
|
||||
output(data: string) {
|
||||
this.term.write(this.decoder.decode(data));
|
||||
};
|
||||
|
||||
showMessage(message: string, timeout: number) {
|
||||
this.message.textContent = message;
|
||||
this.elem.appendChild(this.message);
|
||||
|
||||
if (this.messageTimer) {
|
||||
clearTimeout(this.messageTimer);
|
||||
}
|
||||
if (timeout > 0) {
|
||||
this.messageTimer = setTimeout(() => {
|
||||
this.elem.removeChild(this.message);
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
removeMessage(): void {
|
||||
if (this.message.parentNode == this.elem) {
|
||||
this.elem.removeChild(this.message);
|
||||
}
|
||||
}
|
||||
|
||||
setWindowTitle(title: string) {
|
||||
document.title = title;
|
||||
};
|
||||
|
||||
setPreferences(value: object) {
|
||||
};
|
||||
|
||||
onInput(callback: (input: string) => void) {
|
||||
this.term.on("data", (data) => {
|
||||
callback(data);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
onResize(callback: (colmuns: number, rows: number) => void) {
|
||||
this.term.on("resize", (data) => {
|
||||
callback(data.cols, data.rows);
|
||||
});
|
||||
};
|
||||
|
||||
deactivate(): void {
|
||||
this.term.off("data");
|
||||
this.term.off("resize");
|
||||
this.term.blur();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.removeMessage();
|
||||
this.term.clear();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
window.removeEventListener("resize", this.resizeListener);
|
||||
this.term.destroy();
|
||||
}
|
||||
}
|
20
js/tsconfig.json
Normal file
20
js/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals" : true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"outDir": "./dist/",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"target": "es5",
|
||||
"module": "commonJS",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["./typings/*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
51
js/typings/libapps/index.d.ts
vendored
Normal file
51
js/typings/libapps/index.d.ts
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
export declare namespace hterm {
|
||||
export class Terminal {
|
||||
io: IO;
|
||||
onTerminalReady: () => void;
|
||||
|
||||
constructor();
|
||||
getPrefs(): Prefs;
|
||||
decorate(HTMLElement);
|
||||
installKeyboard(): void;
|
||||
uninstallKeyboard(): void;
|
||||
setWindowTitle(title: string): void;
|
||||
reset(): void;
|
||||
softReset(): void;
|
||||
}
|
||||
|
||||
export class IO {
|
||||
writeUTF8: ((data: string) => void);
|
||||
writeUTF16: ((data: string) => void);
|
||||
onVTKeystroke: ((data: string) => void) | null;
|
||||
sendString: ((data: string) => void) | null;
|
||||
onTerminalResize: ((columns: number, rows: number) => void) | null;
|
||||
|
||||
push(): IO;
|
||||
writeUTF(data: string);
|
||||
showOverlay(message: string, timeout: number | null);
|
||||
}
|
||||
|
||||
export class Prefs {
|
||||
set(key: string, value: string): void;
|
||||
}
|
||||
|
||||
export var defaultStorage: lib.Storage;
|
||||
}
|
||||
|
||||
export declare namespace lib {
|
||||
export interface Storage {
|
||||
}
|
||||
|
||||
export interface Memory {
|
||||
new (): Storage;
|
||||
Memory(): Storage
|
||||
}
|
||||
|
||||
export var Storage: {
|
||||
Memory: Memory
|
||||
}
|
||||
|
||||
export class UTF8Decoder {
|
||||
decode(str: string)
|
||||
}
|
||||
}
|
29
js/webpack.config.js
Normal file
29
js/webpack.config.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: "./src/main.ts",
|
||||
output: {
|
||||
filename: "./dist/gotty-bundle.js"
|
||||
},
|
||||
devtool: "source-map",
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js"],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader",
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: /node_modules/,
|
||||
loader: 'license-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new UglifyJSPlugin()
|
||||
]
|
||||
};
|
1
libapps
1
libapps
|
@ -1 +0,0 @@
|
|||
Subproject commit 9dc111b58c7e5af86c1b3a0cb3b04a22a8dcf584
|
253
main.go
253
main.go
|
@ -1,135 +1,125 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
|
||||
"github.com/yudai/gotty/app"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"github.com/yudai/gotty/backend/localcommand"
|
||||
"github.com/yudai/gotty/pkg/homedir"
|
||||
"github.com/yudai/gotty/server"
|
||||
"github.com/yudai/gotty/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := cli.NewApp()
|
||||
cmd.Version = "0.0.4"
|
||||
cmd.Name = "gotty"
|
||||
cmd.Usage = "Share your terminal as a web application"
|
||||
cmd.HideHelp = true
|
||||
cmd.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "addr, a",
|
||||
Value: "",
|
||||
Usage: "IP address to listen",
|
||||
EnvVar: "GOTTY_ADDR",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "port, p",
|
||||
Value: "8080",
|
||||
Usage: "Port number to listen",
|
||||
EnvVar: "GOTTY_PORT",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "permit-write, w",
|
||||
Usage: "Permit clients to write to the TTY (BE CAREFUL)",
|
||||
EnvVar: "GOTTY_PERMIT_WRITE",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "credential, c",
|
||||
Usage: "Credential for Basic Authentication (ex: user:pass)",
|
||||
EnvVar: "GOTTY_CREDENTIAL",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "random-url, r",
|
||||
Usage: "Add a random string to the URL",
|
||||
EnvVar: "GOTTY_RANDOM_URL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "profile-file, f",
|
||||
Value: app.DefaultProfileFilePath,
|
||||
Usage: "Path to profile file",
|
||||
EnvVar: "GOTTY_PROFILE_FILE",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "enable-tls, t",
|
||||
Usage: "Enable TLS/SSL",
|
||||
EnvVar: "GOTTY_ENABLE_TLS",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tls-crt",
|
||||
Value: app.DefaultTLSCrtPath,
|
||||
Usage: "TLS/SSL crt",
|
||||
EnvVar: "GOTTY_TLS_CRT",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tls-key",
|
||||
Value: app.DefaultTLSKeyPath,
|
||||
Usage: "TLS/SSL key",
|
||||
EnvVar: "GOTTY_TLS_KEY",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "title-format",
|
||||
Value: "GoTTY - {{ .Command }} ({{ .Hostname }})",
|
||||
Usage: "Title format of browser window",
|
||||
EnvVar: "GOTTY_TITLE_FORMAT",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "auto-reconnect",
|
||||
Value: -1,
|
||||
Usage: "Seconds to automatically reconnect to the server when the connection is closed (default: disabled)",
|
||||
EnvVar: "GOTTY_AUTO_RECONNECT",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "once",
|
||||
Usage: "Accept only one client and exit on disconnection",
|
||||
EnvVar: "GOTTY_ONCE",
|
||||
},
|
||||
}
|
||||
cmd.Action = func(c *cli.Context) {
|
||||
if len(c.Args()) == 0 {
|
||||
fmt.Println("Error: No command given.\n")
|
||||
cli.ShowAppHelp(c)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
app, err := app.New(
|
||||
app.Options{
|
||||
c.String("addr"),
|
||||
c.String("port"),
|
||||
c.Bool("permit-write"),
|
||||
c.String("credential"),
|
||||
c.Bool("random-url"),
|
||||
c.String("profile-file"),
|
||||
c.Bool("enable-tls"),
|
||||
c.String("tls-cert"),
|
||||
c.String("tls-key"),
|
||||
c.String("title-format"),
|
||||
c.Int("auto-reconnect"),
|
||||
c.Bool("once"),
|
||||
c.Args(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
registerSignals(app)
|
||||
|
||||
err = app.Run()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(3)
|
||||
}
|
||||
}
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "gotty"
|
||||
app.Version = Version + "+" + CommitID
|
||||
app.Usage = "Share your terminal as a web application"
|
||||
app.HideHelp = true
|
||||
cli.AppHelpTemplate = helpTemplate
|
||||
|
||||
cmd.Run(os.Args)
|
||||
appOptions := &server.Options{}
|
||||
if err := utils.ApplyDefaultValues(appOptions); err != nil {
|
||||
exit(err, 1)
|
||||
}
|
||||
backendOptions := &localcommand.Options{}
|
||||
if err := utils.ApplyDefaultValues(backendOptions); err != nil {
|
||||
exit(err, 1)
|
||||
}
|
||||
|
||||
cliFlags, flagMappings, err := utils.GenerateFlags(appOptions, backendOptions)
|
||||
if err != nil {
|
||||
exit(err, 3)
|
||||
}
|
||||
|
||||
app.Flags = append(
|
||||
cliFlags,
|
||||
cli.StringFlag{
|
||||
Name: "config",
|
||||
Value: "~/.gotty",
|
||||
Usage: "Config file path",
|
||||
EnvVar: "GOTTY_CONFIG",
|
||||
},
|
||||
)
|
||||
|
||||
app.Action = func(c *cli.Context) {
|
||||
if len(c.Args()) == 0 {
|
||||
msg := "Error: No command given."
|
||||
cli.ShowAppHelp(c)
|
||||
exit(fmt.Errorf(msg), 1)
|
||||
}
|
||||
|
||||
configFile := c.String("config")
|
||||
_, err := os.Stat(homedir.Expand(configFile))
|
||||
if configFile != "~/.gotty" || !os.IsNotExist(err) {
|
||||
if err := utils.ApplyConfigFile(configFile, appOptions, backendOptions); err != nil {
|
||||
exit(err, 2)
|
||||
}
|
||||
}
|
||||
|
||||
utils.ApplyFlags(cliFlags, flagMappings, c, appOptions, backendOptions)
|
||||
|
||||
appOptions.EnableBasicAuth = c.IsSet("credential")
|
||||
appOptions.EnableTLSClientAuth = c.IsSet("tls-ca-crt")
|
||||
|
||||
err = appOptions.Validate()
|
||||
if err != nil {
|
||||
exit(err, 6)
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
factory, err := localcommand.NewFactory(args[0], args[1:], backendOptions)
|
||||
if err != nil {
|
||||
exit(err, 3)
|
||||
}
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
appOptions.TitleVariables = map[string]interface{}{
|
||||
"command": args[0],
|
||||
"argv": args[1:],
|
||||
"hostname": hostname,
|
||||
}
|
||||
|
||||
srv, err := server.New(factory, appOptions)
|
||||
if err != nil {
|
||||
exit(err, 3)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
gCtx, gCancel := context.WithCancel(context.Background())
|
||||
|
||||
log.Printf("GoTTY is starting with command: %s", strings.Join(args, " "))
|
||||
|
||||
errs := make(chan error, 1)
|
||||
go func() {
|
||||
errs <- srv.Run(ctx, server.WithGracefullContext(gCtx))
|
||||
}()
|
||||
err = waitSignals(errs, cancel, gCancel)
|
||||
|
||||
if err != nil && err != context.Canceled {
|
||||
fmt.Printf("Error: %s\n", err)
|
||||
exit(err, 8)
|
||||
}
|
||||
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func registerSignals(app *app.App) {
|
||||
func exit(err error, code int) {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func waitSignals(errs chan error, cancel context.CancelFunc, gracefullCancel context.CancelFunc) error {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(
|
||||
sigChan,
|
||||
|
@ -137,15 +127,26 @@ func registerSignals(app *app.App) {
|
|||
syscall.SIGTERM,
|
||||
)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
s := <-sigChan
|
||||
switch s {
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
if !app.Exit() {
|
||||
os.Exit(4)
|
||||
}
|
||||
select {
|
||||
case err := <-errs:
|
||||
return err
|
||||
|
||||
case s := <-sigChan:
|
||||
switch s {
|
||||
case syscall.SIGINT:
|
||||
gracefullCancel()
|
||||
fmt.Println("C-C to force close")
|
||||
select {
|
||||
case err := <-errs:
|
||||
return err
|
||||
case <-sigChan:
|
||||
fmt.Println("Force closing...")
|
||||
cancel()
|
||||
return <-errs
|
||||
}
|
||||
default:
|
||||
cancel()
|
||||
return <-errs
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
13
pkg/homedir/expand.go
Normal file
13
pkg/homedir/expand.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package homedir
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func Expand(path string) string {
|
||||
if path[0:2] == "~/" {
|
||||
return os.Getenv("HOME") + path[1:]
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
}
|
18
pkg/randomstring/generate.go
Normal file
18
pkg/randomstring/generate.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package randomstring
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Generate(length int) string {
|
||||
const base = 36
|
||||
size := big.NewInt(base)
|
||||
n := make([]byte, length)
|
||||
for i, _ := range n {
|
||||
c, _ := rand.Int(rand.Reader, size)
|
||||
n[i] = strconv.FormatInt(c.Int64(), base)[0]
|
||||
}
|
||||
return string(n)
|
||||
}
|
BIN
resources/favicon.png
Normal file
BIN
resources/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 863 B |
|
@ -1,87 +0,0 @@
|
|||
(function() {
|
||||
var httpsEnabled = window.location.protocol == "https:";
|
||||
var url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws';
|
||||
var protocols = ["gotty"];
|
||||
var autoReconnect = -1;
|
||||
|
||||
var openWs = function() {
|
||||
var ws = new WebSocket(url, protocols);
|
||||
|
||||
var term;
|
||||
|
||||
ws.onopen = function(event) {
|
||||
hterm.defaultStorage = new lib.Storage.Local();
|
||||
hterm.defaultStorage.clear();
|
||||
|
||||
term = new hterm.Terminal();
|
||||
|
||||
term.getPrefs().set("send-encoding", "raw");
|
||||
|
||||
term.onTerminalReady = function() {
|
||||
var io = term.io.push();
|
||||
|
||||
io.onVTKeystroke = function(str) {
|
||||
ws.send("0" + str);
|
||||
};
|
||||
|
||||
io.sendString = io.onVTKeystroke;
|
||||
|
||||
io.onTerminalResize = function(columns, rows) {
|
||||
ws.send(
|
||||
"1" + JSON.stringify(
|
||||
{
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
}
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
term.installKeyboard();
|
||||
};
|
||||
|
||||
term.decorate(document.body);
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
data = event.data.slice(1);
|
||||
switch(event.data[0]) {
|
||||
case '0':
|
||||
term.io.writeUTF16(data);
|
||||
break;
|
||||
case '1':
|
||||
term.setWindowTitle(data);
|
||||
break;
|
||||
case '2':
|
||||
preferences = JSON.parse(data);
|
||||
Object.keys(preferences).forEach(function(key) {
|
||||
term.getPrefs().set(key, preferences[key]);
|
||||
});
|
||||
break;
|
||||
case '3':
|
||||
autoReconnect = JSON.parse(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = function(event) {
|
||||
if (term) {
|
||||
term.uninstallKeyboard();
|
||||
term.io.showOverlay("Connection Closed", null);
|
||||
}
|
||||
tryReconnect();
|
||||
}
|
||||
|
||||
ws.onerror = function(error) {
|
||||
tryReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
openWs();
|
||||
|
||||
var tryReconnect = function() {
|
||||
if (autoReconnect >= 0) {
|
||||
setTimeout(openWs, autoReconnect * 1000);
|
||||
}
|
||||
}
|
||||
})()
|
7
resources/index.css
Normal file
7
resources/index.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
html, body, #terminal {
|
||||
background: black;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0%;
|
||||
margin: 0%;
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GoTTY</title>
|
||||
<style>body {position: absolute; height: 100%; width: 100%; margin: 0px;}</style>
|
||||
<title>{{ .title }}</title>
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
<link rel="stylesheet" href="./css/index.css" />
|
||||
<link rel="stylesheet" href="./css/xterm.css" />
|
||||
<link rel="stylesheet" href="./css/xterm_customize.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="hterm.js"></script>
|
||||
<script src="gotty.js"></script>
|
||||
<div id="terminal"></div>
|
||||
<script src="./auth_token.js"></script>
|
||||
<script src="./config.js"></script>
|
||||
<script src="./js/gotty-bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
19
resources/xterm_customize.css
Normal file
19
resources/xterm_customize.css
Normal file
|
@ -0,0 +1,19 @@
|
|||
.terminal {
|
||||
font-family: "DejaVu Sans Mono", "Everson Mono", FreeMono, Menlo, Terminal, monospace, "Apple Symbols";
|
||||
}
|
||||
|
||||
.xterm-overlay {
|
||||
font-family: "DejaVu Sans Mono", "Everson Mono", FreeMono, Menlo, Terminal, monospace, "Apple Symbols";
|
||||
border-radius: 15px;
|
||||
font-size: xx-large;
|
||||
color: black;
|
||||
background: white;
|
||||
opacity: 0.75;
|
||||
padding: 0.2em 0.5em 0.2em 0.5em;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
user-select: none;
|
||||
transition: opacity 180ms ease-in;
|
||||
}
|
379
server/asset.go
Normal file
379
server/asset.go
Normal file
File diff suppressed because one or more lines are too long
70
server/handler_atomic.go
Normal file
70
server/handler_atomic.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type counter struct {
|
||||
duration time.Duration
|
||||
zeroTimer *time.Timer
|
||||
wg sync.WaitGroup
|
||||
connections int
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func newCounter(duration time.Duration) *counter {
|
||||
zeroTimer := time.NewTimer(duration)
|
||||
|
||||
// when duration is 0, drain the expire event here
|
||||
// so that user will never get the event.
|
||||
if duration == 0 {
|
||||
<-zeroTimer.C
|
||||
}
|
||||
|
||||
return &counter{
|
||||
duration: duration,
|
||||
zeroTimer: zeroTimer,
|
||||
}
|
||||
}
|
||||
|
||||
func (counter *counter) add(n int) int {
|
||||
counter.mutex.Lock()
|
||||
defer counter.mutex.Unlock()
|
||||
|
||||
if counter.duration > 0 {
|
||||
counter.zeroTimer.Stop()
|
||||
}
|
||||
counter.wg.Add(n)
|
||||
counter.connections += n
|
||||
|
||||
return counter.connections
|
||||
}
|
||||
|
||||
func (counter *counter) done() int {
|
||||
counter.mutex.Lock()
|
||||
defer counter.mutex.Unlock()
|
||||
|
||||
counter.connections--
|
||||
counter.wg.Done()
|
||||
if counter.connections == 0 && counter.duration > 0 {
|
||||
counter.zeroTimer.Reset(counter.duration)
|
||||
}
|
||||
|
||||
return counter.connections
|
||||
}
|
||||
|
||||
func (counter *counter) count() int {
|
||||
counter.mutex.Lock()
|
||||
defer counter.mutex.Unlock()
|
||||
|
||||
return counter.connections
|
||||
}
|
||||
|
||||
func (counter *counter) wait() {
|
||||
counter.wg.Wait()
|
||||
}
|
||||
|
||||
func (counter *counter) timer() *time.Timer {
|
||||
return counter.zeroTimer
|
||||
}
|
235
server/handlers.go
Normal file
235
server/handlers.go
Normal file
|
@ -0,0 +1,235 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/yudai/gotty/webtty"
|
||||
)
|
||||
|
||||
func (server *Server) generateHandleWS(ctx context.Context, cancel context.CancelFunc, counter *counter) http.HandlerFunc {
|
||||
once := new(int64)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-counter.timer().C:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if server.options.Once {
|
||||
success := atomic.CompareAndSwapInt64(once, 0, 1)
|
||||
if !success {
|
||||
http.Error(w, "Server is shutting down", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
num := counter.add(1)
|
||||
closeReason := "unknown reason"
|
||||
|
||||
defer func() {
|
||||
num := counter.done()
|
||||
log.Printf(
|
||||
"Connection closed by %s: %s, connections: %d/%d",
|
||||
closeReason, r.RemoteAddr, num, server.options.MaxConnection,
|
||||
)
|
||||
|
||||
if server.options.Once {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
if int64(server.options.MaxConnection) != 0 {
|
||||
if num > server.options.MaxConnection {
|
||||
closeReason = "exceeding max number of connections"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("New client connected: %s, connections: %d/%d", r.RemoteAddr, num, server.options.MaxConnection)
|
||||
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := server.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
closeReason = err.Error()
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = server.processWSConn(ctx, conn)
|
||||
|
||||
switch err {
|
||||
case ctx.Err():
|
||||
closeReason = "cancelation"
|
||||
case webtty.ErrSlaveClosed:
|
||||
closeReason = server.factory.Name()
|
||||
case webtty.ErrMasterClosed:
|
||||
closeReason = "client"
|
||||
default:
|
||||
closeReason = fmt.Sprintf("an error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) processWSConn(ctx context.Context, conn *websocket.Conn) error {
|
||||
typ, initLine, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to authenticate websocket connection")
|
||||
}
|
||||
if typ != websocket.TextMessage {
|
||||
return errors.New("failed to authenticate websocket connection: invalid message type")
|
||||
}
|
||||
|
||||
var init InitMessage
|
||||
err = json.Unmarshal(initLine, &init)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to authenticate websocket connection")
|
||||
}
|
||||
if init.AuthToken != server.options.Credential {
|
||||
return errors.New("failed to authenticate websocket connection")
|
||||
}
|
||||
|
||||
queryPath := "?"
|
||||
if server.options.PermitArguments && init.Arguments != "" {
|
||||
queryPath = init.Arguments
|
||||
}
|
||||
|
||||
query, err := url.Parse(queryPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse arguments")
|
||||
}
|
||||
params := query.Query()
|
||||
var slave Slave
|
||||
slave, err = server.factory.New(params)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to create backend")
|
||||
}
|
||||
defer slave.Close()
|
||||
|
||||
titleVars := server.titleVariables(
|
||||
[]string{"server", "master", "slave"},
|
||||
map[string]map[string]interface{}{
|
||||
"server": server.options.TitleVariables,
|
||||
"master": map[string]interface{}{
|
||||
"remote_addr": conn.RemoteAddr(),
|
||||
},
|
||||
"slave": slave.WindowTitleVariables(),
|
||||
},
|
||||
)
|
||||
|
||||
titleBuf := new(bytes.Buffer)
|
||||
err = server.titleTemplate.Execute(titleBuf, titleVars)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to fill window title template")
|
||||
}
|
||||
|
||||
opts := []webtty.Option{
|
||||
webtty.WithWindowTitle(titleBuf.Bytes()),
|
||||
}
|
||||
if server.options.PermitWrite {
|
||||
opts = append(opts, webtty.WithPermitWrite())
|
||||
}
|
||||
if server.options.EnableReconnect {
|
||||
opts = append(opts, webtty.WithReconnect(server.options.ReconnectTime))
|
||||
}
|
||||
if server.options.Width > 0 {
|
||||
opts = append(opts, webtty.WithFixedColumns(server.options.Width))
|
||||
}
|
||||
if server.options.Height > 0 {
|
||||
opts = append(opts, webtty.WithFixedRows(server.options.Height))
|
||||
}
|
||||
if server.options.Preferences != nil {
|
||||
opts = append(opts, webtty.WithMasterPreferences(server.options.Preferences))
|
||||
}
|
||||
|
||||
tty, err := webtty.New(&wsWrapper{conn}, slave, opts...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to create webtty")
|
||||
}
|
||||
|
||||
err = tty.Run(ctx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (server *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
titleVars := server.titleVariables(
|
||||
[]string{"server", "master"},
|
||||
map[string]map[string]interface{}{
|
||||
"server": server.options.TitleVariables,
|
||||
"master": map[string]interface{}{
|
||||
"remote_addr": r.RemoteAddr,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
titleBuf := new(bytes.Buffer)
|
||||
err := server.titleTemplate.Execute(titleBuf, titleVars)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
indexVars := map[string]interface{}{
|
||||
"title": titleBuf.String(),
|
||||
}
|
||||
|
||||
indexBuf := new(bytes.Buffer)
|
||||
err = server.indexTemplate.Execute(indexBuf, indexVars)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(indexBuf.Bytes())
|
||||
}
|
||||
|
||||
func (server *Server) handleAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
// @TODO hashing?
|
||||
w.Write([]byte("var gotty_auth_token = '" + server.options.Credential + "';"))
|
||||
}
|
||||
|
||||
func (server *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
w.Write([]byte("var gotty_term = '" + server.options.Term + "';"))
|
||||
}
|
||||
|
||||
// titleVariables merges maps in a specified order.
|
||||
// varUnits are name-keyed maps, whose names will be iterated using order.
|
||||
func (server *Server) titleVariables(order []string, varUnits map[string]map[string]interface{}) map[string]interface{} {
|
||||
titleVars := map[string]interface{}{}
|
||||
|
||||
for _, name := range order {
|
||||
vars, ok := varUnits[name]
|
||||
if !ok {
|
||||
panic("title variable name error")
|
||||
}
|
||||
for key, val := range vars {
|
||||
titleVars[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
// safe net for conflicted keys
|
||||
for _, name := range order {
|
||||
titleVars[name] = varUnits[name]
|
||||
}
|
||||
|
||||
return titleVars
|
||||
}
|
6
server/init_message.go
Normal file
6
server/init_message.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package server
|
||||
|
||||
type InitMessage struct {
|
||||
Arguments string `json:"Arguments,omitempty"`
|
||||
AuthToken string `json:"AuthToken,omitempty"`
|
||||
}
|
28
server/list_address.go
Normal file
28
server/list_address.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func listAddresses() (addresses []string) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
addresses = make([]string, 0, len(ifaces))
|
||||
|
||||
for _, iface := range ifaces {
|
||||
ifAddrs, _ := iface.Addrs()
|
||||
for _, ifAddr := range ifAddrs {
|
||||
switch v := ifAddr.(type) {
|
||||
case *net.IPNet:
|
||||
addresses = append(addresses, v.IP.String())
|
||||
case *net.IPAddr:
|
||||
addresses = append(addresses, v.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addresses
|
||||
}
|
23
server/log_response_writer.go
Normal file
23
server/log_response_writer.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type logResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (w *logResponseWriter) WriteHeader(status int) {
|
||||
w.status = status
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (w *logResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hj, _ := w.ResponseWriter.(http.Hijacker)
|
||||
w.status = http.StatusSwitchingProtocols
|
||||
return hj.Hijack()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue